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.
- package/README.md +24 -16
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +28 -14
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +21 -10
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +81 -42
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/pair.js +29 -3
- package/dist/cli/commands/pair.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/daemon/routes/ai-tasks.js +1 -1
- package/dist/daemon/routes/ai-tasks.js.map +1 -1
- package/dist/daemon/routes/pair.d.ts.map +1 -1
- package/dist/daemon/routes/pair.js +18 -3
- package/dist/daemon/routes/pair.js.map +1 -1
- package/dist/daemon/services/ai-task-manager.d.ts +12 -0
- package/dist/daemon/services/ai-task-manager.d.ts.map +1 -1
- package/dist/daemon/services/ai-task-manager.js +319 -108
- package/dist/daemon/services/ai-task-manager.js.map +1 -1
- package/dist/dashboard/public/views/ai-tasks.js +51 -4
- package/dist/mcp-server/daemon-client.d.ts.map +1 -1
- package/dist/mcp-server/daemon-client.js +7 -2
- package/dist/mcp-server/daemon-client.js.map +1 -1
- package/dist/protocol/errors.d.ts +1 -0
- package/dist/protocol/errors.d.ts.map +1 -1
- package/dist/protocol/errors.js +1 -0
- package/dist/protocol/errors.js.map +1 -1
- package/dist/protocol/schemas.d.ts +3 -0
- package/dist/protocol/schemas.d.ts.map +1 -1
- package/dist/protocol/schemas.js +1 -0
- package/dist/protocol/schemas.js.map +1 -1
- package/dist/protocol/types.d.ts +4 -0
- package/dist/protocol/types.d.ts.map +1 -1
- package/package.json +6 -3
- 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
|
-
|
|
109
|
-
|
|
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 ${
|
|
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":"AACA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,SAAS,IAAI,MAAM,EAAE,MAAM,MAAM,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAWjG,MAAM,UAAU,kBAAkB,CAAC,GAAoB,EAAE,GAAgB;IACvE,8CAA8C;IAC9C,IAAI,OAAO,GAA0B,IAAI,CAAC;IAC1C,IAAI,WAAW,GAAyC,IAAI,CAAC;IAC7D,IAAI,cAAc,GAAyC,IAAI,CAAC;IAChE,sDAAsD;IACtD,IAAI,WAAW,GAA0E,IAAI,CAAC;IAE9F,+DAA+D;IAC/D,GAAG,CAAC,IAAI,CAAC,oBAAoB,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;QACvD,IAAI,OAAO,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;YAC7E,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAC,CAAC;QAC7E,CAAC;QAED,wBAAwB;QACxB,MAAM,IAAI,GAAG,UAAU,CAAC,YAAY,CAAC,CAAC;QACtC,IAAI,CAAC,YAAY,EAAE,CAAC;QAEpB,kCAAkC;QAClC,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,YAAY,EAAE,GAAG,EAAE,IAAI,mBAAmB,CAAC,CAAC,QAAQ,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC;QAElH,OAAO,GAAG;YACR,UAAU,EAAE,IAAI;YAChB,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC;YACtC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,eAAe;YACvC,KAAK,EAAE,SAAS;SACjB,CAAC;QACF,WAAW,GAAG,IAAI,CAAC;QACnB,WAAW,GAAG,IAAI,CAAC;QAEnB,cAAc;QACd,IAAI,cAAc;YAAE,YAAY,CAAC,cAAc,CAAC,CAAC;QACjD,cAAc,GAAG,UAAU,CAAC,GAAG,EAAE;YAC/B,IAAI,OAAO;gBAAE,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC;YACvC,OAAO,GAAG,IAAI,CAAC;YACf,WAAW,GAAG,IAAI,CAAC;YACnB,WAAW,GAAG,IAAI,CAAC;QACrB,CAAC,EAAE,eAAe,CAAC,CAAC;QAEpB,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,4EAA4E;IAC5E,GAAG,CAAC,IAAI,CAAC,uBAAuB,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACzD,MAAM,IAAI,GAAG,OAAO,CAAC,IAA0B,CAAC;QAEhD,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC5C,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC,CAAC;QACxE,CAAC;QAED,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;YACnC,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC;YAC1B,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC,CAAC;QACtE,CAAC;QAED,IAAI,IAAI,CAAC,UAAU,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC;YAC3C,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC;QAClE,CAAC;QAED,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,oCAAoC,EAAE,CAAC,CAAC;QACjF,CAAC;QAED,iCAAiC;QACjC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QACzD,MAAM,YAAY,GAAG,WAAW,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;QAE3D,8EAA8E;QAC9E,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,CAAC;QACxF,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAEtE,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,SAAS,CAAC;QACvC,OAAO,CAAC,GAAG,GAAG,GAAG,CAAC;QAClB,OAAO,CAAC,KAAK,GAAG,eAAe,CAAC;QAChC,WAAW,GAAG;YACZ,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,eAAe,EAAE,IAAI,CAAC,eAAe;SACtC,CAAC;QAEF,uCAAuC;QACvC,IAAI,cAAkC,CAAC;QACvC,IAAI,GAAG,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC;YAC/C,cAAc,GAAG,KAAK,CAAC,WAAW,CAAC;QACrC,CAAC;QAED,MAAM,QAAQ,GAAwB;YACpC,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,eAAe,EAAE,cAAc;YAC/B,GAAG;SACJ,CAAC;QAEF,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,iEAAiE;IACjE,GAAG,CAAC,IAAI,CAAC,sBAAsB,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACxD,MAAM,IAAI,GAAG,OAAO,CAAC,IAA6B,CAAC;QAEnD,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK,KAAK,eAAe,EAAE,CAAC;YAClD,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0CAA0C,EAAE,CAAC,CAAC;QACvF,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC;YAC1B,OAAO,GAAG,IAAI,CAAC;YACf,WAAW,GAAG,IAAI,CAAC;YACnB,WAAW,GAAG,IAAI,CAAC;YACnB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC;QAC1D,CAAC;QAED,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC,CAAC;QACnE,CAAC;QAED,uBAAuB;QACvB,MAAM,eAAe,CAAC,WAAW,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,eAAe,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QAE1G,OAAO,CAAC,KAAK,GAAG,WAAW,CAAC;QAC5B,
|
|
1
|
+
{"version":3,"file":"pair.js","sourceRoot":"","sources":["../../src/routes/pair.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,SAAS,IAAI,MAAM,EAAE,MAAM,MAAM,CAAC;AAC/D,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAWjG,MAAM,UAAU,kBAAkB,CAAC,GAAoB,EAAE,GAAgB;IACvE,8CAA8C;IAC9C,IAAI,OAAO,GAA0B,IAAI,CAAC;IAC1C,IAAI,WAAW,GAAyC,IAAI,CAAC;IAC7D,IAAI,cAAc,GAAyC,IAAI,CAAC;IAChE,sDAAsD;IACtD,IAAI,WAAW,GAA0E,IAAI,CAAC;IAE9F,+DAA+D;IAC/D,GAAG,CAAC,IAAI,CAAC,oBAAoB,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;QACvD,IAAI,OAAO,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;YAC7E,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAC,CAAC;QAC7E,CAAC;QAED,wBAAwB;QACxB,MAAM,IAAI,GAAG,UAAU,CAAC,YAAY,CAAC,CAAC;QACtC,IAAI,CAAC,YAAY,EAAE,CAAC;QAEpB,kCAAkC;QAClC,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,YAAY,EAAE,GAAG,EAAE,IAAI,mBAAmB,CAAC,CAAC,QAAQ,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAC;QAElH,OAAO,GAAG;YACR,UAAU,EAAE,IAAI;YAChB,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC;YACtC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,eAAe;YACvC,KAAK,EAAE,SAAS;SACjB,CAAC;QACF,WAAW,GAAG,IAAI,CAAC;QACnB,WAAW,GAAG,IAAI,CAAC;QAEnB,cAAc;QACd,IAAI,cAAc;YAAE,YAAY,CAAC,cAAc,CAAC,CAAC;QACjD,cAAc,GAAG,UAAU,CAAC,GAAG,EAAE;YAC/B,IAAI,OAAO;gBAAE,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC;YACvC,OAAO,GAAG,IAAI,CAAC;YACf,WAAW,GAAG,IAAI,CAAC;YACnB,WAAW,GAAG,IAAI,CAAC;QACrB,CAAC,EAAE,eAAe,CAAC,CAAC;QAEpB,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,4EAA4E;IAC5E,GAAG,CAAC,IAAI,CAAC,uBAAuB,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACzD,MAAM,IAAI,GAAG,OAAO,CAAC,IAA0B,CAAC;QAEhD,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC5C,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC,CAAC;QACxE,CAAC;QAED,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;YACnC,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC;YAC1B,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC,CAAC;QACtE,CAAC;QAED,IAAI,IAAI,CAAC,UAAU,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC;YAC3C,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC;QAClE,CAAC;QAED,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,oCAAoC,EAAE,CAAC,CAAC;QACjF,CAAC;QAED,iCAAiC;QACjC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QACzD,MAAM,YAAY,GAAG,WAAW,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;QAE3D,8EAA8E;QAC9E,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,EAAE,CAAC;QACxF,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QAEtE,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,SAAS,CAAC;QACvC,OAAO,CAAC,GAAG,GAAG,GAAG,CAAC;QAClB,OAAO,CAAC,KAAK,GAAG,eAAe,CAAC;QAChC,WAAW,GAAG;YACZ,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,eAAe,EAAE,IAAI,CAAC,eAAe;SACtC,CAAC;QAEF,uCAAuC;QACvC,IAAI,cAAkC,CAAC;QACvC,IAAI,GAAG,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC;YAC/C,cAAc,GAAG,KAAK,CAAC,WAAW,CAAC;QACrC,CAAC;QAED,MAAM,QAAQ,GAAwB;YACpC,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,QAAQ,EAAE,GAAG,CAAC,QAAQ;YACtB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,eAAe,EAAE,cAAc;YAC/B,GAAG;SACJ,CAAC;QAEF,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,iEAAiE;IACjE,4EAA4E;IAC5E,8EAA8E;IAC9E,4EAA4E;IAC5E,iCAAiC;IACjC,IAAI,iBAAiB,GAAkB,IAAI,CAAC;IAC5C,IAAI,WAAW,GAAW,CAAC,CAAC;IAE5B,GAAG,CAAC,IAAI,CAAC,sBAAsB,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACxD,MAAM,IAAI,GAAG,OAAO,CAAC,IAA6B,CAAC;QAEnD,qEAAqE;QACrE,IAAI,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK,KAAK,WAAW,CAAC,IAAI,iBAAiB,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACvF,mEAAmE;YACnE,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,WAAW,GAAG,MAAM,EAAE,CAAC;gBACtC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,iBAAiB,EAAE,EAAE,CAAC;YACxE,CAAC;QACH,CAAC;QAED,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK,KAAK,eAAe,EAAE,CAAC;YAClD,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0CAA0C,EAAE,CAAC,CAAC;QACvF,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC;YAC1B,OAAO,GAAG,IAAI,CAAC;YACf,WAAW,GAAG,IAAI,CAAC;YACnB,WAAW,GAAG,IAAI,CAAC;YACnB,iBAAiB,GAAG,IAAI,CAAC;YACzB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,CAAC;QAC1D,CAAC;QAED,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC,CAAC;QACnE,CAAC;QAED,uBAAuB;QACvB,MAAM,eAAe,CAAC,WAAW,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,WAAW,CAAC,eAAe,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QAE1G,OAAO,CAAC,KAAK,GAAG,WAAW,CAAC;QAC5B,iBAAiB,GAAG,WAAW,CAAC,QAAQ,CAAC;QACzC,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAEzB,iEAAiE;QACjE,OAAO,GAAG,IAAI,CAAC;QACf,WAAW,GAAG,IAAI,CAAC;QACnB,WAAW,GAAG,IAAI,CAAC;QACnB,IAAI,cAAc;YAAE,YAAY,CAAC,cAAc,CAAC,CAAC;QAEjD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,iBAAiB,EAAE,EAAE,CAAC;IACxE,CAAC,CAAC,CAAC;IAEH,gEAAgE;IAChE,GAAG,CAAC,GAAG,CAAC,qBAAqB,EAAE,KAAK,IAAI,EAAE;QACxC,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QACvC,OAAO;YACL,MAAM,EAAE,IAAI;YACZ,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,GAAG,EAAE,OAAO,CAAC,KAAK,KAAK,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS;YAChE,SAAS,EAAE,OAAO,CAAC,SAAS;SAC7B,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,YAAoB,EAAE,UAAkB,EAAE,eAAwB,EAAE,OAAgB;IACjH,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,EAAE,WAAW,CAAC,CAAC;IAC7E,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,SAAS,CAAC,GAAG,CAAQ,CAAC;IAErC,qBAAqB;IACrB,IAAI,CAAC,MAAM,CAAC,IAAI;QAAE,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC;IACnC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW;QAAE,MAAM,CAAC,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC;IAC3D,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,YAAY,CAAC,GAAG,UAAU,CAAC;IAEnD,mCAAmC;IACnC,IAAI,eAAe,EAAE,CAAC;QACpB,IAAI,CAAC,MAAM,CAAC,GAAG;YAAE,MAAM,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;QACjD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,WAAW;YAAE,MAAM,CAAC,GAAG,CAAC,WAAW,GAAG,EAAE,CAAC;QACzD,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,YAAY,CAAC,GAAG,eAAe,CAAC;IACzD,CAAC;IAED,MAAM,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;AAC9C,CAAC"}
|
|
@@ -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":"
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
216
|
+
else {
|
|
217
|
+
try {
|
|
218
|
+
spawnFile = realpathSync(cliPath);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
spawnFile = cliPath;
|
|
222
|
+
}
|
|
223
|
+
spawnArgs = args;
|
|
142
224
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
250
|
+
pid,
|
|
160
251
|
model: params.model,
|
|
252
|
+
agent: resolvedAgent,
|
|
161
253
|
};
|
|
162
254
|
const task = {
|
|
163
255
|
info,
|
|
164
|
-
ptyProcess
|
|
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
|
|
267
|
+
// Buffer for incomplete lines
|
|
175
268
|
let lineBuffer = '';
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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(`[
|
|
300
|
+
console.log(`[${resolvedAgent.toUpperCase()} ${taskId.slice(0, 8)}] raw(${data.length}): ${JSON.stringify(data.slice(0, 300))}`);
|
|
181
301
|
}
|
|
182
|
-
|
|
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 (
|
|
191
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
|
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);
|