@gongfu/claude-integration
Version:
Claude Code integration toolkit for Gongfu workflow system
1,206 lines (1,184 loc) • 33.2 kB
JavaScript
/**
* @gongfu/claude-integration
* Claude Code integration toolkit for Gongfu workflow system
*
* Copyright (c) 2025 Foundation
* Licensed under MIT
*/
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined")
return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
// src/core/claude-controller.ts
import * as fs from "fs-extra";
import * as path from "path";
import { spawn } from "child_process";
import { EventEmitter } from "events";
// src/utils/logger.ts
import pc from "picocolors";
function createLogger(prefix) {
const p = prefix ? `[${prefix}] ` : "";
return {
info: (message, ...args) => {
console.log(pc.blue(p + message), ...args);
},
warn: (message, ...args) => {
console.warn(pc.yellow(p + message), ...args);
},
error: (message, ...args) => {
console.error(pc.red(p + message), ...args);
},
debug: (message, ...args) => {
if (process.env.DEBUG) {
console.log(pc.gray(p + message), ...args);
}
}
};
}
var logger = createLogger("claude-integration");
// src/core/claude-controller.ts
import { v4 as uuidv4 } from "uuid";
var ClaudeController = class extends EventEmitter {
constructor(options = {}) {
super();
this.options = options;
this.projectRoot = options.projectRoot || process.cwd();
this.claudeDir = path.join(this.projectRoot, ".claude");
this.currentTaskFile = path.join(this.claudeDir, "current-task.md");
}
projectRoot;
claudeDir;
currentTaskFile;
process;
outputBuffer = "";
/**
* Assign a task to Claude Code
*/
async assignTask(taskDescription, options = {}) {
await fs.ensureDir(this.claudeDir);
const taskId = uuidv4();
const task = {
id: taskId,
content: taskDescription,
priority: options.priority || "medium",
status: "in_progress",
createdAt: /* @__PURE__ */ new Date(),
updatedAt: /* @__PURE__ */ new Date()
};
const taskContent = this.createTaskInstruction(task);
await fs.writeFile(this.currentTaskFile, taskContent);
const signalFile = path.join(this.claudeDir, "new-task.signal");
await fs.writeFile(signalFile, JSON.stringify({
taskId,
timestamp: (/* @__PURE__ */ new Date()).toISOString()
}));
setTimeout(() => {
fs.remove(signalFile).catch(() => {
});
}, 5e3);
logger.info(`Task assigned: ${taskId}`);
this.emit("task-assigned", { taskId, task });
return taskId;
}
/**
* Create task instruction content
*/
createTaskInstruction(task) {
return `# \u{1F3AF} Current Task
## Task ID: ${task.id}
## Description
${task.content}
## Priority: ${task.priority}
## Instructions
Please complete this task following these guidelines:
1. **Understand**: Analyze the requirements carefully
2. **Plan**: Create a step-by-step approach
3. **Implement**: Write clean, maintainable code
4. **Test**: Ensure the solution works correctly
5. **Document**: Update relevant documentation
## Completion
When you finish this task:
1. Run tests to ensure everything works
2. Update the task status using the todo tool
3. Commit your changes with a descriptive message
## Context
- Project: ${path.basename(this.projectRoot)}
- Started: ${(/* @__PURE__ */ new Date()).toISOString()}
- Workflow integration enabled
---
*This task was assigned by the Gongfu workflow system*
`;
}
/**
* Start Claude Code in background mode
*/
async startInBackground() {
if (this.process) {
throw new Error("Claude Code is already running");
}
const wrapperScript = path.join(this.claudeDir, "claude-wrapper.sh");
const wrapperContent = `#!/bin/bash
cd "${this.projectRoot}"
export GONGFU_TASK_MODE=1
claude code 2>&1 | tee -a "${path.join(this.claudeDir, "logs", "claude-output.log")}"
`;
await fs.writeFile(wrapperScript, wrapperContent);
await fs.chmod(wrapperScript, "755");
this.process = spawn("bash", [wrapperScript], {
detached: true,
stdio: ["ignore", "pipe", "pipe"],
cwd: this.projectRoot
});
if (this.options.logOutput) {
this.process.stdout?.on("data", (data) => {
this.outputBuffer += data.toString();
this.parseOutput(data.toString());
});
this.process.stderr?.on("data", (data) => {
logger.error("Claude Code error:", data.toString());
});
}
this.process.on("exit", (code) => {
logger.info(`Claude Code exited with code ${code}`);
this.process = void 0;
this.emit("process-exit", code);
});
this.process.unref();
logger.info("Claude Code started in background");
}
/**
* Parse Claude Code output
*/
parseOutput(output) {
const lines = output.split("\n");
for (const line of lines) {
if (line.includes("Running:") || line.includes("Executing:")) {
this.emit("tool-use", {
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
command: line
});
}
if (line.includes("Modified:") || line.includes("Created:")) {
this.emit("file-change", {
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
file: line
});
}
if (line.includes("Thinking:") || line.includes("Planning:")) {
this.emit("thinking", {
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
thought: line
});
}
}
}
/**
* Get Claude Code status
*/
async getStatus() {
const status = {
isActive: false,
recentActivity: []
};
if (this.process && !this.process.killed) {
status.isActive = true;
status.processId = this.process.pid;
}
if (await fs.pathExists(this.currentTaskFile)) {
const content = await fs.readFile(this.currentTaskFile, "utf-8");
const idMatch = content.match(/## Task ID: (.+)/);
const descMatch = content.match(/## Description\n(.+)/);
const startMatch = content.match(/- Started: (.+)/);
if (idMatch && descMatch) {
status.currentTask = {
id: idMatch[1],
content: descMatch[1],
startedAt: startMatch ? startMatch[1] : (/* @__PURE__ */ new Date()).toISOString()
};
}
}
const activityLog = path.join(this.claudeDir, "logs", "activity.log");
if (await fs.pathExists(activityLog)) {
const content = await fs.readFile(activityLog, "utf-8");
const lines = content.split("\n").filter((line) => line.trim()).slice(-10);
for (const line of lines) {
const match = line.match(/^\[(.+?)\] (\w+): (.+)$/);
if (match) {
status.recentActivity.push({
timestamp: match[1],
type: match[2],
description: match[3]
});
}
}
}
return status;
}
/**
* Setup enhanced hooks for Claude Code
*/
async setupHooks(enhanced = false) {
const settingsPath = path.join(this.claudeDir, "settings.local.json");
let settings = {};
if (await fs.pathExists(settingsPath)) {
settings = await fs.readJson(settingsPath);
}
const logsDir = path.join(this.claudeDir, "logs");
await fs.ensureDir(logsDir);
if (enhanced) {
settings.hooks = {
...settings.hooks,
PreToolUse: [
{
matcher: ".*",
hooks: [{
type: "command",
command: `echo "[$(date '+%Y-%m-%d %H:%M:%S')] TOOL_START: {tool} | {args}" >> "${path.join(logsDir, "activity.log")}"`,
timeout: 1
}]
}
],
PostToolUse: [
...settings.hooks?.PostToolUse || [],
{
matcher: ".*",
hooks: [{
type: "command",
command: `echo "[$(date '+%Y-%m-%d %H:%M:%S')] TOOL_END: {tool} | Success: {success}" >> "${path.join(logsDir, "activity.log")}"`,
timeout: 1
}]
},
{
matcher: "^(Edit|Write|MultiEdit)",
hooks: [{
type: "command",
command: `cd "${this.projectRoot}" && npx /workflow-cli claude log --type file-change --data "{file_path}"`,
timeout: 2
}]
}
],
Start: [{
hooks: [{
type: "command",
command: `cd "${this.projectRoot}" && npx /workflow-cli claude log --type session-start`,
timeout: 1
}]
}],
Stop: [{
hooks: [{
type: "command",
command: `cd "${this.projectRoot}" && npx /workflow-cli claude log --type session-end`,
timeout: 1
}]
}]
};
}
await fs.writeJson(settingsPath, settings, { spaces: 2 });
logger.info("Claude Code hooks configured");
}
/**
* Stop Claude Code
*/
async stop() {
if (this.process && !this.process.killed) {
this.process.kill("SIGTERM");
logger.info("Claude Code stopped");
}
}
};
// src/core/claude-monitor.ts
import { EventEmitter as EventEmitter2 } from "events";
import * as fs2 from "fs-extra";
import * as path2 from "path";
import { watch } from "chokidar";
import express from "express";
import { v4 as uuidv42 } from "uuid";
var ClaudeMonitor = class extends EventEmitter2 {
constructor(options = {}) {
super();
this.options = options;
this.projectRoot = options.projectRoot || process.cwd();
this.claudeDir = path2.join(this.projectRoot, ".claude");
this.logsDir = path2.join(this.claudeDir, "logs");
}
projectRoot;
claudeDir;
logsDir;
watcher;
sseServer;
sseClients = /* @__PURE__ */ new Set();
activityBuffer = [];
maxBufferSize = 1e3;
/**
* Start monitoring Claude Code activity
*/
async start() {
await fs2.ensureDir(this.logsDir);
this.startFileWatcher();
if (this.options.outputFormat === "sse") {
await this.startSSEServer();
}
await this.parseExistingLogs();
logger.info("Claude monitor started");
}
/**
* Stop monitoring
*/
async stop() {
if (this.watcher) {
await this.watcher.close();
}
if (this.sseServer) {
this.sseServer.close();
}
logger.info("Claude monitor stopped");
}
/**
* Log an activity
*/
async logActivity(type, data) {
const activity = {
id: uuidv42(),
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
type,
data,
source: "api"
};
const logFile = path2.join(this.logsDir, "activity.log");
const logLine = `[${activity.timestamp}] ${type}: ${JSON.stringify(data)}
`;
await fs2.appendFile(logFile, logLine);
this.emitActivity(activity);
}
/**
* Start file watcher
*/
startFileWatcher() {
this.watcher = watch(this.claudeDir, {
persistent: true,
ignoreInitial: true,
depth: 2
});
this.watcher.on("change", async (filePath) => {
if (filePath.includes("current-task.md")) {
const content = await fs2.readFile(filePath, "utf-8");
this.emitActivity({
id: uuidv42(),
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
type: "task-update",
data: { file: filePath, content },
source: "file"
});
}
});
this.watcher.on("add", async (filePath) => {
if (filePath.includes("logs/")) {
await this.parseLogFile(filePath);
}
});
this.watcher.on("change", async (filePath) => {
if (filePath.includes("logs/")) {
await this.parseLogFile(filePath, true);
}
});
}
/**
* Parse existing log files
*/
async parseExistingLogs() {
const logFiles = await fs2.readdir(this.logsDir).catch(() => []);
for (const file of logFiles) {
if (file.endsWith(".log")) {
await this.parseLogFile(path2.join(this.logsDir, file));
}
}
}
/**
* Parse a log file
*/
async parseLogFile(filePath, tailOnly = false) {
try {
const content = await fs2.readFile(filePath, "utf-8");
const lines = content.split("\n").filter((line) => line.trim());
const linesToProcess = tailOnly ? lines.slice(-10) : lines;
for (const line of linesToProcess) {
const activity = this.parseLogLine(line);
if (activity) {
this.emitActivity(activity);
}
}
} catch (error) {
logger.error(`Failed to parse log file ${filePath}:`, error);
}
}
/**
* Parse a single log line
*/
parseLogLine(line) {
const patterns = [
// Standard activity log
/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+): (.+)$/,
// Tool usage logs
/^\[(.+?)\] (PRE_TOOL|POST_TOOL): (\w+) \| (.+)$/,
// Session logs
/^\[(.+?)\] (SESSION_START|SESSION_END)$/
];
for (const pattern of patterns) {
const match = line.match(pattern);
if (match) {
return {
id: uuidv42(),
timestamp: match[1],
type: match[2].toLowerCase().replace(/_/g, "-"),
data: match[3] ? this.parseLogData(match[3]) : {},
source: "hook"
};
}
}
return null;
}
/**
* Parse log data
*/
parseLogData(data) {
try {
return JSON.parse(data);
} catch {
const pairs = data.split(" | ");
const result = {};
for (const pair of pairs) {
const [key, value] = pair.split(": ");
if (key && value) {
result[key.toLowerCase()] = value;
}
}
return Object.keys(result).length > 0 ? result : data;
}
}
/**
* Emit activity to appropriate output
*/
emitActivity(activity) {
this.activityBuffer.push(activity);
if (this.activityBuffer.length > this.maxBufferSize) {
this.activityBuffer.shift();
}
this.emit("activity", activity);
switch (this.options.outputFormat) {
case "json":
console.log(JSON.stringify(activity));
break;
case "text":
console.log(`[${activity.timestamp}] ${activity.type}: ${JSON.stringify(activity.data)}`);
break;
case "sse":
this.broadcastSSE(activity);
break;
}
if (activity.type === "post-tool" && activity.data.tool === "TodoWrite") {
this.emit("task-completed", activity);
}
}
/**
* Start SSE server
*/
async startSSEServer() {
const app = express();
const port = this.options.ssePort || 3002;
app.get("/claude/events", (req, res) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*"
});
res.write(`data: ${JSON.stringify({ type: "connected" })}
`);
for (const activity of this.activityBuffer) {
res.write(`data: ${JSON.stringify(activity)}
`);
}
this.sseClients.add(res);
req.on("close", () => {
this.sseClients.delete(res);
});
});
app.get("/claude/summary", (req, res) => {
const summary = this.getActivitySummary();
res.json(summary);
});
this.sseServer = app.listen(port);
logger.info(`SSE server started on port ${port}`);
}
/**
* Broadcast to SSE clients
*/
broadcastSSE(activity) {
for (const client of this.sseClients) {
client.write(`data: ${JSON.stringify(activity)}
`);
}
}
/**
* Get activity summary
*/
getActivitySummary() {
const summary = {
totalActivities: this.activityBuffer.length,
recentActivities: this.activityBuffer.slice(-10),
activityTypes: {},
toolUsage: {}
};
for (const activity of this.activityBuffer) {
summary.activityTypes[activity.type] = (summary.activityTypes[activity.type] || 0) + 1;
if (activity.type === "post-tool" && activity.data.tool) {
summary.toolUsage[activity.data.tool] = (summary.toolUsage[activity.data.tool] || 0) + 1;
}
}
return summary;
}
};
// src/core/claude-process-manager.ts
import { spawn as spawn2 } from "child_process";
import { EventEmitter as EventEmitter3 } from "events";
import * as fs3 from "fs-extra";
import * as path3 from "path";
// src/utils/output-parser.ts
var stripAnsi;
try {
stripAnsi = __require("strip-ansi");
if (stripAnsi.default)
stripAnsi = stripAnsi.default;
} catch {
stripAnsi = (str) => str.replace(/\u001b\[[0-9;]*m/g, "");
}
var ClaudeOutputParser = class {
patterns = {
toolUse: /(?:Running|Executing|Using tool):\s*(\w+)(?:\s+with\s+(.+))?/,
fileChange: /(?:Modified|Created|Deleted):\s*(.+)/,
thinking: /(?:Thinking|Planning|Analyzing):\s*(.+)/,
error: /(?:Error|Failed):\s*(.+)/,
task: /(?:Task|Working on):\s*(.+)/,
progress: /(?:Progress|Completed):\s*(\d+)%?\s*(?:of\s+)?(.+)?/,
command: /(?:Command|Executing command):\s*(.+)/,
codeBlock: /```(\w+)?\n([\s\S]*?)```/,
response: /(?:Response|Output):\s*(.+)/
};
parse(output) {
const cleaned = stripAnsi(output).trim();
if (!cleaned)
return null;
for (const [type, pattern] of Object.entries(this.patterns)) {
const match = cleaned.match(pattern);
if (match) {
return {
type,
raw: cleaned,
...this.extractMatchData(type, match)
};
}
}
if (cleaned.startsWith("{") || cleaned.startsWith("[")) {
try {
return {
type: "json",
raw: cleaned,
data: JSON.parse(cleaned)
};
} catch {
}
}
if (cleaned.includes("|") && cleaned.split("|").length > 2) {
return this.parseTableRow(cleaned);
}
return {
type: "text",
raw: cleaned,
content: cleaned
};
}
extractMatchData(type, match) {
switch (type) {
case "toolUse":
return {
tool: match[1],
args: match[2]
};
case "fileChange":
return {
file: match[1]
};
case "thinking":
case "error":
case "task":
case "command":
case "response":
return {
message: match[1]
};
case "progress":
return {
percent: parseInt(match[1]),
task: match[2]
};
case "codeBlock":
return {
language: match[1] || "text",
code: match[2]
};
default:
return {};
}
}
parseTableRow(line) {
const parts = line.split("|").map((p) => p.trim()).filter((p) => p);
return {
type: "table-row",
raw: line,
columns: parts
};
}
/**
* Parse multiple lines of output
*/
parseLines(output) {
const lines = output.split("\n");
const results = [];
for (const line of lines) {
const parsed = this.parse(line);
if (parsed) {
results.push(parsed);
}
}
return results;
}
/**
* Extract code blocks from output
*/
extractCodeBlocks(output) {
const blocks = [];
const matches = output.matchAll(/```(\w+)?\n([\s\S]*?)```/g);
for (const match of matches) {
blocks.push({
language: match[1] || "text",
code: match[2]
});
}
return blocks;
}
/**
* Extract tool calls from output
*/
extractToolCalls(output) {
const calls = [];
const lines = output.split("\n");
for (const line of lines) {
const match = line.match(this.patterns.toolUse);
if (match) {
calls.push({
tool: match[1],
args: match[2]
});
}
}
return calls;
}
};
// src/core/claude-process-manager.ts
import stripAnsi2 from "strip-ansi";
var stripAnsiCompat = typeof stripAnsi2 === "function" ? stripAnsi2 : stripAnsi2.default || ((str) => str);
var ClaudeProcessManager = class extends EventEmitter3 {
constructor(options = {}) {
super();
this.options = options;
this.projectRoot = options.projectRoot || process.cwd();
this.outputParser = new ClaudeOutputParser();
}
process;
// pty.IPty
projectRoot;
outputBuffer = [];
maxBufferSize = 1e4;
isRunning = false;
outputParser;
/**
* Start Claude Code with enhanced process control
*/
async start(args = []) {
if (this.isRunning) {
throw new Error("Claude Code is already running");
}
const env = {
...process.env,
GONGFU_INTEGRATION: "1",
FORCE_COLOR: "0",
// Disable color for easier parsing
...this.options.env
};
if (this.options.usePty) {
this.startWithPty(args, env);
} else {
this.startWithSpawn(args, env);
}
this.isRunning = true;
this.emit("started");
}
/**
* Start with node-pty for better terminal emulation
*/
startWithPty(args, env) {
const pty = __require("node-pty");
this.process = pty.spawn("claude", ["code", ...args], {
name: "xterm-color",
cols: 120,
rows: 30,
cwd: this.projectRoot,
env
});
this.process.onData((data) => {
this.handleOutput(data, "stdout");
});
this.process.onExit((exitCode) => {
this.handleExit(exitCode);
});
}
/**
* Start with standard spawn
*/
startWithSpawn(args, env) {
this.process = spawn2("claude", ["code", ...args], {
cwd: this.projectRoot,
env,
stdio: this.options.captureOutput ? "pipe" : "inherit"
});
if (this.options.captureOutput && this.process.stdout && this.process.stderr) {
this.process.stdout.on("data", (data) => {
this.handleOutput(data.toString(), "stdout");
});
this.process.stderr.on("data", (data) => {
this.handleOutput(data.toString(), "stderr");
});
}
this.process.on("exit", (code) => {
this.handleExit(code || 0);
});
this.process.on("error", (error) => {
this.emit("error", error);
});
}
/**
* Handle output from Claude Code
*/
handleOutput(data, type) {
const output = {
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
type,
content: data
};
const parsed = this.outputParser.parse(data);
if (parsed) {
output.parsed = parsed;
this.emit("parsed-output", parsed);
}
this.outputBuffer.push(output);
if (this.outputBuffer.length > this.maxBufferSize) {
this.outputBuffer.shift();
}
this.emit("output", output);
if (this.options.captureOutput) {
this.logOutput(output);
}
}
/**
* Handle process exit
*/
handleExit(code) {
this.isRunning = false;
this.process = void 0;
this.emit("exit", code);
logger.info(`Claude Code exited with code ${code}`);
}
/**
* Send input to Claude Code
*/
async sendInput(input) {
if (!this.process) {
throw new Error("Claude Code is not running");
}
if (this.options.usePty) {
this.process.write(input);
} else if (this.process.stdin) {
this.process.stdin.write(input);
}
this.emit("input", input);
}
/**
* Send a command (with newline)
*/
async sendCommand(command) {
await this.sendInput(`${command}
`);
}
/**
* Stop Claude Code
*/
async stop() {
if (!this.process) {
return;
}
if (this.options.usePty) {
this.process.write("");
await this.waitForExit(5e3);
}
if (this.process) {
if (this.options.usePty) {
this.process.kill();
} else {
this.process.kill("SIGTERM");
}
}
}
/**
* Wait for process to exit
*/
waitForExit(timeout) {
return new Promise((resolve) => {
const timer = setTimeout(resolve, timeout);
this.once("exit", () => {
clearTimeout(timer);
resolve();
});
});
}
/**
* Get recent output
*/
getRecentOutput(lines = 100) {
return this.outputBuffer.slice(-lines);
}
/**
* Search output history
*/
searchOutput(pattern) {
const regex = typeof pattern === "string" ? new RegExp(pattern, "i") : pattern;
return this.outputBuffer.filter(
(output) => regex.test(output.content) || output.parsed && regex.test(JSON.stringify(output.parsed))
);
}
/**
* Log output to file
*/
async logOutput(output) {
const logDir = path3.join(this.projectRoot, ".claude", "logs");
await fs3.ensureDir(logDir);
const logFile = path3.join(logDir, `claude-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.log`);
const logLine = `[${output.timestamp}] [${output.type}] ${stripAnsi2(output.content)}`;
await fs3.appendFile(logFile, logLine);
}
/**
* Get process info
*/
getInfo() {
return {
isRunning: this.isRunning,
pid: this.process?.pid,
uptime: this.process ? Date.now() - this.process.startTime : 0,
bufferSize: this.outputBuffer.length
};
}
};
// src/core/claude-activity-handler.ts
import { EventEmitter as EventEmitter4 } from "events";
import express2 from "express";
import { watch as watch2 } from "chokidar";
import * as fs4 from "fs-extra";
import * as path4 from "path";
var ClaudeActivityHandler = class extends EventEmitter4 {
projectRoot;
claudeDir;
watcher;
activities = [];
maxActivities = 1e3;
constructor(projectRoot) {
super();
this.projectRoot = projectRoot;
this.claudeDir = path4.join(projectRoot, ".claude");
}
async start() {
await fs4.ensureDir(path4.join(this.claudeDir, "logs"));
this.startWatcher();
await this.parseExistingLogs();
logger.info("Claude activity handler started");
}
async stop() {
if (this.watcher) {
await this.watcher.close();
}
logger.info("Claude activity handler stopped");
}
/**
* Add SSE endpoints to express app
*/
setupSSEEndpoints(app) {
app.get("/mcp/claude/stream", (req, res) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*"
});
res.write(`event: connected
`);
res.write(`data: ${JSON.stringify({
type: "connected",
activities: this.activities.slice(-10)
})}
`);
const activityHandler = (activity) => {
res.write(`event: activity
`);
res.write(`data: ${JSON.stringify(activity)}
`);
};
this.on("activity", activityHandler);
const keepAlive = setInterval(() => {
res.write(":keepalive\n\n");
}, 3e4);
req.on("close", () => {
this.off("activity", activityHandler);
clearInterval(keepAlive);
});
});
app.get("/mcp/claude/summary", (req, res) => {
const summary = this.getActivitySummary();
res.json(summary);
});
app.get("/mcp/claude/current-task", async (req, res) => {
const task = await this.getCurrentTask();
res.json(task);
});
app.post("/mcp/claude/log", express2.json(), (req, res) => {
const { type, data } = req.body;
this.logActivity(type, data);
res.json({ success: true });
});
}
/**
* Start file watcher
*/
startWatcher() {
this.watcher = watch2(this.claudeDir, {
persistent: true,
ignoreInitial: true,
depth: 3
});
this.watcher.on("change", async (filePath) => {
if (filePath.includes("logs/")) {
await this.parseLogFile(filePath, true);
} else if (filePath.includes("current-task.md")) {
const content = await fs4.readFile(filePath, "utf-8");
this.addActivity({
type: "task-update",
data: { file: filePath, content },
source: "file"
});
}
});
this.watcher.on("add", async (filePath) => {
if (filePath.includes("logs/") && filePath.endsWith(".log")) {
await this.parseLogFile(filePath);
}
});
}
/**
* Parse existing log files
*/
async parseExistingLogs() {
const logsDir = path4.join(this.claudeDir, "logs");
try {
const files = await fs4.readdir(logsDir);
for (const file of files) {
if (file.endsWith(".log")) {
await this.parseLogFile(path4.join(logsDir, file));
}
}
} catch (error) {
logger.debug("No existing logs to parse");
}
}
/**
* Parse a log file
*/
async parseLogFile(filePath, tailOnly = false) {
try {
const content = await fs4.readFile(filePath, "utf-8");
const lines = content.split("\n").filter((line) => line.trim());
const linesToProcess = tailOnly ? lines.slice(-20) : lines;
for (const line of linesToProcess) {
const activity = this.parseLogLine(line);
if (activity) {
this.addActivity(activity);
}
}
} catch (error) {
logger.error(`Failed to parse log file ${filePath}:`, error);
}
}
/**
* Parse a log line
*/
parseLogLine(line) {
const toolMatch = line.match(/^\[(.+?)\] (PRE_TOOL|POST_TOOL|TOOL_START|TOOL_END): (\w+) \| (.+)$/);
if (toolMatch) {
return {
type: "tool-use",
data: {
phase: toolMatch[2],
tool: toolMatch[3],
details: this.parseDetails(toolMatch[4])
},
source: "hook"
};
}
const fileMatch = line.match(/^\[(.+?)\] FILE_MODIFIED: (.+)$/);
if (fileMatch) {
return {
type: "file-change",
data: { file: fileMatch[2] },
source: "hook"
};
}
const cmdMatch = line.match(/^\[(.+?)\] COMMAND_RUN: (.+)$/);
if (cmdMatch) {
return {
type: "command",
data: { command: cmdMatch[2] },
source: "hook"
};
}
const sessionMatch = line.match(/^\[(.+?)\] (SESSION_START|SESSION_END)$/);
if (sessionMatch) {
return {
type: "session",
data: { event: sessionMatch[2] },
source: "hook"
};
}
return null;
}
/**
* Parse details from log line
*/
parseDetails(details) {
try {
return JSON.parse(details);
} catch {
const pairs = details.split(" | ");
const result = {};
for (const pair of pairs) {
const [key, value] = pair.split(": ");
if (key && value) {
result[key.toLowerCase().replace(/\s+/g, "_")] = value;
}
}
return Object.keys(result).length > 0 ? result : details;
}
}
/**
* Add activity to the list
*/
addActivity(activity) {
const fullActivity = {
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
...activity
};
this.activities.push(fullActivity);
if (this.activities.length > this.maxActivities) {
this.activities = this.activities.slice(-this.maxActivities);
}
this.emit("activity", fullActivity);
}
/**
* Log an activity manually
*/
logActivity(type, data) {
this.addActivity({
type,
data,
source: "monitor"
});
}
/**
* Get activity summary
*/
getActivitySummary() {
const summary = {
totalActivities: this.activities.length,
recentActivities: this.activities.slice(-20),
byType: {},
bySource: {},
toolUsage: {},
fileChanges: []
};
for (const activity of this.activities) {
summary.byType[activity.type] = (summary.byType[activity.type] || 0) + 1;
summary.bySource[activity.source] = (summary.bySource[activity.source] || 0) + 1;
if (activity.type === "tool-use" && activity.data.tool) {
summary.toolUsage[activity.data.tool] = (summary.toolUsage[activity.data.tool] || 0) + 1;
}
if (activity.type === "file-change" && activity.data.file) {
if (!summary.fileChanges.includes(activity.data.file)) {
summary.fileChanges.push(activity.data.file);
}
}
}
return summary;
}
/**
* Get current task
*/
async getCurrentTask() {
const taskFile = path4.join(this.claudeDir, "current-task.md");
try {
const content = await fs4.readFile(taskFile, "utf-8");
const idMatch = content.match(/## Task ID: (.+)/);
const descMatch = content.match(/## Description\n(.+)/);
const priorityMatch = content.match(/## Priority: (.+)/);
const startedMatch = content.match(/- Started: (.+)/);
return {
exists: true,
id: idMatch?.[1],
description: descMatch?.[1],
priority: priorityMatch?.[1],
startedAt: startedMatch?.[1],
content
};
} catch {
return { exists: false };
}
}
};
// src/index.ts
var CLAUDE_DIR = ".claude";
var CLAUDE_LOGS_DIR = ".claude/logs";
var CLAUDE_HOOKS_FILE = ".claude/settings.local.json";
var CLAUDE_TASK_FILE = ".claude/current-task.md";
var version = "0.1.0";
export {
CLAUDE_DIR,
CLAUDE_HOOKS_FILE,
CLAUDE_LOGS_DIR,
CLAUDE_TASK_FILE,
ClaudeActivityHandler,
ClaudeController,
ClaudeMonitor,
ClaudeOutputParser,
ClaudeProcessManager,
createLogger,
version
};
//# sourceMappingURL=index.js.map