loopsy 1.0.4 → 1.0.6

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 (38) hide show
  1. package/README.md +24 -16
  2. package/dist/cli/commands/doctor.d.ts.map +1 -1
  3. package/dist/cli/commands/doctor.js +28 -14
  4. package/dist/cli/commands/doctor.js.map +1 -1
  5. package/dist/cli/commands/init.d.ts.map +1 -1
  6. package/dist/cli/commands/init.js +21 -10
  7. package/dist/cli/commands/init.js.map +1 -1
  8. package/dist/cli/commands/mcp.d.ts.map +1 -1
  9. package/dist/cli/commands/mcp.js +81 -42
  10. package/dist/cli/commands/mcp.js.map +1 -1
  11. package/dist/cli/commands/pair.js +29 -3
  12. package/dist/cli/commands/pair.js.map +1 -1
  13. package/dist/cli/index.js +1 -1
  14. package/dist/daemon/routes/ai-tasks.js +1 -1
  15. package/dist/daemon/routes/ai-tasks.js.map +1 -1
  16. package/dist/daemon/routes/pair.d.ts.map +1 -1
  17. package/dist/daemon/routes/pair.js +18 -3
  18. package/dist/daemon/routes/pair.js.map +1 -1
  19. package/dist/daemon/services/ai-task-manager.d.ts +12 -0
  20. package/dist/daemon/services/ai-task-manager.d.ts.map +1 -1
  21. package/dist/daemon/services/ai-task-manager.js +319 -108
  22. package/dist/daemon/services/ai-task-manager.js.map +1 -1
  23. package/dist/dashboard/public/views/ai-tasks.js +51 -4
  24. package/dist/mcp-server/daemon-client.d.ts.map +1 -1
  25. package/dist/mcp-server/daemon-client.js +7 -2
  26. package/dist/mcp-server/daemon-client.js.map +1 -1
  27. package/dist/protocol/errors.d.ts +1 -0
  28. package/dist/protocol/errors.d.ts.map +1 -1
  29. package/dist/protocol/errors.js +1 -0
  30. package/dist/protocol/errors.js.map +1 -1
  31. package/dist/protocol/schemas.d.ts +3 -0
  32. package/dist/protocol/schemas.d.ts.map +1 -1
  33. package/dist/protocol/schemas.js +1 -0
  34. package/dist/protocol/schemas.js.map +1 -1
  35. package/dist/protocol/types.d.ts +4 -0
  36. package/dist/protocol/types.d.ts.map +1 -1
  37. package/package.json +6 -3
  38. package/scripts/postinstall.mjs +1 -1
@@ -87,8 +87,21 @@ export function registerPairRoutes(app, ctx) {
87
87
  return response;
88
88
  });
89
89
  // POST /api/v1/pair/confirm — Both sides confirm the SAS matches
90
+ // This is called by both the waiter (Machine A, via daemon request) and the
91
+ // initiator (Machine B, via direct HTTP). Accept confirm on already-completed
92
+ // sessions to avoid a race condition where the first confirmer clears state
93
+ // before the second can confirm.
94
+ let completedPeerName = null;
95
+ let completedAt = 0;
90
96
  app.post('/api/v1/pair/confirm', async (request, reply) => {
91
97
  const body = request.body;
98
+ // Accept confirm on recently-completed sessions (race condition fix)
99
+ if ((!session || session.state === 'completed') && completedPeerName && body.confirmed) {
100
+ // Session was already confirmed by the other side — return success
101
+ if (Date.now() - completedAt < 60_000) {
102
+ return { success: true, message: `Paired with ${completedPeerName}` };
103
+ }
104
+ }
92
105
  if (!session || session.state !== 'key_exchanged') {
93
106
  return reply.status(404).send({ error: 'No pairing session awaiting confirmation' });
94
107
  }
@@ -97,6 +110,7 @@ export function registerPairRoutes(app, ctx) {
97
110
  session = null;
98
111
  sessionEcdh = null;
99
112
  pendingPeer = null;
113
+ completedPeerName = null;
100
114
  return { success: false, message: 'Pairing cancelled' };
101
115
  }
102
116
  if (!pendingPeer) {
@@ -105,14 +119,15 @@ export function registerPairRoutes(app, ctx) {
105
119
  // Write peer to config
106
120
  await addPeerToConfig(pendingPeer.hostname, pendingPeer.apiKey, pendingPeer.certFingerprint, ctx.dataDir);
107
121
  session.state = 'completed';
108
- const completedPeer = pendingPeer.hostname;
109
- // Cleanup
122
+ completedPeerName = pendingPeer.hostname;
123
+ completedAt = Date.now();
124
+ // Cleanup session but keep completedPeerName for late confirmers
110
125
  session = null;
111
126
  sessionEcdh = null;
112
127
  pendingPeer = null;
113
128
  if (sessionTimeout)
114
129
  clearTimeout(sessionTimeout);
115
- return { success: true, message: `Paired with ${completedPeer}` };
130
+ return { success: true, message: `Paired with ${completedPeerName}` };
116
131
  });
117
132
  // GET /api/v1/pair/status — Check current pairing session state
118
133
  app.get('/api/v1/pair/status', async () => {
@@ -1 +1 @@
1
- {"version":3,"file":"pair.js","sourceRoot":"","sources":["../../src/routes/pair.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pair.js","sourceRoot":"","sources":["../../src/routes/pair.ts"],"names":[],"mappings":"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"}
@@ -14,6 +14,16 @@ export declare class AiTaskManager {
14
14
  private daemonPort;
15
15
  private apiKey;
16
16
  constructor(config: AiTaskManagerConfig);
17
+ /** Auto-detect the first available agent CLI */
18
+ private detectAgent;
19
+ /** Build CLI args for Claude */
20
+ private buildClaudeArgs;
21
+ /** Build CLI args for Gemini CLI */
22
+ private buildGeminiArgs;
23
+ /** Build CLI args for Codex CLI */
24
+ private buildCodexArgs;
25
+ /** Build a sanitized env for the given agent */
26
+ private buildEnv;
17
27
  dispatch(params: AiTaskParams, fromNodeId: string): Promise<AiTaskInfo>;
18
28
  /**
19
29
  * Register a permission request from the hook script.
@@ -50,6 +60,8 @@ export declare class AiTaskManager {
50
60
  getHookScriptPath(): string;
51
61
  get activeCount(): number;
52
62
  private handleClaudeEvent;
63
+ private handleGeminiEvent;
64
+ private handleCodexEvent;
53
65
  private emit;
54
66
  }
55
67
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"ai-task-manager.d.ts","sourceRoot":"","sources":["../../src/services/ai-task-manager.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EAGV,sBAAsB,EACtB,iBAAiB,EACjB,sBAAsB,EACtB,uBAAuB,EACxB,MAAM,kBAAkB,CAAC;AAS1B,KAAK,aAAa,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;AA0BxD,MAAM,WAAW,mBAAmB;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,WAAW,CAA6E;IAChG,OAAO,CAAC,kBAAkB,CAA6C;IACvE,OAAO,CAAC,mBAAmB,CAA8C;IACzE,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,MAAM,CAAS;gBAEX,MAAM,EAAE,mBAAmB;IAMjC,QAAQ,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IA6O7E;;;OAGG;IACH,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,QAAQ,GAAG,WAAW,CAAC,GAAG,OAAO;IAmCjH;;;OAGG;IACH,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO;IAmClG;;;OAGG;IACH,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,uBAAuB,GAAG,IAAI;IAMxF;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO;IAIzF,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAc/B,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAQ/C,WAAW,IAAI,UAAU,EAAE;IAM3B,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,EAAE;IAQnD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI;IAOvE,SAAS,IAAI,IAAI;IAQjB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IASnC;;;OAGG;IACH,iBAAiB,IAAI,MAAM;IAO3B,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,OAAO,CAAC,iBAAiB;IAuDzB,OAAO,CAAC,IAAI;CAWb"}
1
+ {"version":3,"file":"ai-task-manager.d.ts","sourceRoot":"","sources":["../../src/services/ai-task-manager.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EAGV,sBAAsB,EACtB,iBAAiB,EAEjB,sBAAsB,EACtB,uBAAuB,EACxB,MAAM,kBAAkB,CAAC;AAU1B,KAAK,aAAa,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;AA2BxD,MAAM,WAAW,mBAAmB;IAClC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,WAAW,CAA6E;IAChG,OAAO,CAAC,kBAAkB,CAA6C;IACvE,OAAO,CAAC,mBAAmB,CAA8C;IACzE,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,MAAM,CAAS;gBAEX,MAAM,EAAE,mBAAmB;IAMvC,gDAAgD;YAClC,WAAW;IAWzB,gCAAgC;IAChC,OAAO,CAAC,eAAe;IA4BvB,oCAAoC;IACpC,OAAO,CAAC,eAAe;IAkBvB,mCAAmC;IACnC,OAAO,CAAC,cAAc;IAmBtB,gDAAgD;IAChD,OAAO,CAAC,QAAQ;IA4BV,QAAQ,CAAC,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAmP7E;;;OAGG;IACH,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,QAAQ,GAAG,WAAW,CAAC,GAAG,OAAO;IAmCjH;;;OAGG;IACH,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO;IAmClG;;;OAGG;IACH,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,uBAAuB,GAAG,IAAI;IAMxF;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,sBAAsB,GAAG;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO;IAIzF,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAyB/B,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS;IAQ/C,WAAW,IAAI,UAAU,EAAE;IAM3B,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,iBAAiB,EAAE;IAQnD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,GAAG,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI;IAOvE,SAAS,IAAI,IAAI;IAWjB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IASnC;;;OAGG;IACH,iBAAiB,IAAI,MAAM;IAO3B,IAAI,WAAW,IAAI,MAAM,CAExB;IAED,OAAO,CAAC,iBAAiB;IAuDzB,OAAO,CAAC,iBAAiB;IAoCzB,OAAO,CAAC,gBAAgB;IAqCxB,OAAO,CAAC,IAAI;CAWb"}
@@ -3,6 +3,7 @@ import { realpathSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { tmpdir, homedir } from 'node:os';
6
+ import { spawn } from 'node:child_process';
6
7
  import * as pty from 'node-pty';
7
8
  import { which } from '../utils/which.js';
8
9
  import { LoopsyError, LoopsyErrorCode, MAX_CONCURRENT_AI_TASKS, DEFAULT_AI_TASK_TIMEOUT, AI_TASK_EVENT_BUFFER_SIZE, } from '@loopsy/protocol';
@@ -34,19 +35,17 @@ export class AiTaskManager {
34
35
  this.daemonPort = config.daemonPort;
35
36
  this.apiKey = config.apiKey;
36
37
  }
37
- async dispatch(params, fromNodeId) {
38
- const activeCount = Array.from(this.tasks.values()).filter((t) => t.info.status === 'running' || t.info.status === 'waiting_approval').length;
39
- if (activeCount >= this.maxConcurrent) {
40
- throw new LoopsyError(LoopsyErrorCode.AI_TASK_MAX_CONCURRENT, `Max concurrent AI tasks (${this.maxConcurrent}) reached`);
41
- }
42
- // Find claude CLI
43
- const claudePath = await which('claude');
44
- if (!claudePath) {
45
- throw new LoopsyError(LoopsyErrorCode.AI_TASK_CLAUDE_NOT_FOUND, 'Claude CLI not found in PATH. Install claude-code first.');
38
+ /** Auto-detect the first available agent CLI */
39
+ async detectAgent() {
40
+ for (const agent of ['claude', 'gemini', 'codex']) {
41
+ const path = await which(agent);
42
+ if (path)
43
+ return agent;
46
44
  }
47
- const taskId = randomUUID();
48
- const now = Date.now();
49
- // Build CLI args
45
+ throw new LoopsyError(LoopsyErrorCode.AI_TASK_AGENT_NOT_FOUND, 'No AI agent CLI found in PATH. Install claude-code, gemini-cli, or codex-cli.');
46
+ }
47
+ /** Build CLI args for Claude */
48
+ buildClaudeArgs(params) {
50
49
  const args = ['-p', params.prompt, '--output-format', 'stream-json', '--verbose'];
51
50
  if (params.permissionMode) {
52
51
  args.push('--permission-mode', params.permissionMode);
@@ -69,34 +68,112 @@ export class AiTaskManager {
69
68
  if (params.additionalArgs?.length) {
70
69
  args.push(...params.additionalArgs);
71
70
  }
72
- // Strip all Claude-related env vars to avoid nesting detection
71
+ return args;
72
+ }
73
+ /** Build CLI args for Gemini CLI */
74
+ buildGeminiArgs(params) {
75
+ const args = ['-p', params.prompt, '--output-format', 'stream-json'];
76
+ if (params.permissionMode === 'bypassPermissions') {
77
+ args.push('--yolo');
78
+ }
79
+ else if (params.permissionMode === 'acceptEdits') {
80
+ args.push('--approval-mode', 'auto_edit');
81
+ }
82
+ if (params.model) {
83
+ args.push('-m', params.model);
84
+ }
85
+ if (params.additionalArgs?.length) {
86
+ args.push(...params.additionalArgs);
87
+ }
88
+ return args;
89
+ }
90
+ /** Build CLI args for Codex CLI */
91
+ buildCodexArgs(params) {
92
+ const args = ['exec', params.prompt, '--json'];
93
+ if (params.permissionMode === 'bypassPermissions' || params.permissionMode === 'acceptEdits') {
94
+ args.push('--full-auto');
95
+ }
96
+ if (params.model) {
97
+ args.push('-m', params.model);
98
+ }
99
+ if (params.cwd) {
100
+ args.push('--cd', params.cwd);
101
+ }
102
+ if (params.additionalArgs?.length) {
103
+ args.push(...params.additionalArgs);
104
+ }
105
+ return args;
106
+ }
107
+ /** Build a sanitized env for the given agent */
108
+ buildEnv(agent, taskId) {
73
109
  const env = {};
74
110
  for (const [key, val] of Object.entries(process.env)) {
75
111
  if (val === undefined)
76
112
  continue;
77
- if (key.startsWith('CLAUDE') || key.startsWith('ANTHROPIC_') || key.startsWith('OTEL_') || key === 'MCP_') {
78
- continue;
113
+ // Per-agent env stripping
114
+ switch (agent) {
115
+ case 'claude':
116
+ if (key.startsWith('CLAUDE') || key.startsWith('ANTHROPIC_') || key.startsWith('OTEL_') || key === 'MCP_')
117
+ continue;
118
+ break;
119
+ case 'gemini':
120
+ if (key.startsWith('GEMINI_') && key !== 'GEMINI_API_KEY')
121
+ continue;
122
+ break;
123
+ case 'codex':
124
+ if (key.startsWith('CODEX_') && key !== 'CODEX_API_KEY')
125
+ continue;
126
+ break;
79
127
  }
80
128
  env[key] = val;
81
129
  }
82
- // Set env vars for the permission hook script (also available via CLI args)
130
+ // Loopsy task env vars
83
131
  env.LOOPSY_TASK_ID = taskId;
84
132
  env.LOOPSY_DAEMON_PORT = String(this.daemonPort);
85
133
  env.LOOPSY_API_KEY = this.apiKey;
134
+ return env;
135
+ }
136
+ async dispatch(params, fromNodeId) {
137
+ const activeCount = Array.from(this.tasks.values()).filter((t) => t.info.status === 'running' || t.info.status === 'waiting_approval').length;
138
+ if (activeCount >= this.maxConcurrent) {
139
+ throw new LoopsyError(LoopsyErrorCode.AI_TASK_MAX_CONCURRENT, `Max concurrent AI tasks (${this.maxConcurrent}) reached`);
140
+ }
141
+ // Resolve agent
142
+ const agentParam = params.agent || 'auto';
143
+ const resolvedAgent = agentParam === 'auto'
144
+ ? await this.detectAgent()
145
+ : agentParam;
146
+ // Find CLI binary
147
+ const cliPath = await which(resolvedAgent);
148
+ if (!cliPath) {
149
+ const errorCode = resolvedAgent === 'claude'
150
+ ? LoopsyErrorCode.AI_TASK_CLAUDE_NOT_FOUND
151
+ : LoopsyErrorCode.AI_TASK_AGENT_NOT_FOUND;
152
+ throw new LoopsyError(errorCode, `${resolvedAgent} CLI not found in PATH`);
153
+ }
154
+ const taskId = randomUUID();
155
+ const now = Date.now();
156
+ // Build per-agent CLI args
157
+ let args;
158
+ switch (resolvedAgent) {
159
+ case 'claude':
160
+ args = this.buildClaudeArgs(params);
161
+ break;
162
+ case 'gemini':
163
+ args = this.buildGeminiArgs(params);
164
+ break;
165
+ case 'codex':
166
+ args = this.buildCodexArgs(params);
167
+ break;
168
+ }
169
+ // Build sanitized env
170
+ const env = this.buildEnv(resolvedAgent, taskId);
86
171
  const isBypass = params.permissionMode === 'bypassPermissions';
87
172
  const taskCwd = params.cwd || homedir();
88
173
  const taskTmpDir = join(tmpdir(), `loopsy-task-${taskId}`);
89
174
  let spawnCwd;
90
- if (isBypass) {
91
- // Bypass mode: no hook needed — run directly in the actual cwd.
92
- // The hook would register permission requests that nobody approves,
93
- // causing a deadlock. Skip it entirely.
94
- mkdirSync(taskTmpDir, { recursive: true }); // still needed for cleanup tracking
95
- spawnCwd = taskCwd;
96
- }
97
- else {
98
- // Hook mode: cwd = taskTmpDir so Claude discovers .claude/settings.local.json
99
- // containing the PreToolUse hook config with task-specific args baked in.
175
+ // Permission hook setup — Claude only, non-bypass mode
176
+ if (resolvedAgent === 'claude' && !isBypass) {
100
177
  const hookScriptPath = this.getHookScriptPath();
101
178
  const claudeSettingsDir = join(taskTmpDir, '.claude');
102
179
  mkdirSync(claudeSettingsDir, { recursive: true });
@@ -112,43 +189,57 @@ export class AiTaskManager {
112
189
  }],
113
190
  },
114
191
  }, null, 2));
115
- // Write a CLAUDE.md that tells Claude the actual working directory
116
192
  writeFileSync(join(taskTmpDir, 'CLAUDE.md'), `Your actual working directory is: ${taskCwd}\n` +
117
193
  `Always use absolute paths rooted at ${taskCwd} for all file operations.\n`);
118
- // Grant access to the actual cwd and user's home directory
119
194
  args.push('--add-dir', taskCwd);
120
195
  if (homedir() !== taskCwd) {
121
196
  args.push('--add-dir', homedir());
122
197
  }
123
198
  spawnCwd = taskTmpDir;
124
199
  }
125
- // Use node-pty to spawn with a pseudo-TTY
126
- // Claude CLI (Bun runtime) buffers stdout when connected to a pipe;
127
- // a PTY ensures real-time streaming output
128
- let spawnFile;
129
- let spawnArgs;
130
- if (process.platform === 'win32') {
131
- // On Windows, .cmd files must be spawned via cmd.exe for ConPTY to work
132
- spawnFile = 'cmd.exe';
133
- spawnArgs = ['/c', claudePath, ...args];
134
- }
135
200
  else {
136
- // On macOS/Linux, resolve symlinks for node-pty
137
- try {
138
- spawnFile = realpathSync(claudePath);
201
+ mkdirSync(taskTmpDir, { recursive: true });
202
+ spawnCwd = taskCwd;
203
+ }
204
+ // Spawn the process
205
+ let ptyProcess;
206
+ let childProcess;
207
+ let pid;
208
+ if (resolvedAgent === 'claude') {
209
+ // Claude needs PTY (Bun runtime buffers stdout on pipes)
210
+ let spawnFile;
211
+ let spawnArgs;
212
+ if (process.platform === 'win32') {
213
+ spawnFile = 'cmd.exe';
214
+ spawnArgs = ['/c', cliPath, ...args];
139
215
  }
140
- catch {
141
- spawnFile = claudePath;
216
+ else {
217
+ try {
218
+ spawnFile = realpathSync(cliPath);
219
+ }
220
+ catch {
221
+ spawnFile = cliPath;
222
+ }
223
+ spawnArgs = args;
142
224
  }
143
- spawnArgs = args;
144
- }
145
- const proc = pty.spawn(spawnFile, spawnArgs, {
146
- name: 'xterm-256color',
147
- cols: process.platform === 'win32' ? 9999 : 32000, // ConPTY needs wide cols to avoid JSON line wrapping
148
- rows: 50,
149
- cwd: spawnCwd,
150
- env,
151
- });
225
+ ptyProcess = pty.spawn(spawnFile, spawnArgs, {
226
+ name: 'xterm-256color',
227
+ cols: process.platform === 'win32' ? 9999 : 32000,
228
+ rows: 50,
229
+ cwd: spawnCwd,
230
+ env,
231
+ });
232
+ pid = ptyProcess.pid;
233
+ }
234
+ else {
235
+ // Gemini and Codex use child_process.spawn
236
+ childProcess = spawn(cliPath, args, {
237
+ cwd: spawnCwd,
238
+ env,
239
+ stdio: ['ignore', 'pipe', 'pipe'],
240
+ });
241
+ pid = childProcess.pid;
242
+ }
152
243
  const info = {
153
244
  taskId,
154
245
  prompt: params.prompt,
@@ -156,12 +247,14 @@ export class AiTaskManager {
156
247
  startedAt: now,
157
248
  updatedAt: now,
158
249
  fromNodeId,
159
- pid: proc.pid,
250
+ pid,
160
251
  model: params.model,
252
+ agent: resolvedAgent,
161
253
  };
162
254
  const task = {
163
255
  info,
164
- ptyProcess: proc,
256
+ ptyProcess,
257
+ childProcess,
165
258
  eventBuffer: [],
166
259
  subscribers: new Set(),
167
260
  };
@@ -171,58 +264,57 @@ export class AiTaskManager {
171
264
  this.cancel(taskId);
172
265
  this.emit(task, { type: 'error', taskId, timestamp: Date.now(), data: 'Task timed out' });
173
266
  }, DEFAULT_AI_TASK_TIMEOUT);
174
- // Buffer for incomplete lines from PTY
267
+ // Buffer for incomplete lines
175
268
  let lineBuffer = '';
176
- // Parse PTY output data arrives as chunks that may contain partial lines
177
- proc.onData((data) => {
178
- // Debug: log raw PTY data
269
+ const processLine = (line) => {
270
+ if (!line)
271
+ return;
272
+ if (process.env.LOOPSY_DEBUG_PTY) {
273
+ console.log(`[${resolvedAgent.toUpperCase()} ${taskId.slice(0, 8)}] line(${line.length}): ${line.slice(0, 100)}...`);
274
+ }
275
+ try {
276
+ const parsed = JSON.parse(line);
277
+ switch (resolvedAgent) {
278
+ case 'claude':
279
+ this.handleClaudeEvent(task, parsed);
280
+ break;
281
+ case 'gemini':
282
+ this.handleGeminiEvent(task, parsed);
283
+ break;
284
+ case 'codex':
285
+ this.handleCodexEvent(task, parsed);
286
+ break;
287
+ }
288
+ }
289
+ catch (err) {
290
+ if (process.env.LOOPSY_DEBUG_PTY) {
291
+ console.log(`[${resolvedAgent.toUpperCase()} ${taskId.slice(0, 8)}] JSON parse fail: ${err.message}, line starts: ${JSON.stringify(line.slice(0, 80))}`);
292
+ }
293
+ if (line.length > 0) {
294
+ this.emit(task, { type: 'text', taskId, timestamp: Date.now(), data: line });
295
+ }
296
+ }
297
+ };
298
+ const processChunk = (data, needsAnsiStrip) => {
179
299
  if (process.env.LOOPSY_DEBUG_PTY) {
180
- console.log(`[PTY ${taskId.slice(0, 8)}] raw(${data.length}): ${JSON.stringify(data.slice(0, 300))}`);
300
+ console.log(`[${resolvedAgent.toUpperCase()} ${taskId.slice(0, 8)}] raw(${data.length}): ${JSON.stringify(data.slice(0, 300))}`);
181
301
  }
182
- // Strip ANSI before buffering to prevent escape sequences splitting JSON
183
- const cleaned = stripAnsi(data);
302
+ const cleaned = needsAnsiStrip ? stripAnsi(data) : data;
184
303
  lineBuffer += cleaned;
185
304
  const lines = lineBuffer.split('\n');
186
- // Keep the last element (may be incomplete)
187
305
  lineBuffer = lines.pop() || '';
188
306
  for (const rawLine of lines) {
189
307
  const line = rawLine.trim();
190
- if (!line)
191
- continue;
192
- if (process.env.LOOPSY_DEBUG_PTY) {
193
- console.log(`[PTY ${taskId.slice(0, 8)}] line(${line.length}): ${line.slice(0, 100)}...`);
194
- }
195
- try {
196
- const parsed = JSON.parse(line);
197
- this.handleClaudeEvent(task, parsed);
198
- }
199
- catch (err) {
200
- if (process.env.LOOPSY_DEBUG_PTY) {
201
- console.log(`[PTY ${taskId.slice(0, 8)}] JSON parse fail: ${err.message}, line starts: ${JSON.stringify(line.slice(0, 80))}`);
202
- }
203
- // Non-JSON line — forward as text
204
- if (line.length > 0) {
205
- this.emit(task, { type: 'text', taskId, timestamp: Date.now(), data: line });
206
- }
207
- }
308
+ if (line)
309
+ processLine(line);
208
310
  }
209
- });
210
- // Handle process exit
211
- proc.onExit(({ exitCode, signal }) => {
212
- // Process any remaining buffered data
311
+ };
312
+ const handleExit = (exitCode, signal) => {
313
+ // Process remaining buffer
213
314
  if (lineBuffer.trim()) {
214
- const line = stripAnsi(lineBuffer).trim();
215
- if (line) {
216
- try {
217
- const parsed = JSON.parse(line);
218
- this.handleClaudeEvent(task, parsed);
219
- }
220
- catch {
221
- if (line.length > 0) {
222
- this.emit(task, { type: 'text', taskId, timestamp: Date.now(), data: line });
223
- }
224
- }
225
- }
315
+ const line = (resolvedAgent === 'claude' ? stripAnsi(lineBuffer) : lineBuffer).trim();
316
+ if (line)
317
+ processLine(line);
226
318
  lineBuffer = '';
227
319
  }
228
320
  if (task.timeoutTimer)
@@ -236,15 +328,32 @@ export class AiTaskManager {
236
328
  info.error = signal ? `Killed by signal ${signal}` : `Exit code ${exitCode}`;
237
329
  }
238
330
  this.emit(task, { type: 'exit', taskId, timestamp: Date.now(), data: { exitCode, signal } });
239
- // Move to completed tasks (preserve event buffer) — kept until explicitly deleted
240
331
  this.recentTasks.set(taskId, { info: { ...info }, eventBuffer: [...task.eventBuffer] });
241
332
  this.tasks.delete(taskId);
242
- // Clean up per-task temp directory
243
333
  try {
244
334
  rmSync(taskTmpDir, { recursive: true, force: true });
245
335
  }
246
336
  catch { }
247
- });
337
+ };
338
+ if (ptyProcess) {
339
+ // PTY data handler (Claude)
340
+ ptyProcess.onData((data) => processChunk(data, true));
341
+ ptyProcess.onExit(({ exitCode, signal }) => handleExit(exitCode, signal));
342
+ }
343
+ else if (childProcess) {
344
+ // child_process data handler (Gemini/Codex)
345
+ childProcess.stdout.on('data', (data) => processChunk(data.toString(), false));
346
+ childProcess.stderr.on('data', (data) => {
347
+ const text = data.toString().trim();
348
+ if (text) {
349
+ this.emit(task, { type: 'text', taskId, timestamp: Date.now(), data: text });
350
+ }
351
+ });
352
+ childProcess.on('exit', (code, signal) => {
353
+ const sigNum = signal ? ({ SIGTERM: 15, SIGKILL: 9 }[signal] || 1) : null;
354
+ handleExit(code ?? null, sigNum);
355
+ });
356
+ }
248
357
  // Emit initial status
249
358
  this.emit(task, { type: 'status', taskId, timestamp: now, data: { status: 'running' } });
250
359
  return { ...info };
@@ -338,15 +447,28 @@ export class AiTaskManager {
338
447
  return false;
339
448
  task.info.status = 'cancelled';
340
449
  task.info.updatedAt = Date.now();
341
- task.ptyProcess.kill('SIGTERM');
342
- setTimeout(() => {
343
- if (this.tasks.has(taskId)) {
344
- try {
345
- task.ptyProcess.kill('SIGKILL');
450
+ if (task.ptyProcess) {
451
+ task.ptyProcess.kill('SIGTERM');
452
+ setTimeout(() => {
453
+ if (this.tasks.has(taskId)) {
454
+ try {
455
+ task.ptyProcess.kill('SIGKILL');
456
+ }
457
+ catch { }
346
458
  }
347
- catch { }
348
- }
349
- }, 5000);
459
+ }, 5000);
460
+ }
461
+ else if (task.childProcess) {
462
+ task.childProcess.kill('SIGTERM');
463
+ setTimeout(() => {
464
+ if (this.tasks.has(taskId)) {
465
+ try {
466
+ task.childProcess.kill('SIGKILL');
467
+ }
468
+ catch { }
469
+ }
470
+ }, 5000);
471
+ }
350
472
  return true;
351
473
  }
352
474
  getTask(taskId) {
@@ -382,7 +504,10 @@ export class AiTaskManager {
382
504
  cancelAll() {
383
505
  for (const [id, task] of this.tasks) {
384
506
  try {
385
- task.ptyProcess.kill('SIGKILL');
507
+ if (task.ptyProcess)
508
+ task.ptyProcess.kill('SIGKILL');
509
+ else if (task.childProcess)
510
+ task.childProcess.kill('SIGKILL');
386
511
  }
387
512
  catch { }
388
513
  if (task.timeoutTimer)
@@ -473,6 +598,92 @@ export class AiTaskManager {
473
598
  this.emit(task, { type: 'text', taskId, timestamp: ts, data: JSON.stringify(parsed) });
474
599
  }
475
600
  }
601
+ handleGeminiEvent(task, parsed) {
602
+ const taskId = task.info.taskId;
603
+ const ts = Date.now();
604
+ const cliType = parsed.type;
605
+ if (cliType === 'init') {
606
+ this.emit(task, { type: 'system', taskId, timestamp: ts, data: parsed });
607
+ }
608
+ else if (cliType === 'message') {
609
+ // Extract text from content blocks
610
+ const content = parsed.content || parsed.message?.content || [];
611
+ if (Array.isArray(content)) {
612
+ for (const block of content) {
613
+ if (block.type === 'text' || block.text) {
614
+ this.emit(task, { type: 'text', taskId, timestamp: ts, data: block.text || '' });
615
+ }
616
+ }
617
+ }
618
+ else if (typeof content === 'string') {
619
+ this.emit(task, { type: 'text', taskId, timestamp: ts, data: content });
620
+ }
621
+ else if (parsed.text) {
622
+ this.emit(task, { type: 'text', taskId, timestamp: ts, data: parsed.text });
623
+ }
624
+ }
625
+ else if (cliType === 'tool_use') {
626
+ this.emit(task, { type: 'tool_use', taskId, timestamp: ts, data: { name: parsed.name, input: parsed.input } });
627
+ }
628
+ else if (cliType === 'tool_result') {
629
+ this.emit(task, { type: 'tool_result', taskId, timestamp: ts, data: { name: parsed.name, content: parsed.content, output: parsed.output } });
630
+ }
631
+ else if (cliType === 'result') {
632
+ this.emit(task, { type: 'result', taskId, timestamp: ts, data: { text: parsed.response || parsed.result, cost: parsed.stats?.cost, duration: parsed.stats?.duration_ms } });
633
+ }
634
+ else if (cliType === 'error') {
635
+ task.info.error = parsed.message || parsed.error || JSON.stringify(parsed);
636
+ this.emit(task, { type: 'error', taskId, timestamp: ts, data: parsed.message || parsed.error || parsed });
637
+ }
638
+ else {
639
+ // Forward unknown events as text
640
+ this.emit(task, { type: 'text', taskId, timestamp: ts, data: JSON.stringify(parsed) });
641
+ }
642
+ }
643
+ handleCodexEvent(task, parsed) {
644
+ const taskId = task.info.taskId;
645
+ const ts = Date.now();
646
+ const cliType = parsed.type;
647
+ if (cliType === 'thread.started') {
648
+ this.emit(task, { type: 'system', taskId, timestamp: ts, data: parsed });
649
+ }
650
+ else if (cliType === 'item.completed') {
651
+ const item = parsed.item || parsed;
652
+ const itemType = item.type || item.item_type;
653
+ if (itemType === 'agent_message' || itemType === 'message') {
654
+ const text = item.content || item.text || '';
655
+ if (text)
656
+ this.emit(task, { type: 'text', taskId, timestamp: ts, data: text });
657
+ }
658
+ else if (itemType === 'reasoning') {
659
+ const text = item.content || item.text || '';
660
+ if (text)
661
+ this.emit(task, { type: 'thinking', taskId, timestamp: ts, data: text });
662
+ }
663
+ else if (itemType === 'command_execution') {
664
+ this.emit(task, { type: 'tool_use', taskId, timestamp: ts, data: { name: 'command', input: { command: item.command || item.input } } });
665
+ if (item.output || item.result) {
666
+ this.emit(task, { type: 'tool_result', taskId, timestamp: ts, data: { name: 'command', content: item.output || item.result } });
667
+ }
668
+ }
669
+ else if (itemType === 'file_change') {
670
+ this.emit(task, { type: 'tool_use', taskId, timestamp: ts, data: { name: 'file_change', input: { file: item.file || item.path, action: item.action } } });
671
+ }
672
+ else if (itemType === 'mcp_tool_call') {
673
+ this.emit(task, { type: 'tool_use', taskId, timestamp: ts, data: { name: item.tool_name || item.name, input: item.input || item.arguments } });
674
+ }
675
+ }
676
+ else if (cliType === 'error') {
677
+ task.info.error = parsed.message || parsed.error || JSON.stringify(parsed);
678
+ this.emit(task, { type: 'error', taskId, timestamp: ts, data: parsed.message || parsed.error || parsed });
679
+ }
680
+ else if (cliType === 'turn.completed') {
681
+ // Ignore — exit event handles completion
682
+ }
683
+ else {
684
+ this.emit(task, { type: 'text', taskId, timestamp: ts, data: JSON.stringify(parsed) });
685
+ }
686
+ }
476
687
  emit(task, event) {
477
688
  // Buffer event
478
689
  task.eventBuffer.push(event);