claudit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +139 -0
  2. package/bin/claudit-mcp.js +3 -0
  3. package/bin/claudit.js +11 -0
  4. package/client/dist/assets/index-DhjH_2Wd.css +32 -0
  5. package/client/dist/assets/index-Dwom-XdC.js +98 -0
  6. package/client/dist/index.html +13 -0
  7. package/package.json +40 -0
  8. package/server/dist/server/src/index.js +170 -0
  9. package/server/dist/server/src/mcp-server.js +144 -0
  10. package/server/dist/server/src/routes/cron.js +101 -0
  11. package/server/dist/server/src/routes/filesystem.js +71 -0
  12. package/server/dist/server/src/routes/groups.js +60 -0
  13. package/server/dist/server/src/routes/sessions.js +206 -0
  14. package/server/dist/server/src/routes/todo.js +93 -0
  15. package/server/dist/server/src/routes/todoProviders.js +179 -0
  16. package/server/dist/server/src/services/claudeProcess.js +220 -0
  17. package/server/dist/server/src/services/cronScheduler.js +163 -0
  18. package/server/dist/server/src/services/cronStorage.js +154 -0
  19. package/server/dist/server/src/services/database.js +103 -0
  20. package/server/dist/server/src/services/eventBus.js +11 -0
  21. package/server/dist/server/src/services/groupStorage.js +52 -0
  22. package/server/dist/server/src/services/historyIndex.js +100 -0
  23. package/server/dist/server/src/services/jsonStore.js +41 -0
  24. package/server/dist/server/src/services/managedSessions.js +96 -0
  25. package/server/dist/server/src/services/providerConfigStorage.js +80 -0
  26. package/server/dist/server/src/services/providers/TodoProvider.js +1 -0
  27. package/server/dist/server/src/services/providers/larkDocsProvider.js +75 -0
  28. package/server/dist/server/src/services/providers/mcpClient.js +151 -0
  29. package/server/dist/server/src/services/providers/meegoProvider.js +99 -0
  30. package/server/dist/server/src/services/providers/registry.js +17 -0
  31. package/server/dist/server/src/services/providers/supabaseProvider.js +172 -0
  32. package/server/dist/server/src/services/ptyManager.js +263 -0
  33. package/server/dist/server/src/services/sessionIndexCache.js +24 -0
  34. package/server/dist/server/src/services/sessionParser.js +98 -0
  35. package/server/dist/server/src/services/sessionScanner.js +244 -0
  36. package/server/dist/server/src/services/todoStorage.js +112 -0
  37. package/server/dist/server/src/services/todoSyncEngine.js +170 -0
  38. package/server/dist/server/src/types.js +1 -0
  39. package/server/dist/shared/src/index.js +1 -0
  40. package/server/dist/shared/src/types.js +2 -0
@@ -0,0 +1,206 @@
1
+ import { Router } from 'express';
2
+ import { execSync } from 'child_process';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import { getSessionIndex, invalidateSessionCache } from '../services/historyIndex.js';
7
+ import { parseSession } from '../services/sessionParser.js';
8
+ import { addManagedSession, renameManagedSession, archiveManagedSession, removeManagedSession, pinManagedSession } from '../services/managedSessions.js';
9
+ import { eventBus } from '../services/eventBus.js';
10
+ const router = Router();
11
+ // GET /api/sessions?q=keyword&hideEmpty=true&managedOnly=true
12
+ router.get('/', (req, res) => {
13
+ try {
14
+ const query = req.query.q;
15
+ const hideEmpty = req.query.hideEmpty === 'true';
16
+ const managedOnly = req.query.managedOnly === 'true';
17
+ const groups = getSessionIndex(query, hideEmpty, managedOnly);
18
+ res.json(groups);
19
+ }
20
+ catch (err) {
21
+ console.error('Error fetching sessions:', err);
22
+ res.status(500).json({ error: 'Failed to fetch sessions' });
23
+ }
24
+ });
25
+ // POST /api/sessions/new — create a fresh claude session
26
+ router.post('/new', (req, res) => {
27
+ try {
28
+ const { projectPath, worktree, displayName, initialPrompt } = req.body;
29
+ if (!projectPath || typeof projectPath !== 'string') {
30
+ res.status(400).json({ error: 'projectPath is required' });
31
+ return;
32
+ }
33
+ if (!fs.existsSync(projectPath)) {
34
+ res.status(400).json({ error: 'projectPath does not exist' });
35
+ return;
36
+ }
37
+ let actualProjectPath = projectPath;
38
+ // Handle git worktree creation
39
+ if (worktree?.branchName) {
40
+ const gitDir = path.join(projectPath, '.git');
41
+ if (!fs.existsSync(gitDir)) {
42
+ res.status(400).json({ error: 'Source path is not a git repository' });
43
+ return;
44
+ }
45
+ const worktreePath = path.join(path.dirname(projectPath), `${path.basename(projectPath)}-${worktree.branchName}`);
46
+ try {
47
+ execSync(`git worktree add ${JSON.stringify(worktreePath)} -b ${JSON.stringify(worktree.branchName)}`, {
48
+ cwd: projectPath, encoding: 'utf-8', timeout: 15_000,
49
+ });
50
+ }
51
+ catch {
52
+ // Branch may already exist — try without -b
53
+ try {
54
+ execSync(`git worktree add ${JSON.stringify(worktreePath)} ${JSON.stringify(worktree.branchName)}`, {
55
+ cwd: projectPath, encoding: 'utf-8', timeout: 15_000,
56
+ });
57
+ }
58
+ catch (e) {
59
+ res.status(500).json({ error: `Failed to create worktree: ${e.message}` });
60
+ return;
61
+ }
62
+ }
63
+ actualProjectPath = worktreePath;
64
+ }
65
+ // Run claude with the initial prompt to create a session
66
+ const prompt = (typeof initialPrompt === 'string' && initialPrompt.trim())
67
+ ? initialPrompt.trim()
68
+ : 'hello';
69
+ const cleanEnv = Object.fromEntries(Object.entries(process.env).filter(([k]) => k !== 'CLAUDECODE'));
70
+ const escapedPrompt = prompt.replace(/'/g, "'\\''");
71
+ const result = execSync(`claude -p --output-format json --max-turns 1 '${escapedPrompt}'`, { cwd: actualProjectPath, encoding: 'utf-8', timeout: 60_000, env: cleanEnv });
72
+ // Parse the JSON result to get the session ID
73
+ const parsed = JSON.parse(result);
74
+ const sessionId = parsed.session_id;
75
+ if (!sessionId) {
76
+ res.status(500).json({ error: 'Failed to get session ID from claude output' });
77
+ return;
78
+ }
79
+ // Compute project hash
80
+ const projectHash = actualProjectPath.replace(/\//g, '-');
81
+ // Record in managed store
82
+ addManagedSession(sessionId, actualProjectPath);
83
+ if (displayName) {
84
+ renameManagedSession(sessionId, displayName);
85
+ }
86
+ // Invalidate cache so the new session shows up
87
+ invalidateSessionCache();
88
+ eventBus.emitSessionEvent({ type: 'session:created', sessionId });
89
+ res.json({ sessionId, projectPath: actualProjectPath, projectHash });
90
+ }
91
+ catch (err) {
92
+ console.error('Error creating session:', err);
93
+ res.status(500).json({ error: err.message || 'Failed to create session' });
94
+ }
95
+ });
96
+ // GET /api/sessions/archived — return archived sessions
97
+ router.get('/archived', (req, res) => {
98
+ try {
99
+ const groups = getSessionIndex(undefined, false, false, true);
100
+ const count = groups.reduce((sum, g) => sum + g.sessions.length, 0);
101
+ res.json({ count, groups });
102
+ }
103
+ catch (err) {
104
+ console.error('Error fetching archived sessions:', err);
105
+ res.status(500).json({ error: 'Failed to fetch archived sessions' });
106
+ }
107
+ });
108
+ // PATCH /api/sessions/:sessionId/pin — pin or unpin a session
109
+ router.patch('/:sessionId/pin', (req, res) => {
110
+ try {
111
+ const { sessionId } = req.params;
112
+ const { pinned } = req.body;
113
+ if (typeof pinned !== 'boolean') {
114
+ res.status(400).json({ error: 'pinned (boolean) is required' });
115
+ return;
116
+ }
117
+ pinManagedSession(sessionId, pinned);
118
+ invalidateSessionCache();
119
+ eventBus.emitSessionEvent({ type: 'session:updated', sessionId });
120
+ res.json({ ok: true });
121
+ }
122
+ catch (err) {
123
+ console.error('Error pinning session:', err);
124
+ res.status(500).json({ error: err.message || 'Failed to pin session' });
125
+ }
126
+ });
127
+ // PATCH /api/sessions/:sessionId/archive — archive or unarchive a session
128
+ router.patch('/:sessionId/archive', (req, res) => {
129
+ try {
130
+ const { sessionId } = req.params;
131
+ const { archived } = req.body;
132
+ if (typeof archived !== 'boolean') {
133
+ res.status(400).json({ error: 'archived (boolean) is required' });
134
+ return;
135
+ }
136
+ archiveManagedSession(sessionId, archived);
137
+ invalidateSessionCache();
138
+ eventBus.emitSessionEvent({ type: 'session:archived', sessionId });
139
+ res.json({ ok: true });
140
+ }
141
+ catch (err) {
142
+ console.error('Error archiving session:', err);
143
+ res.status(500).json({ error: err.message || 'Failed to archive session' });
144
+ }
145
+ });
146
+ // PATCH /api/sessions/:sessionId/name — rename a session
147
+ router.patch('/:sessionId/name', (req, res) => {
148
+ try {
149
+ const { sessionId } = req.params;
150
+ const { name } = req.body;
151
+ if (typeof name !== 'string') {
152
+ res.status(400).json({ error: 'name is required' });
153
+ return;
154
+ }
155
+ let entry = renameManagedSession(sessionId, name);
156
+ if (!entry) {
157
+ // Session not in managed store — add it so we can still rename
158
+ addManagedSession(sessionId, '');
159
+ entry = renameManagedSession(sessionId, name);
160
+ }
161
+ // Invalidate cache so the new name shows up
162
+ invalidateSessionCache();
163
+ eventBus.emitSessionEvent({ type: 'session:updated', sessionId });
164
+ res.json({ ok: true });
165
+ }
166
+ catch (err) {
167
+ console.error('Error renaming session:', err);
168
+ res.status(500).json({ error: err.message || 'Failed to rename session' });
169
+ }
170
+ });
171
+ // DELETE /api/sessions/:projectHash/:sessionId — delete session file and managed entry
172
+ router.delete('/:projectHash/:sessionId', (req, res) => {
173
+ try {
174
+ const { projectHash, sessionId } = req.params;
175
+ const sessionFile = path.join(os.homedir(), '.claude', 'projects', projectHash, `${sessionId}.jsonl`);
176
+ if (fs.existsSync(sessionFile)) {
177
+ fs.unlinkSync(sessionFile);
178
+ }
179
+ removeManagedSession(sessionId);
180
+ invalidateSessionCache();
181
+ eventBus.emitSessionEvent({ type: 'session:deleted', sessionId });
182
+ res.json({ ok: true });
183
+ }
184
+ catch (err) {
185
+ console.error('Error deleting session:', err);
186
+ res.status(500).json({ error: err.message || 'Failed to delete session' });
187
+ }
188
+ });
189
+ // GET /api/sessions/:projectHash/:sessionId
190
+ router.get('/:projectHash/:sessionId', (req, res) => {
191
+ try {
192
+ const { projectHash, sessionId } = req.params;
193
+ const detail = parseSession(projectHash, sessionId);
194
+ res.json(detail);
195
+ }
196
+ catch (err) {
197
+ console.error('Error parsing session:', err);
198
+ if (err.message?.includes('not found')) {
199
+ res.status(404).json({ error: 'Session not found' });
200
+ }
201
+ else {
202
+ res.status(500).json({ error: 'Failed to parse session' });
203
+ }
204
+ }
205
+ });
206
+ export default router;
@@ -0,0 +1,93 @@
1
+ import { Router } from 'express';
2
+ import { getAllTodos, getTodo, createTodo, updateTodo, deleteTodo, reorderTodos, } from '../services/todoStorage.js';
3
+ import { pushCompletion } from '../services/todoSyncEngine.js';
4
+ const router = Router();
5
+ // GET /api/todo — list all todos
6
+ router.get('/', (_req, res) => {
7
+ try {
8
+ res.json(getAllTodos());
9
+ }
10
+ catch (err) {
11
+ console.error('Error fetching todos:', err);
12
+ res.status(500).json({ error: 'Failed to fetch todos' });
13
+ }
14
+ });
15
+ // POST /api/todo — create todo
16
+ router.post('/', (req, res) => {
17
+ try {
18
+ const { title, description, priority, sessionId, sessionLabel, groupId } = req.body;
19
+ if (!title) {
20
+ res.status(400).json({ error: 'title is required' });
21
+ return;
22
+ }
23
+ const todo = createTodo({
24
+ title,
25
+ description,
26
+ completed: false,
27
+ priority: priority ?? 'medium',
28
+ sessionId,
29
+ sessionLabel,
30
+ groupId,
31
+ position: 0, // will be auto-computed by createTodo
32
+ });
33
+ res.status(201).json(todo);
34
+ }
35
+ catch (err) {
36
+ console.error('Error creating todo:', err);
37
+ res.status(500).json({ error: 'Failed to create todo' });
38
+ }
39
+ });
40
+ // PUT /api/todo/reorder — batch reorder todos
41
+ router.put('/reorder', (req, res) => {
42
+ try {
43
+ const { items } = req.body;
44
+ if (!Array.isArray(items)) {
45
+ res.status(400).json({ error: 'items array is required' });
46
+ return;
47
+ }
48
+ reorderTodos(items);
49
+ res.json({ success: true });
50
+ }
51
+ catch (err) {
52
+ console.error('Error reordering todos:', err);
53
+ res.status(500).json({ error: 'Failed to reorder todos' });
54
+ }
55
+ });
56
+ // PUT /api/todo/:id — update todo
57
+ router.put('/:id', (req, res) => {
58
+ try {
59
+ const before = getTodo(req.params.id);
60
+ const todo = updateTodo(req.params.id, req.body);
61
+ if (!todo) {
62
+ res.status(404).json({ error: 'Todo not found' });
63
+ return;
64
+ }
65
+ // If a provider-linked todo was just completed, push completion async
66
+ if (todo.provider && todo.completed && before && !before.completed) {
67
+ pushCompletion(todo).catch(err => {
68
+ console.error('Failed to push completion:', err);
69
+ });
70
+ }
71
+ res.json(todo);
72
+ }
73
+ catch (err) {
74
+ console.error('Error updating todo:', err);
75
+ res.status(500).json({ error: 'Failed to update todo' });
76
+ }
77
+ });
78
+ // DELETE /api/todo/:id — delete todo
79
+ router.delete('/:id', (req, res) => {
80
+ try {
81
+ const ok = deleteTodo(req.params.id);
82
+ if (!ok) {
83
+ res.status(404).json({ error: 'Todo not found' });
84
+ return;
85
+ }
86
+ res.json({ success: true });
87
+ }
88
+ catch (err) {
89
+ console.error('Error deleting todo:', err);
90
+ res.status(500).json({ error: 'Failed to delete todo' });
91
+ }
92
+ });
93
+ export default router;
@@ -0,0 +1,179 @@
1
+ import { Router } from 'express';
2
+ import { getAllProviders, getProvider } from '../services/providers/registry.js';
3
+ import { getAllConfigs, getConfig, createConfig, updateConfig, deleteConfig, } from '../services/providerConfigStorage.js';
4
+ import { syncProvider, syncAllProviders } from '../services/todoSyncEngine.js';
5
+ const router = Router();
6
+ // GET /api/todo/providers — list provider types
7
+ router.get('/', (_req, res) => {
8
+ try {
9
+ const providers = getAllProviders().map(p => ({
10
+ id: p.id,
11
+ displayName: p.displayName,
12
+ configSchema: p.configSchema,
13
+ }));
14
+ res.json(providers);
15
+ }
16
+ catch (err) {
17
+ console.error('Error listing providers:', err);
18
+ res.status(500).json({ error: 'Failed to list providers' });
19
+ }
20
+ });
21
+ /**
22
+ * Redact secret fields from a config object.
23
+ */
24
+ function redactSecrets(providerId, config) {
25
+ const provider = getProvider(providerId);
26
+ if (!provider)
27
+ return config;
28
+ const secretKeys = new Set(provider.configSchema.filter(f => f.secret).map(f => f.key));
29
+ const redacted = {};
30
+ for (const [key, value] of Object.entries(config)) {
31
+ redacted[key] = secretKeys.has(key) && typeof value === 'string' && value
32
+ ? '••••••••'
33
+ : value;
34
+ }
35
+ return redacted;
36
+ }
37
+ // GET /api/todo/providers/configs — list all configs (secrets redacted)
38
+ router.get('/configs', (_req, res) => {
39
+ try {
40
+ const configs = getAllConfigs().map(c => ({
41
+ ...c,
42
+ config: redactSecrets(c.providerId, c.config),
43
+ }));
44
+ res.json(configs);
45
+ }
46
+ catch (err) {
47
+ console.error('Error listing configs:', err);
48
+ res.status(500).json({ error: 'Failed to list configs' });
49
+ }
50
+ });
51
+ // POST /api/todo/providers/configs — create config
52
+ router.post('/configs', async (req, res) => {
53
+ try {
54
+ const { providerId, name, enabled, config: providerConfig, syncIntervalMinutes } = req.body;
55
+ if (!providerId || !name) {
56
+ res.status(400).json({ error: 'providerId and name are required' });
57
+ return;
58
+ }
59
+ const provider = getProvider(providerId);
60
+ if (!provider) {
61
+ res.status(400).json({ error: `Unknown provider: ${providerId}` });
62
+ return;
63
+ }
64
+ // Validate config if provider supports it
65
+ if (provider.validateConfig) {
66
+ const validationError = await provider.validateConfig(providerConfig || {});
67
+ if (validationError) {
68
+ res.status(400).json({ error: validationError });
69
+ return;
70
+ }
71
+ }
72
+ const created = createConfig({
73
+ providerId,
74
+ name,
75
+ enabled: enabled ?? true,
76
+ config: providerConfig || {},
77
+ syncIntervalMinutes: syncIntervalMinutes ?? 0,
78
+ });
79
+ res.status(201).json({
80
+ ...created,
81
+ config: redactSecrets(created.providerId, created.config),
82
+ });
83
+ }
84
+ catch (err) {
85
+ console.error('Error creating config:', err);
86
+ res.status(500).json({ error: 'Failed to create config' });
87
+ }
88
+ });
89
+ // PUT /api/todo/providers/configs/:id — update config
90
+ router.put('/configs/:id', async (req, res) => {
91
+ try {
92
+ const existing = getConfig(req.params.id);
93
+ if (!existing) {
94
+ res.status(404).json({ error: 'Config not found' });
95
+ return;
96
+ }
97
+ // If config fields are being updated, merge with existing (don't overwrite secrets with redacted values)
98
+ let newConfig = req.body.config;
99
+ if (newConfig) {
100
+ const provider = getProvider(existing.providerId);
101
+ if (provider) {
102
+ const secretKeys = new Set(provider.configSchema.filter(f => f.secret).map(f => f.key));
103
+ const merged = { ...existing.config };
104
+ for (const [key, value] of Object.entries(newConfig)) {
105
+ // Skip redacted placeholder values for secret fields
106
+ if (secretKeys.has(key) && value === '••••••••')
107
+ continue;
108
+ merged[key] = value;
109
+ }
110
+ newConfig = merged;
111
+ }
112
+ }
113
+ const updates = {};
114
+ if (req.body.name !== undefined)
115
+ updates.name = req.body.name;
116
+ if (req.body.enabled !== undefined)
117
+ updates.enabled = req.body.enabled;
118
+ if (newConfig !== undefined)
119
+ updates.config = newConfig;
120
+ if (req.body.syncIntervalMinutes !== undefined)
121
+ updates.syncIntervalMinutes = req.body.syncIntervalMinutes;
122
+ const updated = updateConfig(req.params.id, updates);
123
+ if (!updated) {
124
+ res.status(404).json({ error: 'Config not found' });
125
+ return;
126
+ }
127
+ res.json({
128
+ ...updated,
129
+ config: redactSecrets(updated.providerId, updated.config),
130
+ });
131
+ }
132
+ catch (err) {
133
+ console.error('Error updating config:', err);
134
+ res.status(500).json({ error: 'Failed to update config' });
135
+ }
136
+ });
137
+ // DELETE /api/todo/providers/configs/:id — delete config
138
+ router.delete('/configs/:id', (req, res) => {
139
+ try {
140
+ const ok = deleteConfig(req.params.id);
141
+ if (!ok) {
142
+ res.status(404).json({ error: 'Config not found' });
143
+ return;
144
+ }
145
+ res.json({ success: true });
146
+ }
147
+ catch (err) {
148
+ console.error('Error deleting config:', err);
149
+ res.status(500).json({ error: 'Failed to delete config' });
150
+ }
151
+ });
152
+ // POST /api/todo/providers/configs/:id/sync — manual sync one config
153
+ router.post('/configs/:id/sync', async (req, res) => {
154
+ try {
155
+ const config = getConfig(req.params.id);
156
+ if (!config) {
157
+ res.status(404).json({ error: 'Config not found' });
158
+ return;
159
+ }
160
+ const result = await syncProvider(config);
161
+ res.json(result);
162
+ }
163
+ catch (err) {
164
+ console.error('Error syncing provider:', err);
165
+ res.status(500).json({ error: 'Failed to sync provider' });
166
+ }
167
+ });
168
+ // POST /api/todo/providers/sync-all — manual sync all enabled configs
169
+ router.post('/sync-all', async (_req, res) => {
170
+ try {
171
+ const results = await syncAllProviders();
172
+ res.json(results);
173
+ }
174
+ catch (err) {
175
+ console.error('Error syncing all providers:', err);
176
+ res.status(500).json({ error: 'Failed to sync providers' });
177
+ }
178
+ });
179
+ export default router;
@@ -0,0 +1,220 @@
1
+ import { spawn, execSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import { EventEmitter } from 'events';
5
+ // Resolve claude binary path at startup so spawn can find it
6
+ const CLAUDE_BIN = (() => {
7
+ try {
8
+ return execSync('which claude', { encoding: 'utf-8' }).trim();
9
+ }
10
+ catch {
11
+ return 'claude'; // fallback
12
+ }
13
+ })();
14
+ console.log(`[claude] Binary path: ${CLAUDE_BIN}`);
15
+ export class ClaudeProcess extends EventEmitter {
16
+ proc = null;
17
+ sessionId;
18
+ projectPath;
19
+ stdoutBuffer = '';
20
+ stderrBuffer = '';
21
+ lastTextLength = 0;
22
+ lastThinkingLength = 0;
23
+ lastMessageId = '';
24
+ sentToolUseIds = new Set();
25
+ userMessageSent = false;
26
+ pendingMessage = null;
27
+ constructor(sessionId, projectPath) {
28
+ super();
29
+ this.sessionId = sessionId;
30
+ this.projectPath = projectPath;
31
+ }
32
+ resetTracking() {
33
+ this.lastTextLength = 0;
34
+ this.lastThinkingLength = 0;
35
+ this.lastMessageId = '';
36
+ this.sentToolUseIds.clear();
37
+ this.stdoutBuffer = '';
38
+ this.stderrBuffer = '';
39
+ }
40
+ _spawnProcess(onClose) {
41
+ const cwd = fs.existsSync(this.projectPath) ? this.projectPath : os.homedir();
42
+ console.log(`[claude] Spawning CLI: resume=${this.sessionId} cwd=${cwd}`);
43
+ const proc = spawn(CLAUDE_BIN, [
44
+ '--resume', this.sessionId,
45
+ '-p',
46
+ '--output-format', 'stream-json',
47
+ '--input-format', 'stream-json',
48
+ '--verbose',
49
+ ], {
50
+ cwd,
51
+ env: Object.fromEntries(Object.entries(process.env).filter(([k]) => k !== 'CLAUDECODE')),
52
+ stdio: ['pipe', 'pipe', 'pipe'],
53
+ });
54
+ proc.stdout?.on('data', (data) => {
55
+ this.processStreamData(data, 'stdout');
56
+ });
57
+ proc.stderr?.on('data', (data) => {
58
+ this.processStreamData(data, 'stderr');
59
+ });
60
+ proc.on('close', onClose);
61
+ proc.on('error', (err) => {
62
+ console.error(`[claude] Process error: ${err.message}`);
63
+ this.emit('error', err.message);
64
+ this.proc = null;
65
+ });
66
+ return proc;
67
+ }
68
+ start() {
69
+ this.proc = this._spawnProcess((code) => {
70
+ console.log(`[claude] Process exited with code ${code}`);
71
+ this.proc = null;
72
+ if (this.userMessageSent) {
73
+ this.emit('done');
74
+ }
75
+ else if (this.pendingMessage) {
76
+ console.log('[claude] Process exited during resume, restarting for pending message...');
77
+ const msg = this.pendingMessage;
78
+ this.pendingMessage = null;
79
+ this.restart(msg);
80
+ }
81
+ });
82
+ }
83
+ /** Restart the process and send a message once ready */
84
+ restart(content) {
85
+ this.userMessageSent = true;
86
+ this.resetTracking();
87
+ this.proc = this._spawnProcess((code) => {
88
+ console.log(`[claude] Restarted process exited with code ${code}`);
89
+ this.emit('done');
90
+ this.proc = null;
91
+ });
92
+ // Write the user message to stdin
93
+ const msg = JSON.stringify({
94
+ type: 'user',
95
+ message: { role: 'user', content },
96
+ }) + '\n';
97
+ console.log(`[claude] Sending to restarted stdin: ${msg.trim().slice(0, 200)}`);
98
+ this.proc.stdin?.write(msg);
99
+ }
100
+ processStreamData(data, source) {
101
+ const bufferKey = source === 'stdout' ? 'stdoutBuffer' : 'stderrBuffer';
102
+ this[bufferKey] += data.toString();
103
+ const lines = this[bufferKey].split('\n');
104
+ this[bufferKey] = lines.pop() || '';
105
+ for (const line of lines) {
106
+ if (!line.trim())
107
+ continue;
108
+ try {
109
+ const parsed = JSON.parse(line);
110
+ this.handleStreamEvent(parsed);
111
+ }
112
+ catch {
113
+ if (source === 'stderr') {
114
+ const text = line.trim();
115
+ if (text.includes('Error') || text.includes('error')) {
116
+ console.error(`[claude] stderr text: ${text.slice(0, 300)}`);
117
+ this.emit('error', text);
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+ handleStreamEvent(event) {
124
+ switch (event.type) {
125
+ case 'system':
126
+ this.emit('ready');
127
+ break;
128
+ case 'assistant': {
129
+ if (!this.userMessageSent)
130
+ break;
131
+ const msg = event.message;
132
+ if (!msg?.content)
133
+ break;
134
+ const messageId = msg.id || '';
135
+ if (messageId !== this.lastMessageId) {
136
+ this.lastMessageId = messageId;
137
+ this.lastTextLength = 0;
138
+ this.lastThinkingLength = 0;
139
+ }
140
+ let fullText = '';
141
+ let fullThinking = '';
142
+ for (const block of msg.content) {
143
+ if (block.type === 'text' && block.text) {
144
+ fullText += block.text;
145
+ }
146
+ else if (block.type === 'thinking' && block.thinking) {
147
+ fullThinking += block.thinking;
148
+ }
149
+ else if (block.type === 'tool_use' && !this.sentToolUseIds.has(block.id)) {
150
+ this.sentToolUseIds.add(block.id);
151
+ this.emit('tool_use', {
152
+ name: block.name,
153
+ id: block.id,
154
+ input: block.input || {},
155
+ });
156
+ }
157
+ }
158
+ if (fullThinking.length > this.lastThinkingLength) {
159
+ const delta = fullThinking.slice(this.lastThinkingLength);
160
+ this.lastThinkingLength = fullThinking.length;
161
+ this.emit('assistant_thinking', delta);
162
+ }
163
+ if (fullText.length > this.lastTextLength) {
164
+ const delta = fullText.slice(this.lastTextLength);
165
+ this.lastTextLength = fullText.length;
166
+ this.emit('assistant_text', delta);
167
+ }
168
+ break;
169
+ }
170
+ case 'result':
171
+ if (!this.userMessageSent) {
172
+ console.log('[claude] Ignoring initial resume result');
173
+ break;
174
+ }
175
+ if (event.result && typeof event.result === 'string' && this.lastTextLength === 0) {
176
+ this.emit('assistant_text', event.result);
177
+ }
178
+ console.log(`[claude] Result: subtype=${event.subtype} result=${String(event.result).slice(0, 100)}`);
179
+ this.emit('done');
180
+ break;
181
+ case 'user':
182
+ break;
183
+ default:
184
+ break;
185
+ }
186
+ }
187
+ sendMessage(content) {
188
+ if (!this.proc?.stdin?.writable) {
189
+ console.log('[claude] Process not running, restarting for message...');
190
+ this.restart(content);
191
+ return;
192
+ }
193
+ this.userMessageSent = true;
194
+ this.lastTextLength = 0;
195
+ this.lastThinkingLength = 0;
196
+ this.lastMessageId = '';
197
+ this.sentToolUseIds.clear();
198
+ const msg = JSON.stringify({
199
+ type: 'user',
200
+ message: { role: 'user', content },
201
+ }) + '\n';
202
+ console.log(`[claude] Sending to stdin: ${msg.trim().slice(0, 200)}`);
203
+ this.proc.stdin.write(msg);
204
+ }
205
+ isAlive() {
206
+ return this.proc !== null && !this.proc.killed;
207
+ }
208
+ stop() {
209
+ if (this.proc) {
210
+ console.log('[claude] Stopping process');
211
+ this.proc.kill('SIGTERM');
212
+ setTimeout(() => {
213
+ if (this.proc) {
214
+ this.proc.kill('SIGKILL');
215
+ this.proc = null;
216
+ }
217
+ }, 3000);
218
+ }
219
+ }
220
+ }