@kibitzsh/kibitz 0.0.4 → 0.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 +11 -8
- package/dist/cli/index.js +2676 -341
- package/dist/core/commentary.js +929 -602
- package/dist/core/platform-support.js +127 -155
- package/dist/core/session-dispatch.js +391 -379
- package/dist/core/watcher.js +852 -829
- package/package.json +5 -3
|
@@ -1,441 +1,453 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/core/session-dispatch.ts
|
|
31
|
+
var session_dispatch_exports = {};
|
|
32
|
+
__export(session_dispatch_exports, {
|
|
33
|
+
SessionDispatchService: () => SessionDispatchService,
|
|
34
|
+
buildExistingDispatchCommand: () => buildExistingDispatchCommand,
|
|
35
|
+
buildInteractiveDispatchCommand: () => buildInteractiveDispatchCommand,
|
|
36
|
+
resolveDispatchCommand: () => resolveDispatchCommand
|
|
17
37
|
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
module.exports = __toCommonJS(session_dispatch_exports);
|
|
39
|
+
var import_events = require("events");
|
|
40
|
+
var import_child_process2 = require("child_process");
|
|
41
|
+
var fs2 = __toESM(require("fs"));
|
|
42
|
+
var path2 = __toESM(require("path"));
|
|
43
|
+
|
|
44
|
+
// src/core/platform-support.ts
|
|
45
|
+
var import_child_process = require("child_process");
|
|
46
|
+
var fs = __toESM(require("fs"));
|
|
47
|
+
var path = __toESM(require("path"));
|
|
48
|
+
function getProviderCliCommand(provider, platform = process.platform) {
|
|
49
|
+
return platform === "win32" ? `${provider}.cmd` : provider;
|
|
50
|
+
}
|
|
51
|
+
function resolveCmdNodeScript(cmdPath) {
|
|
52
|
+
if (!String(cmdPath || "").toLowerCase().endsWith(".cmd")) return null;
|
|
53
|
+
try {
|
|
54
|
+
const content = fs.readFileSync(cmdPath, "utf8");
|
|
55
|
+
const match = content.match(/%dp0%\\(.+?\.js)/i);
|
|
56
|
+
if (!match) return null;
|
|
57
|
+
const scriptPath = path.join(path.dirname(cmdPath), match[1]);
|
|
58
|
+
return fs.existsSync(scriptPath) ? scriptPath : null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function findCommandPath(command, platform = process.platform) {
|
|
64
|
+
try {
|
|
65
|
+
if (platform === "win32") {
|
|
66
|
+
const out2 = (0, import_child_process.execSync)(`where ${command}`, {
|
|
67
|
+
encoding: "utf8",
|
|
68
|
+
timeout: 5e3,
|
|
69
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
70
|
+
}).trim();
|
|
71
|
+
const first = out2.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
72
|
+
return first || void 0;
|
|
50
73
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
this.emitStatus('sent', request.target, `Started new ${provider} session`);
|
|
75
|
-
}
|
|
76
|
-
catch (error) {
|
|
77
|
-
this.emitStatus('failed', request.target, normalizeError(error));
|
|
78
|
-
}
|
|
74
|
+
const out = (0, import_child_process.execFileSync)("which", [command], {
|
|
75
|
+
encoding: "utf8",
|
|
76
|
+
timeout: 5e3,
|
|
77
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
78
|
+
}).trim();
|
|
79
|
+
return out || void 0;
|
|
80
|
+
} catch {
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/core/session-dispatch.ts
|
|
86
|
+
var SessionDispatchService = class extends import_events.EventEmitter {
|
|
87
|
+
constructor(options) {
|
|
88
|
+
super();
|
|
89
|
+
this.options = options;
|
|
90
|
+
}
|
|
91
|
+
async dispatch(request) {
|
|
92
|
+
const prompt = String(request.prompt || "").trim();
|
|
93
|
+
if (!prompt) {
|
|
94
|
+
this.emitStatus("failed", request.target, "Prompt cannot be empty");
|
|
95
|
+
return;
|
|
79
96
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
this.emitStatus('failed', target, 'Missing target session or provider');
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const active = this.options.getActiveSessions();
|
|
88
|
-
const activeMatch = active.find((session) => {
|
|
89
|
-
return session.agent === targetAgent && session.id.toLowerCase() === targetSessionId;
|
|
90
|
-
});
|
|
91
|
-
if (!activeMatch) {
|
|
92
|
-
this.emitStatus('failed', target, `Selected ${describeProvider(targetAgent)} session is not active`);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
const targetLabel = describeTarget(target);
|
|
96
|
-
this.emitStatus('started', target, `Dispatching to ${targetLabel}`);
|
|
97
|
-
try {
|
|
98
|
-
const command = buildExistingDispatchCommand(target, prompt);
|
|
99
|
-
const dispatchCwd = deriveDispatchCwdForSession(activeMatch);
|
|
100
|
-
const beforeDispatch = captureSessionFileSnapshot(activeMatch.filePath);
|
|
101
|
-
const handle = await startBackgroundCommand(command, { cwd: dispatchCwd });
|
|
102
|
-
const dispatchOutcome = await runWithTimeout(waitForDispatchAcknowledgement(activeMatch.filePath, prompt, beforeDispatch, handle.completion), 20_000, 'Dispatch timed out waiting for target session update');
|
|
103
|
-
// If process exited before prompt was observed, enforce full verification.
|
|
104
|
-
if (dispatchOutcome === 'process-complete') {
|
|
105
|
-
verifyExistingDispatchDelivery(activeMatch.filePath, prompt, beforeDispatch);
|
|
106
|
-
}
|
|
107
|
-
// Keep completion promise observed to avoid unhandled rejections after early ack.
|
|
108
|
-
void handle.completion.catch(() => undefined);
|
|
109
|
-
this.emitStatus('sent', target, `Prompt sent to ${targetLabel}`);
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
this.emitStatus('failed', target, normalizeError(error));
|
|
113
|
-
}
|
|
97
|
+
this.emitStatus("queued", request.target, `Queued for ${describeTarget(request.target)}`);
|
|
98
|
+
if (request.target.kind === "existing") {
|
|
99
|
+
await this.dispatchExistingSession(request.target, prompt);
|
|
100
|
+
return;
|
|
114
101
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
this.
|
|
102
|
+
const provider = request.target.kind === "new-codex" ? "codex" : "claude";
|
|
103
|
+
this.emitStatus("started", request.target, `Starting new ${provider} session`);
|
|
104
|
+
try {
|
|
105
|
+
if (request.origin === "vscode") {
|
|
106
|
+
if (!this.options.launchInteractiveSession) {
|
|
107
|
+
throw new Error("Interactive launcher is not available in VS Code mode");
|
|
108
|
+
}
|
|
109
|
+
await this.options.launchInteractiveSession(provider, prompt);
|
|
110
|
+
} else {
|
|
111
|
+
await runInteractiveInTerminal(provider, prompt);
|
|
112
|
+
}
|
|
113
|
+
this.emitStatus("sent", request.target, `Started new ${provider} session`);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
this.emitStatus("failed", request.target, normalizeError(error));
|
|
123
116
|
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
117
|
+
}
|
|
118
|
+
async dispatchExistingSession(target, prompt) {
|
|
119
|
+
const targetSessionId = String(target.sessionId || "").trim().toLowerCase();
|
|
120
|
+
const targetAgent = target.agent;
|
|
121
|
+
if (!targetSessionId || targetAgent !== "claude" && targetAgent !== "codex") {
|
|
122
|
+
this.emitStatus("failed", target, "Missing target session or provider");
|
|
123
|
+
return;
|
|
129
124
|
}
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
const active = this.options.getActiveSessions();
|
|
126
|
+
const activeMatch = active.find((session) => {
|
|
127
|
+
return session.agent === targetAgent && session.id.toLowerCase() === targetSessionId;
|
|
128
|
+
});
|
|
129
|
+
if (!activeMatch) {
|
|
130
|
+
this.emitStatus("failed", target, `Selected ${describeProvider(targetAgent)} session is not active`);
|
|
131
|
+
return;
|
|
134
132
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
133
|
+
const targetLabel = describeTarget(target);
|
|
134
|
+
this.emitStatus("started", target, `Dispatching to ${targetLabel}`);
|
|
135
|
+
try {
|
|
136
|
+
const command = buildExistingDispatchCommand(target, prompt);
|
|
137
|
+
const dispatchCwd = deriveDispatchCwdForSession(activeMatch);
|
|
138
|
+
const beforeDispatch = captureSessionFileSnapshot(activeMatch.filePath);
|
|
139
|
+
const handle = await startBackgroundCommand(command, { cwd: dispatchCwd });
|
|
140
|
+
const dispatchOutcome = await runWithTimeout(
|
|
141
|
+
waitForDispatchAcknowledgement(
|
|
142
|
+
activeMatch.filePath,
|
|
143
|
+
prompt,
|
|
144
|
+
beforeDispatch,
|
|
145
|
+
handle.completion
|
|
146
|
+
),
|
|
147
|
+
2e4,
|
|
148
|
+
"Dispatch timed out waiting for target session update"
|
|
149
|
+
);
|
|
150
|
+
if (dispatchOutcome === "process-complete") {
|
|
151
|
+
verifyExistingDispatchDelivery(activeMatch.filePath, prompt, beforeDispatch);
|
|
152
|
+
}
|
|
153
|
+
void handle.completion.catch(() => void 0);
|
|
154
|
+
this.emitStatus("sent", target, `Prompt sent to ${targetLabel}`);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
this.emitStatus("failed", target, normalizeError(error));
|
|
141
157
|
}
|
|
158
|
+
}
|
|
159
|
+
emitStatus(state, target, message) {
|
|
160
|
+
const status = {
|
|
161
|
+
state,
|
|
162
|
+
message,
|
|
163
|
+
target,
|
|
164
|
+
timestamp: Date.now()
|
|
165
|
+
};
|
|
166
|
+
this.emit("status", status);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
function buildExistingDispatchCommand(target, prompt, platform = process.platform) {
|
|
170
|
+
if (target.kind !== "existing") {
|
|
171
|
+
throw new Error(`Expected existing target, got "${target.kind}"`);
|
|
172
|
+
}
|
|
173
|
+
const sessionId = String(target.sessionId || "").trim();
|
|
174
|
+
const agent = target.agent;
|
|
175
|
+
if (!sessionId || agent !== "claude" && agent !== "codex") {
|
|
176
|
+
throw new Error("Missing existing-session target details");
|
|
177
|
+
}
|
|
178
|
+
if (agent === "codex") {
|
|
142
179
|
return {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
180
|
+
provider: "codex",
|
|
181
|
+
command: getProviderCliCommand("codex", platform),
|
|
182
|
+
args: ["exec", "resume", "--json", "--skip-git-repo-check", sessionId, prompt]
|
|
146
183
|
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
provider: "claude",
|
|
187
|
+
command: getProviderCliCommand("claude", platform),
|
|
188
|
+
args: ["-p", prompt, "--verbose", "--output-format", "stream-json", "--resume", sessionId]
|
|
189
|
+
};
|
|
147
190
|
}
|
|
148
191
|
function buildInteractiveDispatchCommand(provider, prompt, platform = process.platform) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
192
|
+
return {
|
|
193
|
+
provider,
|
|
194
|
+
command: getProviderCliCommand(provider, platform),
|
|
195
|
+
args: [prompt]
|
|
196
|
+
};
|
|
154
197
|
}
|
|
155
198
|
function resolveDispatchCommand(command, platform = process.platform) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
if (pathHint.toLowerCase().endsWith('.cmd')) {
|
|
170
|
-
return {
|
|
171
|
-
command: pathHint,
|
|
172
|
-
args: command.args,
|
|
173
|
-
shell: true,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
199
|
+
const pathHint = findCommandPath(command.command, platform);
|
|
200
|
+
if (!pathHint) {
|
|
201
|
+
throw new Error(`CLI not found: ${command.command}`);
|
|
202
|
+
}
|
|
203
|
+
if (platform === "win32") {
|
|
204
|
+
const nodeScript = resolveCmdNodeScript(pathHint);
|
|
205
|
+
if (nodeScript) {
|
|
206
|
+
return {
|
|
207
|
+
command: process.execPath,
|
|
208
|
+
args: [nodeScript, ...command.args],
|
|
209
|
+
shell: false
|
|
210
|
+
};
|
|
176
211
|
}
|
|
177
|
-
|
|
212
|
+
if (pathHint.toLowerCase().endsWith(".cmd")) {
|
|
213
|
+
return {
|
|
178
214
|
command: pathHint,
|
|
179
215
|
args: command.args,
|
|
180
|
-
shell:
|
|
181
|
-
|
|
216
|
+
shell: true
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
command: pathHint,
|
|
222
|
+
args: command.args,
|
|
223
|
+
shell: false
|
|
224
|
+
};
|
|
182
225
|
}
|
|
183
226
|
async function waitForDispatchAcknowledgement(filePath, prompt, before, completion) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
227
|
+
let completionState = 0;
|
|
228
|
+
let completionError = null;
|
|
229
|
+
completion.then(() => {
|
|
230
|
+
completionState = 1;
|
|
231
|
+
}).catch((error) => {
|
|
232
|
+
completionState = 2;
|
|
233
|
+
completionError = error instanceof Error ? error : new Error(String(error || "Dispatch failed"));
|
|
234
|
+
});
|
|
235
|
+
while (true) {
|
|
236
|
+
if (completionState === 2) {
|
|
237
|
+
throw completionError || new Error("Dispatch failed");
|
|
238
|
+
}
|
|
239
|
+
if (hasSessionUpdateWithPrompt(filePath, prompt, before)) {
|
|
240
|
+
return "prompt-observed";
|
|
241
|
+
}
|
|
242
|
+
if (completionState === 1) {
|
|
243
|
+
return "process-complete";
|
|
244
|
+
}
|
|
245
|
+
await sleepMs(120);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async function startBackgroundCommand(command, options = {}) {
|
|
249
|
+
const resolved = resolveDispatchCommand(command);
|
|
250
|
+
return await new Promise((resolve, reject) => {
|
|
251
|
+
const child = (0, import_child_process2.spawn)(resolved.command, resolved.args, {
|
|
252
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
253
|
+
shell: resolved.shell,
|
|
254
|
+
windowsHide: true,
|
|
255
|
+
...options.cwd ? { cwd: options.cwd } : {}
|
|
191
256
|
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
257
|
+
let stderr = "";
|
|
258
|
+
let spawned = false;
|
|
259
|
+
let launchSettled = false;
|
|
260
|
+
const failLaunch = (error) => {
|
|
261
|
+
if (launchSettled) return;
|
|
262
|
+
launchSettled = true;
|
|
263
|
+
reject(error);
|
|
264
|
+
};
|
|
265
|
+
const completion = new Promise((resolveCompletion, rejectCompletion) => {
|
|
266
|
+
child.on("error", (error) => {
|
|
267
|
+
if (!spawned) {
|
|
268
|
+
failLaunch(error);
|
|
269
|
+
return;
|
|
195
270
|
}
|
|
196
|
-
|
|
197
|
-
|
|
271
|
+
rejectCompletion(error);
|
|
272
|
+
});
|
|
273
|
+
child.on("close", (code) => {
|
|
274
|
+
if (code === 0) {
|
|
275
|
+
resolveCompletion();
|
|
276
|
+
return;
|
|
198
277
|
}
|
|
199
|
-
|
|
200
|
-
|
|
278
|
+
const normalized = String(stderr || "").trim();
|
|
279
|
+
if (looksLikeUnsupportedFlags(normalized)) {
|
|
280
|
+
rejectCompletion(new Error("Provider CLI does not support required resume flags. Update the CLI version."));
|
|
281
|
+
return;
|
|
201
282
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
});
|
|
214
|
-
let stderr = '';
|
|
215
|
-
let spawned = false;
|
|
216
|
-
let launchSettled = false;
|
|
217
|
-
const failLaunch = (error) => {
|
|
218
|
-
if (launchSettled)
|
|
219
|
-
return;
|
|
220
|
-
launchSettled = true;
|
|
221
|
-
reject(error);
|
|
222
|
-
};
|
|
223
|
-
const completion = new Promise((resolveCompletion, rejectCompletion) => {
|
|
224
|
-
child.on('error', (error) => {
|
|
225
|
-
if (!spawned) {
|
|
226
|
-
failLaunch(error);
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
rejectCompletion(error);
|
|
230
|
-
});
|
|
231
|
-
child.on('close', (code) => {
|
|
232
|
-
if (code === 0) {
|
|
233
|
-
resolveCompletion();
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
const normalized = String(stderr || '').trim();
|
|
237
|
-
if (looksLikeUnsupportedFlags(normalized)) {
|
|
238
|
-
rejectCompletion(new Error('Provider CLI does not support required resume flags. Update the CLI version.'));
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
rejectCompletion(new Error(normalized || `Dispatch exited with code ${code}`));
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
child.stderr?.on('data', (data) => {
|
|
245
|
-
stderr += data.toString();
|
|
246
|
-
});
|
|
247
|
-
child.on('spawn', () => {
|
|
248
|
-
spawned = true;
|
|
249
|
-
if (launchSettled)
|
|
250
|
-
return;
|
|
251
|
-
launchSettled = true;
|
|
252
|
-
resolve({ completion });
|
|
253
|
-
});
|
|
283
|
+
rejectCompletion(new Error(normalized || `Dispatch exited with code ${code}`));
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
child.stderr?.on("data", (data) => {
|
|
287
|
+
stderr += data.toString();
|
|
288
|
+
});
|
|
289
|
+
child.on("spawn", () => {
|
|
290
|
+
spawned = true;
|
|
291
|
+
if (launchSettled) return;
|
|
292
|
+
launchSettled = true;
|
|
293
|
+
resolve({ completion });
|
|
254
294
|
});
|
|
295
|
+
});
|
|
255
296
|
}
|
|
256
297
|
async function runInteractiveInTerminal(provider, prompt) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
});
|
|
265
|
-
child.on('error', (error) => reject(error));
|
|
266
|
-
child.on('close', (code) => {
|
|
267
|
-
if (code === 0)
|
|
268
|
-
resolve();
|
|
269
|
-
else
|
|
270
|
-
reject(new Error(`Interactive session exited with code ${code}`));
|
|
271
|
-
});
|
|
298
|
+
const command = buildInteractiveDispatchCommand(provider, prompt);
|
|
299
|
+
const resolved = resolveDispatchCommand(command);
|
|
300
|
+
await new Promise((resolve, reject) => {
|
|
301
|
+
const child = (0, import_child_process2.spawn)(resolved.command, resolved.args, {
|
|
302
|
+
stdio: "inherit",
|
|
303
|
+
shell: resolved.shell,
|
|
304
|
+
windowsHide: false
|
|
272
305
|
});
|
|
306
|
+
child.on("error", (error) => reject(error));
|
|
307
|
+
child.on("close", (code) => {
|
|
308
|
+
if (code === 0) resolve();
|
|
309
|
+
else reject(new Error(`Interactive session exited with code ${code}`));
|
|
310
|
+
});
|
|
311
|
+
});
|
|
273
312
|
}
|
|
274
313
|
function describeTarget(target) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (sessionTitle)
|
|
285
|
-
return `${provider} session (${sessionTitle})`;
|
|
286
|
-
if (project)
|
|
287
|
-
return `${provider} session (${project})`;
|
|
288
|
-
return `${provider} session`;
|
|
314
|
+
if (target.kind === "new-codex") return "new codex session";
|
|
315
|
+
if (target.kind === "new-claude") return "new claude session";
|
|
316
|
+
const provider = describeProvider(target.agent);
|
|
317
|
+
const project = cleanTargetLabel(target.projectName, 24);
|
|
318
|
+
const sessionTitle = cleanTargetLabel(target.sessionTitle, 44);
|
|
319
|
+
if (project && sessionTitle) return `${provider} session (${project} \u203A ${sessionTitle})`;
|
|
320
|
+
if (sessionTitle) return `${provider} session (${sessionTitle})`;
|
|
321
|
+
if (project) return `${provider} session (${project})`;
|
|
322
|
+
return `${provider} session`;
|
|
289
323
|
}
|
|
290
324
|
function describeProvider(agent) {
|
|
291
|
-
|
|
325
|
+
return String(agent || "").toLowerCase() === "claude" ? "Claude" : "Codex";
|
|
292
326
|
}
|
|
293
327
|
function cleanTargetLabel(value, max) {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return text;
|
|
301
|
-
if (max <= 3)
|
|
302
|
-
return text.slice(0, max);
|
|
303
|
-
return `${text.slice(0, max - 3).trimEnd()}...`;
|
|
328
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
329
|
+
if (!text) return "";
|
|
330
|
+
if (/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(text)) return "";
|
|
331
|
+
if (text.length <= max) return text;
|
|
332
|
+
if (max <= 3) return text.slice(0, max);
|
|
333
|
+
return `${text.slice(0, max - 3).trimEnd()}...`;
|
|
304
334
|
}
|
|
305
335
|
function looksLikeUnsupportedFlags(stderr) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
return normalized.includes('unknown option')
|
|
310
|
-
|| normalized.includes('unknown flag')
|
|
311
|
-
|| normalized.includes('unrecognized option')
|
|
312
|
-
|| normalized.includes('did you mean');
|
|
336
|
+
const normalized = String(stderr || "").toLowerCase();
|
|
337
|
+
if (!normalized) return false;
|
|
338
|
+
return normalized.includes("unknown option") || normalized.includes("unknown flag") || normalized.includes("unrecognized option") || normalized.includes("did you mean");
|
|
313
339
|
}
|
|
314
340
|
function normalizeError(error) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
return String(error || 'Dispatch failed');
|
|
341
|
+
if (error instanceof Error && error.message) return error.message;
|
|
342
|
+
return String(error || "Dispatch failed");
|
|
318
343
|
}
|
|
319
344
|
function captureSessionFileSnapshot(filePath) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
345
|
+
try {
|
|
346
|
+
const stat = fs2.statSync(filePath);
|
|
347
|
+
return {
|
|
348
|
+
exists: true,
|
|
349
|
+
size: stat.size,
|
|
350
|
+
mtimeMs: stat.mtimeMs
|
|
351
|
+
};
|
|
352
|
+
} catch {
|
|
353
|
+
return {
|
|
354
|
+
exists: false,
|
|
355
|
+
size: 0,
|
|
356
|
+
mtimeMs: 0
|
|
357
|
+
};
|
|
358
|
+
}
|
|
335
359
|
}
|
|
336
360
|
function verifyExistingDispatchDelivery(filePath, prompt, before) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
361
|
+
if (!hasSessionFileChanged(filePath, before)) {
|
|
362
|
+
throw new Error("Target session did not update after dispatch");
|
|
363
|
+
}
|
|
364
|
+
const promptSignature = firstPromptSignature(prompt);
|
|
365
|
+
if (promptSignature.length < 4) return;
|
|
366
|
+
const tail = readSessionTailSinceOffset(filePath, before.size);
|
|
367
|
+
if (!tail) {
|
|
368
|
+
throw new Error("Target session updated but prompt text was not found");
|
|
369
|
+
}
|
|
370
|
+
if (!tail.toLowerCase().includes(promptSignature.toLowerCase())) {
|
|
371
|
+
throw new Error("Prompt text was not found in target session update");
|
|
372
|
+
}
|
|
350
373
|
}
|
|
351
374
|
function hasSessionUpdateWithPrompt(filePath, prompt, before) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (!tail)
|
|
359
|
-
return false;
|
|
360
|
-
return tail.toLowerCase().includes(promptSignature.toLowerCase());
|
|
375
|
+
if (!hasSessionFileChanged(filePath, before)) return false;
|
|
376
|
+
const promptSignature = firstPromptSignature(prompt);
|
|
377
|
+
if (promptSignature.length < 4) return true;
|
|
378
|
+
const tail = readSessionTailSinceOffset(filePath, before.size);
|
|
379
|
+
if (!tail) return false;
|
|
380
|
+
return tail.toLowerCase().includes(promptSignature.toLowerCase());
|
|
361
381
|
}
|
|
362
382
|
function hasSessionFileChanged(filePath, before) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
383
|
+
const after = captureSessionFileSnapshot(filePath);
|
|
384
|
+
if (!after.exists) {
|
|
385
|
+
throw new Error("Target session file is not accessible after dispatch");
|
|
386
|
+
}
|
|
387
|
+
return after.size > before.size || after.mtimeMs > before.mtimeMs;
|
|
368
388
|
}
|
|
369
389
|
function firstPromptSignature(prompt) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
.filter(Boolean);
|
|
374
|
-
const first = lines[0] || String(prompt || '').trim();
|
|
375
|
-
return first.slice(0, 160);
|
|
390
|
+
const lines = String(prompt || "").split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
|
|
391
|
+
const first = lines[0] || String(prompt || "").trim();
|
|
392
|
+
return first.slice(0, 160);
|
|
376
393
|
}
|
|
377
394
|
function readSessionTailSinceOffset(filePath, previousSize) {
|
|
395
|
+
try {
|
|
396
|
+
const stat = fs2.statSync(filePath);
|
|
397
|
+
const maxBytes = 1024 * 512;
|
|
398
|
+
const start = Math.max(0, previousSize - 2048);
|
|
399
|
+
const desiredStart = stat.size - start > maxBytes ? Math.max(0, stat.size - maxBytes) : start;
|
|
400
|
+
const length = Math.max(0, stat.size - desiredStart);
|
|
401
|
+
if (length === 0) return "";
|
|
402
|
+
const fd = fs2.openSync(filePath, "r");
|
|
378
403
|
try {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
: start;
|
|
385
|
-
const length = Math.max(0, stat.size - desiredStart);
|
|
386
|
-
if (length === 0)
|
|
387
|
-
return '';
|
|
388
|
-
const fd = fs.openSync(filePath, 'r');
|
|
389
|
-
try {
|
|
390
|
-
const buf = Buffer.alloc(length);
|
|
391
|
-
fs.readSync(fd, buf, 0, length, desiredStart);
|
|
392
|
-
return buf.toString('utf8');
|
|
393
|
-
}
|
|
394
|
-
finally {
|
|
395
|
-
fs.closeSync(fd);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
catch {
|
|
399
|
-
return '';
|
|
404
|
+
const buf = Buffer.alloc(length);
|
|
405
|
+
fs2.readSync(fd, buf, 0, length, desiredStart);
|
|
406
|
+
return buf.toString("utf8");
|
|
407
|
+
} finally {
|
|
408
|
+
fs2.closeSync(fd);
|
|
400
409
|
}
|
|
410
|
+
} catch {
|
|
411
|
+
return "";
|
|
412
|
+
}
|
|
401
413
|
}
|
|
402
414
|
async function runWithTimeout(promise, timeoutMs, message) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
});
|
|
415
|
+
return await new Promise((resolve, reject) => {
|
|
416
|
+
const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
417
|
+
promise.then((value) => {
|
|
418
|
+
clearTimeout(timer);
|
|
419
|
+
resolve(value);
|
|
420
|
+
}).catch((error) => {
|
|
421
|
+
clearTimeout(timer);
|
|
422
|
+
reject(error);
|
|
412
423
|
});
|
|
424
|
+
});
|
|
413
425
|
}
|
|
414
426
|
async function sleepMs(ms) {
|
|
415
|
-
|
|
427
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
416
428
|
}
|
|
417
429
|
function deriveDispatchCwdForSession(session) {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
return decodeClaudeProjectPathFromSessionFile(session.filePath);
|
|
430
|
+
if (session.agent !== "claude") return void 0;
|
|
431
|
+
return decodeClaudeProjectPathFromSessionFile(session.filePath);
|
|
421
432
|
}
|
|
422
433
|
function decodeClaudeProjectPathFromSessionFile(filePath) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
const unixPath = `/${parts.join('/')}`;
|
|
437
|
-
if (fs.existsSync(unixPath))
|
|
438
|
-
return unixPath;
|
|
439
|
-
return undefined;
|
|
434
|
+
if (!filePath) return void 0;
|
|
435
|
+
const projectDir = path2.basename(path2.dirname(filePath));
|
|
436
|
+
if (!projectDir) return void 0;
|
|
437
|
+
const parts = projectDir.split("-").filter(Boolean);
|
|
438
|
+
if (parts.length === 0) return void 0;
|
|
439
|
+
if (parts.length >= 2 && /^[A-Za-z]$/.test(parts[0])) {
|
|
440
|
+
const windowsPath = `${parts[0]}:\\${parts.slice(1).join("\\")}`;
|
|
441
|
+
if (fs2.existsSync(windowsPath)) return windowsPath;
|
|
442
|
+
}
|
|
443
|
+
const unixPath = `/${parts.join("/")}`;
|
|
444
|
+
if (fs2.existsSync(unixPath)) return unixPath;
|
|
445
|
+
return void 0;
|
|
440
446
|
}
|
|
441
|
-
|
|
447
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
448
|
+
0 && (module.exports = {
|
|
449
|
+
SessionDispatchService,
|
|
450
|
+
buildExistingDispatchCommand,
|
|
451
|
+
buildInteractiveDispatchCommand,
|
|
452
|
+
resolveDispatchCommand
|
|
453
|
+
});
|