@sma1lboy/kobe 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/kobed.js +4805 -0
- package/dist/{index.js → cli/index.js} +10881 -9121
- package/package.json +3 -2
|
@@ -0,0 +1,4805 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __returnValue = (v) => v;
|
|
5
|
+
function __exportSetter(name, newValue) {
|
|
6
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
7
|
+
}
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, {
|
|
11
|
+
get: all[name],
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
set: __exportSetter.bind(all, name)
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
|
+
var __require = import.meta.require;
|
|
19
|
+
|
|
20
|
+
// src/daemon/protocol.ts
|
|
21
|
+
function serializeTask(task) {
|
|
22
|
+
return {
|
|
23
|
+
id: task.id,
|
|
24
|
+
title: task.title,
|
|
25
|
+
repo: task.repo,
|
|
26
|
+
branch: task.branch,
|
|
27
|
+
worktreePath: task.worktreePath,
|
|
28
|
+
kind: task.kind ?? "task",
|
|
29
|
+
sessionId: task.sessionId,
|
|
30
|
+
tabs: task.tabs,
|
|
31
|
+
activeTabId: task.activeTabId,
|
|
32
|
+
status: task.status,
|
|
33
|
+
archived: task.archived,
|
|
34
|
+
pinned: task.pinned ?? false,
|
|
35
|
+
permissionMode: task.permissionMode,
|
|
36
|
+
model: task.model,
|
|
37
|
+
createdAt: task.createdAt,
|
|
38
|
+
updatedAt: task.updatedAt
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function serializeMessages(messages) {
|
|
42
|
+
return messages.map((m) => ({ ...m }));
|
|
43
|
+
}
|
|
44
|
+
function normalizeEventForWire(taskId, tabId, ev) {
|
|
45
|
+
if (ev.type === "assistant.delta") {
|
|
46
|
+
return {
|
|
47
|
+
type: "event",
|
|
48
|
+
name: "chat.delta",
|
|
49
|
+
payload: { taskId, tabId, delta: ev.text }
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (ev.type === "done") {
|
|
53
|
+
return {
|
|
54
|
+
type: "event",
|
|
55
|
+
name: "chat.complete",
|
|
56
|
+
payload: { taskId, tabId }
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (ev.type === "error") {
|
|
60
|
+
return {
|
|
61
|
+
type: "event",
|
|
62
|
+
name: "engine.status",
|
|
63
|
+
payload: { taskId, tabId, status: "error", message: ev.message }
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
type: "event",
|
|
68
|
+
name: "chat.event",
|
|
69
|
+
payload: { taskId, tabId, event: ev }
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function frameToLine(frame) {
|
|
73
|
+
return `${JSON.stringify(frame)}
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
var DAEMON_PROTOCOL_VERSION = 1;
|
|
77
|
+
|
|
78
|
+
// src/client/index.ts
|
|
79
|
+
import { connect } from "net";
|
|
80
|
+
|
|
81
|
+
class KobeDaemonClient {
|
|
82
|
+
socketPath;
|
|
83
|
+
socket = null;
|
|
84
|
+
buffer = "";
|
|
85
|
+
nextId = 1;
|
|
86
|
+
pending = new Map;
|
|
87
|
+
handlers = new Map;
|
|
88
|
+
constructor(socketPath) {
|
|
89
|
+
this.socketPath = socketPath;
|
|
90
|
+
}
|
|
91
|
+
connect() {
|
|
92
|
+
if (this.socket)
|
|
93
|
+
return Promise.resolve();
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const socket = connect(this.socketPath);
|
|
96
|
+
this.socket = socket;
|
|
97
|
+
socket.once("connect", resolve);
|
|
98
|
+
socket.once("error", reject);
|
|
99
|
+
socket.on("data", (chunk) => this.onData(chunk.toString("utf8")));
|
|
100
|
+
socket.on("close", () => {
|
|
101
|
+
this.socket = null;
|
|
102
|
+
for (const pending of this.pending.values())
|
|
103
|
+
pending.reject(new Error("daemon connection closed"));
|
|
104
|
+
this.pending.clear();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
close() {
|
|
109
|
+
this.socket?.end();
|
|
110
|
+
this.socket = null;
|
|
111
|
+
}
|
|
112
|
+
on(name, handler) {
|
|
113
|
+
let set = this.handlers.get(name);
|
|
114
|
+
if (!set) {
|
|
115
|
+
set = new Set;
|
|
116
|
+
this.handlers.set(name, set);
|
|
117
|
+
}
|
|
118
|
+
set.add(handler);
|
|
119
|
+
return () => {
|
|
120
|
+
set?.delete(handler);
|
|
121
|
+
if (set?.size === 0)
|
|
122
|
+
this.handlers.delete(name);
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async request(name, payload) {
|
|
126
|
+
await this.connect();
|
|
127
|
+
const socket = this.socket;
|
|
128
|
+
if (!socket)
|
|
129
|
+
throw new Error("daemon connection is not open");
|
|
130
|
+
const id = String(this.nextId++);
|
|
131
|
+
const promise = new Promise((resolve, reject) => {
|
|
132
|
+
this.pending.set(id, { resolve: (value) => resolve(value), reject });
|
|
133
|
+
});
|
|
134
|
+
socket.write(frameToLine({ type: "request", id, name, payload }));
|
|
135
|
+
return promise;
|
|
136
|
+
}
|
|
137
|
+
onData(chunk) {
|
|
138
|
+
this.buffer += chunk;
|
|
139
|
+
let nl = this.buffer.indexOf(`
|
|
140
|
+
`);
|
|
141
|
+
while (nl !== -1) {
|
|
142
|
+
const line = this.buffer.slice(0, nl);
|
|
143
|
+
this.buffer = this.buffer.slice(nl + 1);
|
|
144
|
+
if (line.trim().length > 0)
|
|
145
|
+
this.onLine(line);
|
|
146
|
+
nl = this.buffer.indexOf(`
|
|
147
|
+
`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
onLine(line) {
|
|
151
|
+
const frame = JSON.parse(line);
|
|
152
|
+
if (frame.type === "event") {
|
|
153
|
+
this.emit(frame);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (frame.type !== "response")
|
|
157
|
+
return;
|
|
158
|
+
const pending = this.pending.get(frame.id);
|
|
159
|
+
if (!pending)
|
|
160
|
+
return;
|
|
161
|
+
this.pending.delete(frame.id);
|
|
162
|
+
if (frame.error)
|
|
163
|
+
pending.reject(new Error(frame.error.message));
|
|
164
|
+
else
|
|
165
|
+
pending.resolve(frame.payload);
|
|
166
|
+
}
|
|
167
|
+
emit(frame) {
|
|
168
|
+
for (const handler of this.handlers.get(frame.name) ?? [])
|
|
169
|
+
handler(frame);
|
|
170
|
+
for (const handler of this.handlers.get("*") ?? [])
|
|
171
|
+
handler(frame);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
var init_client = () => {};
|
|
175
|
+
|
|
176
|
+
// src/engine/claude-code-local/binary.ts
|
|
177
|
+
import { spawnSync } from "child_process";
|
|
178
|
+
import { existsSync, statSync } from "fs";
|
|
179
|
+
import { homedir } from "os";
|
|
180
|
+
import path from "path";
|
|
181
|
+
async function findClaudeBinary(deps = defaultDeps) {
|
|
182
|
+
const checked = [];
|
|
183
|
+
const tryPath = (p) => {
|
|
184
|
+
if (!p)
|
|
185
|
+
return;
|
|
186
|
+
checked.push(p);
|
|
187
|
+
return deps.fileExists(p) ? p : undefined;
|
|
188
|
+
};
|
|
189
|
+
const whichResult = deps.which("claude");
|
|
190
|
+
if (whichResult) {
|
|
191
|
+
checked.push(`which:${whichResult}`);
|
|
192
|
+
if (deps.fileExists(whichResult))
|
|
193
|
+
return whichResult;
|
|
194
|
+
}
|
|
195
|
+
const home = deps.home();
|
|
196
|
+
const localInstall = tryPath(path.join(home, ".claude", "local", "claude"));
|
|
197
|
+
if (localInstall)
|
|
198
|
+
return localInstall;
|
|
199
|
+
const nvmBin = deps.env("NVM_BIN");
|
|
200
|
+
if (nvmBin) {
|
|
201
|
+
const candidate = tryPath(path.join(nvmBin, "claude"));
|
|
202
|
+
if (candidate)
|
|
203
|
+
return candidate;
|
|
204
|
+
}
|
|
205
|
+
const nvmRoot = path.join(home, ".nvm", "versions", "node");
|
|
206
|
+
const nvmVersions = deps.readdir(nvmRoot).sort().reverse();
|
|
207
|
+
for (const v of nvmVersions) {
|
|
208
|
+
const candidate = tryPath(path.join(nvmRoot, v, "bin", "claude"));
|
|
209
|
+
if (candidate)
|
|
210
|
+
return candidate;
|
|
211
|
+
}
|
|
212
|
+
for (const p of ["/opt/homebrew/bin/claude", "/usr/local/bin/claude", "/usr/bin/claude", "/bin/claude"]) {
|
|
213
|
+
const candidate = tryPath(p);
|
|
214
|
+
if (candidate)
|
|
215
|
+
return candidate;
|
|
216
|
+
}
|
|
217
|
+
for (const rel of [
|
|
218
|
+
".local/bin/claude",
|
|
219
|
+
".npm-global/bin/claude",
|
|
220
|
+
".yarn/bin/claude",
|
|
221
|
+
".bun/bin/claude",
|
|
222
|
+
"bin/claude"
|
|
223
|
+
]) {
|
|
224
|
+
const candidate = tryPath(path.join(home, rel));
|
|
225
|
+
if (candidate)
|
|
226
|
+
return candidate;
|
|
227
|
+
}
|
|
228
|
+
throw new ClaudeBinaryNotFoundError(checked);
|
|
229
|
+
}
|
|
230
|
+
var ClaudeBinaryNotFoundError, defaultDeps;
|
|
231
|
+
var init_binary = __esm(() => {
|
|
232
|
+
ClaudeBinaryNotFoundError = class ClaudeBinaryNotFoundError extends Error {
|
|
233
|
+
checkedPaths;
|
|
234
|
+
constructor(checkedPaths) {
|
|
235
|
+
super(`Claude Code binary not found. Checked: ${checkedPaths.join(", ")}. Ensure 'claude' is on PATH, or install at ~/.claude/local/claude.`);
|
|
236
|
+
this.name = "ClaudeBinaryNotFoundError";
|
|
237
|
+
this.checkedPaths = checkedPaths;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
defaultDeps = {
|
|
241
|
+
fileExists(p) {
|
|
242
|
+
try {
|
|
243
|
+
return statSync(p).isFile();
|
|
244
|
+
} catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
env(name) {
|
|
249
|
+
return process.env[name];
|
|
250
|
+
},
|
|
251
|
+
home() {
|
|
252
|
+
return homedir();
|
|
253
|
+
},
|
|
254
|
+
which(name) {
|
|
255
|
+
const cmd = process.platform === "win32" ? "where" : "which";
|
|
256
|
+
const out = spawnSync(cmd, [name], { encoding: "utf8" });
|
|
257
|
+
if (out.status !== 0)
|
|
258
|
+
return;
|
|
259
|
+
const first = out.stdout.split(`
|
|
260
|
+
`).map((l) => l.trim()).filter(Boolean)[0];
|
|
261
|
+
if (!first)
|
|
262
|
+
return;
|
|
263
|
+
if (first.startsWith("claude:") && first.includes("aliased to")) {
|
|
264
|
+
const aliasTarget = first.split("aliased to")[1]?.trim();
|
|
265
|
+
return aliasTarget && existsSync(aliasTarget) ? aliasTarget : undefined;
|
|
266
|
+
}
|
|
267
|
+
return first;
|
|
268
|
+
},
|
|
269
|
+
readdir(p) {
|
|
270
|
+
try {
|
|
271
|
+
const fs = __require("fs");
|
|
272
|
+
return fs.readdirSync(p);
|
|
273
|
+
} catch {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// src/engine/claude-code-local/history.ts
|
|
281
|
+
import { readFile, readdir, unlink } from "fs/promises";
|
|
282
|
+
import { homedir as homedir2 } from "os";
|
|
283
|
+
import path2 from "path";
|
|
284
|
+
function encodeCwd(cwd) {
|
|
285
|
+
return cwd.replace(/[/.]/g, "-");
|
|
286
|
+
}
|
|
287
|
+
async function readHistory(sessionId, deps = defaultDeps2) {
|
|
288
|
+
const root = deps.projectsDir();
|
|
289
|
+
const projectDirs = await deps.readdir(root);
|
|
290
|
+
for (const dir of projectDirs) {
|
|
291
|
+
const candidate = path2.join(root, dir, `${sessionId}.jsonl`);
|
|
292
|
+
let raw;
|
|
293
|
+
try {
|
|
294
|
+
raw = await deps.readFile(candidate);
|
|
295
|
+
} catch {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
return sortByTimestamp(parseJsonl(raw, sessionId));
|
|
299
|
+
}
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
async function deleteHistory(sessionId, deps = defaultDeps2) {
|
|
303
|
+
const root = deps.projectsDir();
|
|
304
|
+
const projectDirs = await deps.readdir(root);
|
|
305
|
+
for (const dir of projectDirs) {
|
|
306
|
+
const candidate = path2.join(root, dir, `${sessionId}.jsonl`);
|
|
307
|
+
try {
|
|
308
|
+
await unlink(candidate);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (err.code === "ENOENT")
|
|
311
|
+
continue;
|
|
312
|
+
throw err;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function sortByTimestamp(messages) {
|
|
317
|
+
return messages.map((msg, idx) => ({ msg, idx })).sort((a, b) => {
|
|
318
|
+
if (a.msg.timestamp < b.msg.timestamp)
|
|
319
|
+
return -1;
|
|
320
|
+
if (a.msg.timestamp > b.msg.timestamp)
|
|
321
|
+
return 1;
|
|
322
|
+
return a.idx - b.idx;
|
|
323
|
+
}).map((entry) => entry.msg);
|
|
324
|
+
}
|
|
325
|
+
function parseJsonl(raw, sessionId) {
|
|
326
|
+
const out = [];
|
|
327
|
+
for (const line of raw.split(`
|
|
328
|
+
`)) {
|
|
329
|
+
const trimmed = line.trim();
|
|
330
|
+
if (!trimmed)
|
|
331
|
+
continue;
|
|
332
|
+
let parsed;
|
|
333
|
+
try {
|
|
334
|
+
parsed = JSON.parse(trimmed);
|
|
335
|
+
} catch {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (!isObject(parsed))
|
|
339
|
+
continue;
|
|
340
|
+
const msg = extractMessage(parsed, sessionId);
|
|
341
|
+
if (msg)
|
|
342
|
+
out.push(msg);
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
function extractMessage(record, fallbackSessionId) {
|
|
347
|
+
const inner = isObject(record.message) ? record.message : record;
|
|
348
|
+
const role = inner.role;
|
|
349
|
+
if (role !== "user" && role !== "assistant" && role !== "system")
|
|
350
|
+
return null;
|
|
351
|
+
if (!("content" in inner))
|
|
352
|
+
return null;
|
|
353
|
+
const content = inner.content;
|
|
354
|
+
const ts = typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString();
|
|
355
|
+
const sid = typeof record.sessionId === "string" ? record.sessionId : fallbackSessionId;
|
|
356
|
+
return { role, content, timestamp: ts, sessionId: sid };
|
|
357
|
+
}
|
|
358
|
+
function isObject(v) {
|
|
359
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
360
|
+
}
|
|
361
|
+
var defaultDeps2;
|
|
362
|
+
var init_history = __esm(() => {
|
|
363
|
+
defaultDeps2 = {
|
|
364
|
+
projectsDir() {
|
|
365
|
+
return path2.join(homedir2(), ".claude", "projects");
|
|
366
|
+
},
|
|
367
|
+
async readdir(p) {
|
|
368
|
+
try {
|
|
369
|
+
return await readdir(p);
|
|
370
|
+
} catch {
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
async readFile(p) {
|
|
375
|
+
return await readFile(p, "utf8");
|
|
376
|
+
},
|
|
377
|
+
async pathExists(p) {
|
|
378
|
+
try {
|
|
379
|
+
await readdir(p);
|
|
380
|
+
return true;
|
|
381
|
+
} catch {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// src/engine/claude-code-local/registry.ts
|
|
389
|
+
class SessionRegistry {
|
|
390
|
+
handles = new Map;
|
|
391
|
+
register(handle) {
|
|
392
|
+
const existing = this.handles.get(handle.sessionId);
|
|
393
|
+
if (existing) {
|
|
394
|
+
const stale = existing.proc.exitCode !== null || existing.proc.signalCode !== null;
|
|
395
|
+
if (!stale) {
|
|
396
|
+
throw new Error(`SessionRegistry: duplicate sessionId ${handle.sessionId}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
this.handles.set(handle.sessionId, handle);
|
|
400
|
+
}
|
|
401
|
+
unregister(sessionId) {
|
|
402
|
+
this.handles.delete(sessionId);
|
|
403
|
+
}
|
|
404
|
+
get(sessionId) {
|
|
405
|
+
return this.handles.get(sessionId);
|
|
406
|
+
}
|
|
407
|
+
async kill(sessionId, graceMs = 5000) {
|
|
408
|
+
const handle = this.handles.get(sessionId);
|
|
409
|
+
if (!handle)
|
|
410
|
+
return;
|
|
411
|
+
const proc = handle.proc;
|
|
412
|
+
if (proc.exitCode !== null || proc.signalCode !== null) {
|
|
413
|
+
this.unregister(sessionId);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const exited = waitForExit(proc);
|
|
417
|
+
try {
|
|
418
|
+
proc.kill("SIGTERM");
|
|
419
|
+
} catch {}
|
|
420
|
+
const winner = await Promise.race([
|
|
421
|
+
exited.then(() => "exit"),
|
|
422
|
+
delay(graceMs).then(() => "timeout")
|
|
423
|
+
]);
|
|
424
|
+
if (winner === "timeout") {
|
|
425
|
+
try {
|
|
426
|
+
proc.kill("SIGKILL");
|
|
427
|
+
} catch {}
|
|
428
|
+
await Promise.race([exited, delay(1000)]);
|
|
429
|
+
}
|
|
430
|
+
this.unregister(sessionId);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function waitForExit(proc) {
|
|
434
|
+
return new Promise((resolve) => {
|
|
435
|
+
if (proc.exitCode !== null || proc.signalCode !== null) {
|
|
436
|
+
resolve();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
proc.once("close", () => resolve());
|
|
440
|
+
proc.once("exit", () => resolve());
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
function delay(ms) {
|
|
444
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/engine/claude-code-local/sessions.ts
|
|
448
|
+
import { readFile as readFile2, readdir as readdir2, stat } from "fs/promises";
|
|
449
|
+
import { homedir as homedir3 } from "os";
|
|
450
|
+
import path3 from "path";
|
|
451
|
+
async function listSessionsForCwd(cwd, deps = defaultDeps3) {
|
|
452
|
+
const projectDir = path3.join(deps.projectsDir(), encodeCwd(cwd));
|
|
453
|
+
const entries = await deps.readdir(projectDir);
|
|
454
|
+
const jsonlNames = entries.filter((n) => n.endsWith(".jsonl"));
|
|
455
|
+
const out = [];
|
|
456
|
+
for (const name of jsonlNames) {
|
|
457
|
+
const sessionId = name.slice(0, -".jsonl".length);
|
|
458
|
+
const filePath = path3.join(projectDir, name);
|
|
459
|
+
try {
|
|
460
|
+
const [meta, raw] = await Promise.all([deps.stat(filePath), deps.readFile(filePath)]);
|
|
461
|
+
const lines = raw.split(`
|
|
462
|
+
`).filter((l) => l.trim().length > 0);
|
|
463
|
+
out.push({
|
|
464
|
+
sessionId,
|
|
465
|
+
mtimeMs: meta.mtimeMs,
|
|
466
|
+
firstUserMessage: extractFirstUserMessage(lines),
|
|
467
|
+
messageCount: lines.length
|
|
468
|
+
});
|
|
469
|
+
} catch {
|
|
470
|
+
out.push({ sessionId, mtimeMs: 0, firstUserMessage: null, messageCount: 0 });
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
out.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
474
|
+
return out;
|
|
475
|
+
}
|
|
476
|
+
function extractFirstUserMessage(lines) {
|
|
477
|
+
for (const line of lines) {
|
|
478
|
+
let parsed;
|
|
479
|
+
try {
|
|
480
|
+
parsed = JSON.parse(line);
|
|
481
|
+
} catch {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (!isObject2(parsed))
|
|
485
|
+
continue;
|
|
486
|
+
const inner = isObject2(parsed.message) ? parsed.message : parsed;
|
|
487
|
+
if (inner.role !== "user")
|
|
488
|
+
continue;
|
|
489
|
+
const text = stringifyContent(inner.content);
|
|
490
|
+
if (!text)
|
|
491
|
+
continue;
|
|
492
|
+
if (text.startsWith("Caveat:"))
|
|
493
|
+
continue;
|
|
494
|
+
if (text.startsWith("<command-name>") || text.startsWith("<local-command-stdout>"))
|
|
495
|
+
continue;
|
|
496
|
+
return text.length > PREVIEW_MAX_CHARS ? `${text.slice(0, PREVIEW_MAX_CHARS - 1).trimEnd()}\u2026` : text;
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
function stringifyContent(content) {
|
|
501
|
+
if (typeof content === "string")
|
|
502
|
+
return content.trim();
|
|
503
|
+
if (!Array.isArray(content))
|
|
504
|
+
return "";
|
|
505
|
+
const parts = [];
|
|
506
|
+
for (const block of content) {
|
|
507
|
+
if (!isObject2(block))
|
|
508
|
+
continue;
|
|
509
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
510
|
+
parts.push(block.text);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return parts.join(" ").trim();
|
|
514
|
+
}
|
|
515
|
+
function isObject2(v) {
|
|
516
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
517
|
+
}
|
|
518
|
+
var defaultDeps3, PREVIEW_MAX_CHARS = 200;
|
|
519
|
+
var init_sessions = __esm(() => {
|
|
520
|
+
init_history();
|
|
521
|
+
defaultDeps3 = {
|
|
522
|
+
projectsDir() {
|
|
523
|
+
return path3.join(homedir3(), ".claude", "projects");
|
|
524
|
+
},
|
|
525
|
+
async readdir(p) {
|
|
526
|
+
try {
|
|
527
|
+
return await readdir2(p);
|
|
528
|
+
} catch {
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
async readFile(p) {
|
|
533
|
+
return await readFile2(p, "utf8");
|
|
534
|
+
},
|
|
535
|
+
async stat(p) {
|
|
536
|
+
const s = await stat(p);
|
|
537
|
+
return { mtimeMs: s.mtimeMs };
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// src/engine/claude-code-local/spawn.ts
|
|
543
|
+
import { spawn } from "child_process";
|
|
544
|
+
function spawnClaudeProcess(opts) {
|
|
545
|
+
const args = buildArgs(opts);
|
|
546
|
+
const proc = spawn(opts.binaryPath, args, {
|
|
547
|
+
cwd: opts.cwd,
|
|
548
|
+
env: { ...process.env, ...opts.env ?? {} },
|
|
549
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
550
|
+
});
|
|
551
|
+
return {
|
|
552
|
+
proc,
|
|
553
|
+
stdout: proc.stdout,
|
|
554
|
+
stderr: proc.stderr,
|
|
555
|
+
args
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function buildArgs(opts) {
|
|
559
|
+
const args = [];
|
|
560
|
+
if (opts.resumeSessionId) {
|
|
561
|
+
args.push("--resume", opts.resumeSessionId);
|
|
562
|
+
}
|
|
563
|
+
args.push("-p", opts.prompt);
|
|
564
|
+
if (opts.model) {
|
|
565
|
+
args.push("--model", opts.model);
|
|
566
|
+
}
|
|
567
|
+
if (opts.permissionMode) {
|
|
568
|
+
args.push("--permission-mode", opts.permissionMode);
|
|
569
|
+
}
|
|
570
|
+
args.push("--output-format", "stream-json", "--verbose");
|
|
571
|
+
const mcpConfig = process.env.KOBE_MCP_CONFIG;
|
|
572
|
+
if (mcpConfig && mcpConfig.length > 0) {
|
|
573
|
+
args.push("--mcp-config", mcpConfig);
|
|
574
|
+
}
|
|
575
|
+
if (opts.extraArgs && opts.extraArgs.length > 0) {
|
|
576
|
+
args.push(...opts.extraArgs);
|
|
577
|
+
}
|
|
578
|
+
return args;
|
|
579
|
+
}
|
|
580
|
+
var init_spawn = () => {};
|
|
581
|
+
|
|
582
|
+
// src/engine/claude-code-local/stream.ts
|
|
583
|
+
async function* parseStreamJson(lines, opts = {}) {
|
|
584
|
+
let sessionIdEmitted = false;
|
|
585
|
+
const toolNameById = new Map;
|
|
586
|
+
for await (const rawLine of lines) {
|
|
587
|
+
const line = rawLine.trim();
|
|
588
|
+
if (!line)
|
|
589
|
+
continue;
|
|
590
|
+
let msg;
|
|
591
|
+
try {
|
|
592
|
+
msg = JSON.parse(line);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
yield { type: "error", message: `stream-json parse failed: ${stringifyErr(err)}` };
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (!isObject3(msg))
|
|
598
|
+
continue;
|
|
599
|
+
const type = typeof msg.type === "string" ? msg.type : undefined;
|
|
600
|
+
if (!type)
|
|
601
|
+
continue;
|
|
602
|
+
if ("parent_tool_use_id" in msg && msg.parent_tool_use_id != null)
|
|
603
|
+
continue;
|
|
604
|
+
if (type === "system") {
|
|
605
|
+
const subtype = typeof msg.subtype === "string" ? msg.subtype : undefined;
|
|
606
|
+
if (subtype === "init" && !sessionIdEmitted) {
|
|
607
|
+
const sid = typeof msg.session_id === "string" ? msg.session_id : undefined;
|
|
608
|
+
if (sid) {
|
|
609
|
+
sessionIdEmitted = true;
|
|
610
|
+
opts.onSessionId?.(sid);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
if (type === "assistant") {
|
|
616
|
+
const content = extractContentBlocks(msg);
|
|
617
|
+
for (const block of content) {
|
|
618
|
+
if (!isObject3(block))
|
|
619
|
+
continue;
|
|
620
|
+
const blockType = typeof block.type === "string" ? block.type : undefined;
|
|
621
|
+
if (blockType === "text") {
|
|
622
|
+
const text = typeof block.text === "string" ? block.text : "";
|
|
623
|
+
if (text)
|
|
624
|
+
yield { type: "assistant.delta", text };
|
|
625
|
+
} else if (blockType === "tool_use") {
|
|
626
|
+
const name = typeof block.name === "string" ? block.name : "tool";
|
|
627
|
+
const id = typeof block.id === "string" ? block.id : undefined;
|
|
628
|
+
if (id)
|
|
629
|
+
toolNameById.set(id, name);
|
|
630
|
+
const input = "input" in block ? block.input : undefined;
|
|
631
|
+
yield { type: "tool.start", name, input };
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (type === "user") {
|
|
637
|
+
const content = extractContentBlocks(msg);
|
|
638
|
+
for (const block of content) {
|
|
639
|
+
if (!isObject3(block))
|
|
640
|
+
continue;
|
|
641
|
+
const blockType = typeof block.type === "string" ? block.type : undefined;
|
|
642
|
+
if (blockType === "tool_result") {
|
|
643
|
+
const id = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
|
|
644
|
+
const name = id && toolNameById.get(id) || "tool";
|
|
645
|
+
const output = "content" in block ? block.content : undefined;
|
|
646
|
+
yield { type: "tool.result", name, output };
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (type === "result") {
|
|
652
|
+
const usage = isObject3(msg.usage) ? msg.usage : undefined;
|
|
653
|
+
if (usage) {
|
|
654
|
+
const inTok = typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
|
|
655
|
+
const outTok = typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
|
|
656
|
+
const cacheRead = typeof usage.cache_read_input_tokens === "number" ? usage.cache_read_input_tokens : undefined;
|
|
657
|
+
const cacheCreate = typeof usage.cache_creation_input_tokens === "number" ? usage.cache_creation_input_tokens : undefined;
|
|
658
|
+
yield {
|
|
659
|
+
type: "usage",
|
|
660
|
+
input_tokens: inTok,
|
|
661
|
+
output_tokens: outTok,
|
|
662
|
+
...cacheRead !== undefined ? { cache_read_input_tokens: cacheRead } : {},
|
|
663
|
+
...cacheCreate !== undefined ? { cache_creation_input_tokens: cacheCreate } : {}
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
const subtype = typeof msg.subtype === "string" ? msg.subtype : "success";
|
|
667
|
+
if (subtype === "success") {
|
|
668
|
+
yield { type: "done" };
|
|
669
|
+
} else {
|
|
670
|
+
yield { type: "error", message: `claude session ended: ${subtype}` };
|
|
671
|
+
}
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
async function* readLines(stream) {
|
|
677
|
+
let buf = "";
|
|
678
|
+
for await (const chunk of stream) {
|
|
679
|
+
const text = typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
|
680
|
+
buf += text;
|
|
681
|
+
let nl = buf.indexOf(`
|
|
682
|
+
`);
|
|
683
|
+
while (nl !== -1) {
|
|
684
|
+
yield buf.slice(0, nl);
|
|
685
|
+
buf = buf.slice(nl + 1);
|
|
686
|
+
nl = buf.indexOf(`
|
|
687
|
+
`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (buf.length > 0)
|
|
691
|
+
yield buf;
|
|
692
|
+
}
|
|
693
|
+
function isObject3(v) {
|
|
694
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
695
|
+
}
|
|
696
|
+
function extractContentBlocks(msg) {
|
|
697
|
+
if (Array.isArray(msg.content))
|
|
698
|
+
return msg.content;
|
|
699
|
+
const inner = msg.message;
|
|
700
|
+
if (isObject3(inner) && Array.isArray(inner.content))
|
|
701
|
+
return inner.content;
|
|
702
|
+
return [];
|
|
703
|
+
}
|
|
704
|
+
function stringifyErr(err) {
|
|
705
|
+
if (err instanceof Error)
|
|
706
|
+
return err.message;
|
|
707
|
+
try {
|
|
708
|
+
return JSON.stringify(err);
|
|
709
|
+
} catch {
|
|
710
|
+
return String(err);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// src/engine/claude-code-local/index.ts
|
|
715
|
+
class ClaudeCodeLocal {
|
|
716
|
+
registry = new SessionRegistry;
|
|
717
|
+
running = new Map;
|
|
718
|
+
binaryPathResolver;
|
|
719
|
+
stopGraceMs;
|
|
720
|
+
constructor(opts = {}) {
|
|
721
|
+
this.binaryPathResolver = opts.binaryPathResolver ?? findClaudeBinary;
|
|
722
|
+
this.stopGraceMs = opts.stopGraceMs ?? 5000;
|
|
723
|
+
}
|
|
724
|
+
async spawn(cwd, prompt, opts) {
|
|
725
|
+
return this.start({ cwd, prompt, opts });
|
|
726
|
+
}
|
|
727
|
+
async resume(sessionId, prompt, opts) {
|
|
728
|
+
const cwd = opts?.cwd ?? opts?.env?.KOBE_RESUME_CWD ?? process.cwd();
|
|
729
|
+
return this.start({ cwd, prompt, opts, resumeSessionId: sessionId });
|
|
730
|
+
}
|
|
731
|
+
stream(handle) {
|
|
732
|
+
const sid = handle.sessionId;
|
|
733
|
+
const self = this;
|
|
734
|
+
return {
|
|
735
|
+
async* [Symbol.asyncIterator]() {
|
|
736
|
+
const session = self.running.get(sid);
|
|
737
|
+
if (!session)
|
|
738
|
+
return;
|
|
739
|
+
let idx = 0;
|
|
740
|
+
while (true) {
|
|
741
|
+
if (idx < session.queue.length) {
|
|
742
|
+
const ev = session.queue[idx++];
|
|
743
|
+
if (!ev)
|
|
744
|
+
continue;
|
|
745
|
+
yield ev;
|
|
746
|
+
if (ev.type === "done" || ev.type === "error")
|
|
747
|
+
return;
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
if (session.closed)
|
|
751
|
+
return;
|
|
752
|
+
await new Promise((resolve) => session.waiters.push(resolve));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
async readHistory(sessionId) {
|
|
758
|
+
return readHistory(sessionId);
|
|
759
|
+
}
|
|
760
|
+
async deleteHistory(sessionId) {
|
|
761
|
+
return deleteHistory(sessionId);
|
|
762
|
+
}
|
|
763
|
+
async listSessions(cwd) {
|
|
764
|
+
return listSessionsForCwd(cwd);
|
|
765
|
+
}
|
|
766
|
+
async stop(handle) {
|
|
767
|
+
const sid = handle.sessionId;
|
|
768
|
+
await this.registry.kill(sid, this.stopGraceMs);
|
|
769
|
+
const session = this.running.get(sid);
|
|
770
|
+
if (session) {
|
|
771
|
+
session.closed = true;
|
|
772
|
+
this.notify(session);
|
|
773
|
+
this.running.delete(sid);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async start(args) {
|
|
777
|
+
const binaryPath = await this.binaryPathResolver();
|
|
778
|
+
const cliPermissionMode = args.opts?.permissionMode === "plan" ? "plan" : "bypassPermissions";
|
|
779
|
+
const spawned = spawnClaudeProcess({
|
|
780
|
+
binaryPath,
|
|
781
|
+
cwd: args.cwd,
|
|
782
|
+
prompt: args.prompt,
|
|
783
|
+
model: args.opts?.model,
|
|
784
|
+
permissionMode: cliPermissionMode,
|
|
785
|
+
env: args.opts?.env,
|
|
786
|
+
resumeSessionId: args.resumeSessionId
|
|
787
|
+
});
|
|
788
|
+
let resolveHandle = () => {};
|
|
789
|
+
let rejectHandle = () => {};
|
|
790
|
+
const handlePromise = new Promise((res, rej) => {
|
|
791
|
+
resolveHandle = res;
|
|
792
|
+
rejectHandle = rej;
|
|
793
|
+
});
|
|
794
|
+
const queue = [];
|
|
795
|
+
let session;
|
|
796
|
+
let bound = false;
|
|
797
|
+
const bind = (sessionId) => {
|
|
798
|
+
if (bound)
|
|
799
|
+
return;
|
|
800
|
+
bound = true;
|
|
801
|
+
session = {
|
|
802
|
+
sessionId,
|
|
803
|
+
cwd: args.cwd,
|
|
804
|
+
spawned,
|
|
805
|
+
queue,
|
|
806
|
+
waiters: [],
|
|
807
|
+
closed: false
|
|
808
|
+
};
|
|
809
|
+
this.running.set(sessionId, session);
|
|
810
|
+
this.registry.register({
|
|
811
|
+
sessionId,
|
|
812
|
+
cwd: args.cwd,
|
|
813
|
+
proc: spawned.proc,
|
|
814
|
+
startedAt: Date.now()
|
|
815
|
+
});
|
|
816
|
+
resolveHandle({ sessionId, cwd: args.cwd });
|
|
817
|
+
};
|
|
818
|
+
if (args.resumeSessionId) {
|
|
819
|
+
try {
|
|
820
|
+
bind(args.resumeSessionId);
|
|
821
|
+
} catch (err) {
|
|
822
|
+
try {
|
|
823
|
+
spawned.proc.kill("SIGKILL");
|
|
824
|
+
} catch {}
|
|
825
|
+
rejectHandle(err);
|
|
826
|
+
throw err;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
(async () => {
|
|
830
|
+
const events = parseStreamJson(readLines(spawned.stdout), {
|
|
831
|
+
onSessionId: (sid) => bind(sid)
|
|
832
|
+
});
|
|
833
|
+
try {
|
|
834
|
+
for await (const ev of events) {
|
|
835
|
+
queue.push(ev);
|
|
836
|
+
if (session)
|
|
837
|
+
this.notify(session);
|
|
838
|
+
}
|
|
839
|
+
} catch (err) {
|
|
840
|
+
const ev = {
|
|
841
|
+
type: "error",
|
|
842
|
+
message: `parser failure: ${err instanceof Error ? err.message : String(err)}`
|
|
843
|
+
};
|
|
844
|
+
queue.push(ev);
|
|
845
|
+
if (session)
|
|
846
|
+
this.notify(session);
|
|
847
|
+
} finally {
|
|
848
|
+
if (session) {
|
|
849
|
+
session.closed = true;
|
|
850
|
+
this.notify(session);
|
|
851
|
+
this.registry.unregister(session.sessionId);
|
|
852
|
+
}
|
|
853
|
+
if (!bound) {
|
|
854
|
+
rejectHandle(new Error("claude exited without emitting a session id"));
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
})();
|
|
858
|
+
drainStream(spawned.stderr);
|
|
859
|
+
spawned.proc.once("error", (err) => {
|
|
860
|
+
if (!bound)
|
|
861
|
+
rejectHandle(err);
|
|
862
|
+
});
|
|
863
|
+
spawned.proc.once("exit", () => {
|
|
864
|
+
if (!bound) {
|
|
865
|
+
rejectHandle(new Error("claude exited before session id was captured"));
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
return handlePromise;
|
|
869
|
+
}
|
|
870
|
+
notify(session) {
|
|
871
|
+
const waiters = session.waiters;
|
|
872
|
+
session.waiters = [];
|
|
873
|
+
for (const w of waiters)
|
|
874
|
+
w();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
function drainStream(stream) {
|
|
878
|
+
const s = stream;
|
|
879
|
+
s.on("data", () => {});
|
|
880
|
+
s.on("error", () => {});
|
|
881
|
+
}
|
|
882
|
+
var init_claude_code_local = __esm(() => {
|
|
883
|
+
init_binary();
|
|
884
|
+
init_history();
|
|
885
|
+
init_sessions();
|
|
886
|
+
init_spawn();
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
// src/orchestrator/bridge/server.ts
|
|
890
|
+
import { mkdir, unlink as unlink2 } from "fs/promises";
|
|
891
|
+
import { createServer } from "net";
|
|
892
|
+
import { dirname } from "path";
|
|
893
|
+
async function startBridgeServer(orch, socketPath) {
|
|
894
|
+
await mkdir(dirname(socketPath), { recursive: true });
|
|
895
|
+
await unlink2(socketPath).catch(() => {});
|
|
896
|
+
const server = createServer((conn) => {
|
|
897
|
+
let buffer = "";
|
|
898
|
+
conn.on("data", (chunk) => {
|
|
899
|
+
buffer += chunk.toString("utf8");
|
|
900
|
+
let nl = buffer.indexOf(`
|
|
901
|
+
`);
|
|
902
|
+
while (nl !== -1) {
|
|
903
|
+
const line = buffer.slice(0, nl);
|
|
904
|
+
buffer = buffer.slice(nl + 1);
|
|
905
|
+
if (line.trim().length > 0) {
|
|
906
|
+
handleLine(orch, line).then((reply) => conn.write(`${reply}
|
|
907
|
+
`)).catch((err) => {
|
|
908
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
909
|
+
conn.write(`${JSON.stringify({ error: { message: msg } })}
|
|
910
|
+
`);
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
nl = buffer.indexOf(`
|
|
914
|
+
`);
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
conn.on("error", () => {});
|
|
918
|
+
});
|
|
919
|
+
await new Promise((resolve, reject) => {
|
|
920
|
+
server.once("error", reject);
|
|
921
|
+
server.listen(socketPath, () => {
|
|
922
|
+
server.removeListener("error", reject);
|
|
923
|
+
resolve();
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
return {
|
|
927
|
+
socketPath,
|
|
928
|
+
async close() {
|
|
929
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
930
|
+
await unlink2(socketPath).catch(() => {});
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
async function handleLine(orch, line) {
|
|
935
|
+
let req;
|
|
936
|
+
try {
|
|
937
|
+
req = JSON.parse(line);
|
|
938
|
+
} catch (err) {
|
|
939
|
+
return JSON.stringify({ error: { message: `bad json: ${err instanceof Error ? err.message : String(err)}` } });
|
|
940
|
+
}
|
|
941
|
+
const id = req.id ?? null;
|
|
942
|
+
try {
|
|
943
|
+
const result = await dispatch(orch, req.method, req.params ?? {});
|
|
944
|
+
return JSON.stringify({ id, result });
|
|
945
|
+
} catch (err) {
|
|
946
|
+
return JSON.stringify({
|
|
947
|
+
id,
|
|
948
|
+
error: { message: err instanceof Error ? err.message : String(err) }
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
async function dispatch(orch, method, params) {
|
|
953
|
+
switch (method) {
|
|
954
|
+
case "list_tasks": {
|
|
955
|
+
return orch.listTasks().map(serializeTask2);
|
|
956
|
+
}
|
|
957
|
+
case "get_task": {
|
|
958
|
+
const id = requireString(params, "task_id");
|
|
959
|
+
const task = orch.getTask(id);
|
|
960
|
+
if (!task)
|
|
961
|
+
throw new Error(`task not found: ${id}`);
|
|
962
|
+
return serializeTask2(task);
|
|
963
|
+
}
|
|
964
|
+
case "spawn_task": {
|
|
965
|
+
const repo = requireString(params, "repo");
|
|
966
|
+
const prompt = requireString(params, "prompt");
|
|
967
|
+
const title = optionalString(params, "title");
|
|
968
|
+
const baseRef = optionalString(params, "base_branch");
|
|
969
|
+
const task = await orch.createTask({
|
|
970
|
+
repo,
|
|
971
|
+
prompt,
|
|
972
|
+
...title ? { title } : {},
|
|
973
|
+
...baseRef ? { baseRef } : {}
|
|
974
|
+
});
|
|
975
|
+
orch.runTask(task.id, prompt).catch(() => {});
|
|
976
|
+
return serializeTask2(task);
|
|
977
|
+
}
|
|
978
|
+
case "send_message": {
|
|
979
|
+
const id = requireString(params, "task_id");
|
|
980
|
+
const prompt = requireString(params, "prompt");
|
|
981
|
+
await orch.runTask(id, prompt);
|
|
982
|
+
return { ok: true };
|
|
983
|
+
}
|
|
984
|
+
default:
|
|
985
|
+
throw new Error(`unknown method: ${method}`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
function requireString(params, key) {
|
|
989
|
+
const v = params[key];
|
|
990
|
+
if (typeof v !== "string" || v.length === 0) {
|
|
991
|
+
throw new Error(`param '${key}' is required and must be a non-empty string`);
|
|
992
|
+
}
|
|
993
|
+
return v;
|
|
994
|
+
}
|
|
995
|
+
function optionalString(params, key) {
|
|
996
|
+
const v = params[key];
|
|
997
|
+
if (v === undefined || v === null || v === "")
|
|
998
|
+
return;
|
|
999
|
+
if (typeof v !== "string")
|
|
1000
|
+
throw new Error(`param '${key}' must be a string`);
|
|
1001
|
+
return v;
|
|
1002
|
+
}
|
|
1003
|
+
function serializeTask2(task) {
|
|
1004
|
+
return {
|
|
1005
|
+
id: task.id,
|
|
1006
|
+
title: task.title,
|
|
1007
|
+
repo: task.repo,
|
|
1008
|
+
branch: task.branch,
|
|
1009
|
+
worktree_path: task.worktreePath,
|
|
1010
|
+
status: task.status,
|
|
1011
|
+
session_id: task.sessionId
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
var init_server = () => {};
|
|
1015
|
+
|
|
1016
|
+
// src/orchestrator/bridge/index.ts
|
|
1017
|
+
var exports_bridge = {};
|
|
1018
|
+
__export(exports_bridge, {
|
|
1019
|
+
startBridge: () => startBridge
|
|
1020
|
+
});
|
|
1021
|
+
import { writeFile } from "fs/promises";
|
|
1022
|
+
import { homedir as homedir4 } from "os";
|
|
1023
|
+
import { join } from "path";
|
|
1024
|
+
async function startBridge(orch, opts = {}) {
|
|
1025
|
+
const home = opts.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir4();
|
|
1026
|
+
const runDir = join(home, ".kobe", "run");
|
|
1027
|
+
const socketPath = join(runDir, `bridge-${process.pid}.sock`);
|
|
1028
|
+
const mcpConfigPath = join(runDir, `mcp-${process.pid}.json`);
|
|
1029
|
+
const server = await startBridgeServer(orch, socketPath);
|
|
1030
|
+
const entry = process.argv[1] ?? "";
|
|
1031
|
+
const mcpConfig = {
|
|
1032
|
+
mcpServers: {
|
|
1033
|
+
kobe: {
|
|
1034
|
+
command: process.execPath,
|
|
1035
|
+
args: [entry, "mcp-bridge", `--socket=${socketPath}`]
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
await writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
|
|
1040
|
+
process.env.KOBE_MCP_CONFIG = mcpConfigPath;
|
|
1041
|
+
return {
|
|
1042
|
+
socketPath,
|
|
1043
|
+
mcpConfigPath,
|
|
1044
|
+
async close() {
|
|
1045
|
+
await server.close();
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
var init_bridge = __esm(() => {
|
|
1050
|
+
init_server();
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
// ../../node_modules/.bun/solid-js@1.9.10/node_modules/solid-js/dist/dev.js
|
|
1054
|
+
function getContextId(count) {
|
|
1055
|
+
const num = String(count), len = num.length - 1;
|
|
1056
|
+
return sharedConfig.context.id + (len ? String.fromCharCode(96 + len) : "") + num;
|
|
1057
|
+
}
|
|
1058
|
+
function setHydrateContext(context) {
|
|
1059
|
+
sharedConfig.context = context;
|
|
1060
|
+
}
|
|
1061
|
+
function nextHydrateContext() {
|
|
1062
|
+
return {
|
|
1063
|
+
...sharedConfig.context,
|
|
1064
|
+
id: sharedConfig.getNextContextId(),
|
|
1065
|
+
count: 0
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
function createRoot(fn, detachedOwner) {
|
|
1069
|
+
const listener = Listener, owner = Owner, unowned = fn.length === 0, current = detachedOwner === undefined ? owner : detachedOwner, root = unowned ? {
|
|
1070
|
+
owned: null,
|
|
1071
|
+
cleanups: null,
|
|
1072
|
+
context: null,
|
|
1073
|
+
owner: null
|
|
1074
|
+
} : {
|
|
1075
|
+
owned: null,
|
|
1076
|
+
cleanups: null,
|
|
1077
|
+
context: current ? current.context : null,
|
|
1078
|
+
owner: current
|
|
1079
|
+
}, updateFn = unowned ? () => fn(() => {
|
|
1080
|
+
throw new Error("Dispose method must be an explicit argument to createRoot function");
|
|
1081
|
+
}) : () => fn(() => untrack(() => cleanNode(root)));
|
|
1082
|
+
DevHooks.afterCreateOwner && DevHooks.afterCreateOwner(root);
|
|
1083
|
+
Owner = root;
|
|
1084
|
+
Listener = null;
|
|
1085
|
+
try {
|
|
1086
|
+
return runUpdates(updateFn, true);
|
|
1087
|
+
} finally {
|
|
1088
|
+
Listener = listener;
|
|
1089
|
+
Owner = owner;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function createSignal(value, options) {
|
|
1093
|
+
options = options ? Object.assign({}, signalOptions, options) : signalOptions;
|
|
1094
|
+
const s = {
|
|
1095
|
+
value,
|
|
1096
|
+
observers: null,
|
|
1097
|
+
observerSlots: null,
|
|
1098
|
+
comparator: options.equals || undefined
|
|
1099
|
+
};
|
|
1100
|
+
{
|
|
1101
|
+
if (options.name)
|
|
1102
|
+
s.name = options.name;
|
|
1103
|
+
if (options.internal) {
|
|
1104
|
+
s.internal = true;
|
|
1105
|
+
} else {
|
|
1106
|
+
registerGraph(s);
|
|
1107
|
+
if (DevHooks.afterCreateSignal)
|
|
1108
|
+
DevHooks.afterCreateSignal(s);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
const setter = (value2) => {
|
|
1112
|
+
if (typeof value2 === "function") {
|
|
1113
|
+
if (Transition && Transition.running && Transition.sources.has(s))
|
|
1114
|
+
value2 = value2(s.tValue);
|
|
1115
|
+
else
|
|
1116
|
+
value2 = value2(s.value);
|
|
1117
|
+
}
|
|
1118
|
+
return writeSignal(s, value2);
|
|
1119
|
+
};
|
|
1120
|
+
return [readSignal.bind(s), setter];
|
|
1121
|
+
}
|
|
1122
|
+
function createComputed(fn, value, options) {
|
|
1123
|
+
const c = createComputation(fn, value, true, STALE, options);
|
|
1124
|
+
if (Scheduler && Transition && Transition.running)
|
|
1125
|
+
Updates.push(c);
|
|
1126
|
+
else
|
|
1127
|
+
updateComputation(c);
|
|
1128
|
+
}
|
|
1129
|
+
function createRenderEffect(fn, value, options) {
|
|
1130
|
+
const c = createComputation(fn, value, false, STALE, options);
|
|
1131
|
+
if (Scheduler && Transition && Transition.running)
|
|
1132
|
+
Updates.push(c);
|
|
1133
|
+
else
|
|
1134
|
+
updateComputation(c);
|
|
1135
|
+
}
|
|
1136
|
+
function createEffect(fn, value, options) {
|
|
1137
|
+
runEffects = runUserEffects;
|
|
1138
|
+
const c = createComputation(fn, value, false, STALE, options), s = SuspenseContext && useContext(SuspenseContext);
|
|
1139
|
+
if (s)
|
|
1140
|
+
c.suspense = s;
|
|
1141
|
+
if (!options || !options.render)
|
|
1142
|
+
c.user = true;
|
|
1143
|
+
Effects ? Effects.push(c) : updateComputation(c);
|
|
1144
|
+
}
|
|
1145
|
+
function createMemo(fn, value, options) {
|
|
1146
|
+
options = options ? Object.assign({}, signalOptions, options) : signalOptions;
|
|
1147
|
+
const c = createComputation(fn, value, true, 0, options);
|
|
1148
|
+
c.observers = null;
|
|
1149
|
+
c.observerSlots = null;
|
|
1150
|
+
c.comparator = options.equals || undefined;
|
|
1151
|
+
if (Scheduler && Transition && Transition.running) {
|
|
1152
|
+
c.tState = STALE;
|
|
1153
|
+
Updates.push(c);
|
|
1154
|
+
} else
|
|
1155
|
+
updateComputation(c);
|
|
1156
|
+
return readSignal.bind(c);
|
|
1157
|
+
}
|
|
1158
|
+
function isPromise(v) {
|
|
1159
|
+
return v && typeof v === "object" && "then" in v;
|
|
1160
|
+
}
|
|
1161
|
+
function createResource(pSource, pFetcher, pOptions) {
|
|
1162
|
+
let source;
|
|
1163
|
+
let fetcher;
|
|
1164
|
+
let options;
|
|
1165
|
+
if (typeof pFetcher === "function") {
|
|
1166
|
+
source = pSource;
|
|
1167
|
+
fetcher = pFetcher;
|
|
1168
|
+
options = pOptions || {};
|
|
1169
|
+
} else {
|
|
1170
|
+
source = true;
|
|
1171
|
+
fetcher = pSource;
|
|
1172
|
+
options = pFetcher || {};
|
|
1173
|
+
}
|
|
1174
|
+
let pr = null, initP = NO_INIT, id = null, loadedUnderTransition = false, scheduled = false, resolved = "initialValue" in options, dynamic = typeof source === "function" && createMemo(source);
|
|
1175
|
+
const contexts = new Set, [value, setValue] = (options.storage || createSignal)(options.initialValue), [error, setError] = createSignal(undefined), [track, trigger] = createSignal(undefined, {
|
|
1176
|
+
equals: false
|
|
1177
|
+
}), [state, setState] = createSignal(resolved ? "ready" : "unresolved");
|
|
1178
|
+
if (sharedConfig.context) {
|
|
1179
|
+
id = sharedConfig.getNextContextId();
|
|
1180
|
+
if (options.ssrLoadFrom === "initial")
|
|
1181
|
+
initP = options.initialValue;
|
|
1182
|
+
else if (sharedConfig.load && sharedConfig.has(id))
|
|
1183
|
+
initP = sharedConfig.load(id);
|
|
1184
|
+
}
|
|
1185
|
+
function loadEnd(p, v, error2, key) {
|
|
1186
|
+
if (pr === p) {
|
|
1187
|
+
pr = null;
|
|
1188
|
+
key !== undefined && (resolved = true);
|
|
1189
|
+
if ((p === initP || v === initP) && options.onHydrated)
|
|
1190
|
+
queueMicrotask(() => options.onHydrated(key, {
|
|
1191
|
+
value: v
|
|
1192
|
+
}));
|
|
1193
|
+
initP = NO_INIT;
|
|
1194
|
+
if (Transition && p && loadedUnderTransition) {
|
|
1195
|
+
Transition.promises.delete(p);
|
|
1196
|
+
loadedUnderTransition = false;
|
|
1197
|
+
runUpdates(() => {
|
|
1198
|
+
Transition.running = true;
|
|
1199
|
+
completeLoad(v, error2);
|
|
1200
|
+
}, false);
|
|
1201
|
+
} else
|
|
1202
|
+
completeLoad(v, error2);
|
|
1203
|
+
}
|
|
1204
|
+
return v;
|
|
1205
|
+
}
|
|
1206
|
+
function completeLoad(v, err) {
|
|
1207
|
+
runUpdates(() => {
|
|
1208
|
+
if (err === undefined)
|
|
1209
|
+
setValue(() => v);
|
|
1210
|
+
setState(err !== undefined ? "errored" : resolved ? "ready" : "unresolved");
|
|
1211
|
+
setError(err);
|
|
1212
|
+
for (const c of contexts.keys())
|
|
1213
|
+
c.decrement();
|
|
1214
|
+
contexts.clear();
|
|
1215
|
+
}, false);
|
|
1216
|
+
}
|
|
1217
|
+
function read() {
|
|
1218
|
+
const c = SuspenseContext && useContext(SuspenseContext), v = value(), err = error();
|
|
1219
|
+
if (err !== undefined && !pr)
|
|
1220
|
+
throw err;
|
|
1221
|
+
if (Listener && !Listener.user && c) {
|
|
1222
|
+
createComputed(() => {
|
|
1223
|
+
track();
|
|
1224
|
+
if (pr) {
|
|
1225
|
+
if (c.resolved && Transition && loadedUnderTransition)
|
|
1226
|
+
Transition.promises.add(pr);
|
|
1227
|
+
else if (!contexts.has(c)) {
|
|
1228
|
+
c.increment();
|
|
1229
|
+
contexts.add(c);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
return v;
|
|
1235
|
+
}
|
|
1236
|
+
function load(refetching = true) {
|
|
1237
|
+
if (refetching !== false && scheduled)
|
|
1238
|
+
return;
|
|
1239
|
+
scheduled = false;
|
|
1240
|
+
const lookup = dynamic ? dynamic() : source;
|
|
1241
|
+
loadedUnderTransition = Transition && Transition.running;
|
|
1242
|
+
if (lookup == null || lookup === false) {
|
|
1243
|
+
loadEnd(pr, untrack(value));
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
if (Transition && pr)
|
|
1247
|
+
Transition.promises.delete(pr);
|
|
1248
|
+
let error2;
|
|
1249
|
+
const p = initP !== NO_INIT ? initP : untrack(() => {
|
|
1250
|
+
try {
|
|
1251
|
+
return fetcher(lookup, {
|
|
1252
|
+
value: value(),
|
|
1253
|
+
refetching
|
|
1254
|
+
});
|
|
1255
|
+
} catch (fetcherError) {
|
|
1256
|
+
error2 = fetcherError;
|
|
1257
|
+
}
|
|
1258
|
+
});
|
|
1259
|
+
if (error2 !== undefined) {
|
|
1260
|
+
loadEnd(pr, undefined, castError(error2), lookup);
|
|
1261
|
+
return;
|
|
1262
|
+
} else if (!isPromise(p)) {
|
|
1263
|
+
loadEnd(pr, p, undefined, lookup);
|
|
1264
|
+
return p;
|
|
1265
|
+
}
|
|
1266
|
+
pr = p;
|
|
1267
|
+
if ("v" in p) {
|
|
1268
|
+
if (p.s === 1)
|
|
1269
|
+
loadEnd(pr, p.v, undefined, lookup);
|
|
1270
|
+
else
|
|
1271
|
+
loadEnd(pr, undefined, castError(p.v), lookup);
|
|
1272
|
+
return p;
|
|
1273
|
+
}
|
|
1274
|
+
scheduled = true;
|
|
1275
|
+
queueMicrotask(() => scheduled = false);
|
|
1276
|
+
runUpdates(() => {
|
|
1277
|
+
setState(resolved ? "refreshing" : "pending");
|
|
1278
|
+
trigger();
|
|
1279
|
+
}, false);
|
|
1280
|
+
return p.then((v) => loadEnd(p, v, undefined, lookup), (e) => loadEnd(p, undefined, castError(e), lookup));
|
|
1281
|
+
}
|
|
1282
|
+
Object.defineProperties(read, {
|
|
1283
|
+
state: {
|
|
1284
|
+
get: () => state()
|
|
1285
|
+
},
|
|
1286
|
+
error: {
|
|
1287
|
+
get: () => error()
|
|
1288
|
+
},
|
|
1289
|
+
loading: {
|
|
1290
|
+
get() {
|
|
1291
|
+
const s = state();
|
|
1292
|
+
return s === "pending" || s === "refreshing";
|
|
1293
|
+
}
|
|
1294
|
+
},
|
|
1295
|
+
latest: {
|
|
1296
|
+
get() {
|
|
1297
|
+
if (!resolved)
|
|
1298
|
+
return read();
|
|
1299
|
+
const err = error();
|
|
1300
|
+
if (err && !pr)
|
|
1301
|
+
throw err;
|
|
1302
|
+
return value();
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
let owner = Owner;
|
|
1307
|
+
if (dynamic)
|
|
1308
|
+
createComputed(() => (owner = Owner, load(false)));
|
|
1309
|
+
else
|
|
1310
|
+
load(false);
|
|
1311
|
+
return [read, {
|
|
1312
|
+
refetch: (info) => runWithOwner(owner, () => load(info)),
|
|
1313
|
+
mutate: setValue
|
|
1314
|
+
}];
|
|
1315
|
+
}
|
|
1316
|
+
function batch(fn) {
|
|
1317
|
+
return runUpdates(fn, false);
|
|
1318
|
+
}
|
|
1319
|
+
function untrack(fn) {
|
|
1320
|
+
if (!ExternalSourceConfig && Listener === null)
|
|
1321
|
+
return fn();
|
|
1322
|
+
const listener = Listener;
|
|
1323
|
+
Listener = null;
|
|
1324
|
+
try {
|
|
1325
|
+
if (ExternalSourceConfig)
|
|
1326
|
+
return ExternalSourceConfig.untrack(fn);
|
|
1327
|
+
return fn();
|
|
1328
|
+
} finally {
|
|
1329
|
+
Listener = listener;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function on(deps, fn, options) {
|
|
1333
|
+
const isArray = Array.isArray(deps);
|
|
1334
|
+
let prevInput;
|
|
1335
|
+
let defer = options && options.defer;
|
|
1336
|
+
return (prevValue) => {
|
|
1337
|
+
let input;
|
|
1338
|
+
if (isArray) {
|
|
1339
|
+
input = Array(deps.length);
|
|
1340
|
+
for (let i = 0;i < deps.length; i++)
|
|
1341
|
+
input[i] = deps[i]();
|
|
1342
|
+
} else
|
|
1343
|
+
input = deps();
|
|
1344
|
+
if (defer) {
|
|
1345
|
+
defer = false;
|
|
1346
|
+
return prevValue;
|
|
1347
|
+
}
|
|
1348
|
+
const result = untrack(() => fn(input, prevInput, prevValue));
|
|
1349
|
+
prevInput = input;
|
|
1350
|
+
return result;
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
function onMount(fn) {
|
|
1354
|
+
createEffect(() => untrack(fn));
|
|
1355
|
+
}
|
|
1356
|
+
function onCleanup(fn) {
|
|
1357
|
+
if (Owner === null)
|
|
1358
|
+
console.warn("cleanups created outside a `createRoot` or `render` will never be run");
|
|
1359
|
+
else if (Owner.cleanups === null)
|
|
1360
|
+
Owner.cleanups = [fn];
|
|
1361
|
+
else
|
|
1362
|
+
Owner.cleanups.push(fn);
|
|
1363
|
+
return fn;
|
|
1364
|
+
}
|
|
1365
|
+
function getListener() {
|
|
1366
|
+
return Listener;
|
|
1367
|
+
}
|
|
1368
|
+
function runWithOwner(o, fn) {
|
|
1369
|
+
const prev = Owner;
|
|
1370
|
+
const prevListener = Listener;
|
|
1371
|
+
Owner = o;
|
|
1372
|
+
Listener = null;
|
|
1373
|
+
try {
|
|
1374
|
+
return runUpdates(fn, true);
|
|
1375
|
+
} catch (err) {
|
|
1376
|
+
handleError(err);
|
|
1377
|
+
} finally {
|
|
1378
|
+
Owner = prev;
|
|
1379
|
+
Listener = prevListener;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
function startTransition(fn) {
|
|
1383
|
+
if (Transition && Transition.running) {
|
|
1384
|
+
fn();
|
|
1385
|
+
return Transition.done;
|
|
1386
|
+
}
|
|
1387
|
+
const l = Listener;
|
|
1388
|
+
const o = Owner;
|
|
1389
|
+
return Promise.resolve().then(() => {
|
|
1390
|
+
Listener = l;
|
|
1391
|
+
Owner = o;
|
|
1392
|
+
let t;
|
|
1393
|
+
if (Scheduler || SuspenseContext) {
|
|
1394
|
+
t = Transition || (Transition = {
|
|
1395
|
+
sources: new Set,
|
|
1396
|
+
effects: [],
|
|
1397
|
+
promises: new Set,
|
|
1398
|
+
disposed: new Set,
|
|
1399
|
+
queue: new Set,
|
|
1400
|
+
running: true
|
|
1401
|
+
});
|
|
1402
|
+
t.done || (t.done = new Promise((res) => t.resolve = res));
|
|
1403
|
+
t.running = true;
|
|
1404
|
+
}
|
|
1405
|
+
runUpdates(fn, false);
|
|
1406
|
+
Listener = Owner = null;
|
|
1407
|
+
return t ? t.done : undefined;
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
function devComponent(Comp, props) {
|
|
1411
|
+
const c = createComputation(() => untrack(() => {
|
|
1412
|
+
Object.assign(Comp, {
|
|
1413
|
+
[$DEVCOMP]: true
|
|
1414
|
+
});
|
|
1415
|
+
return Comp(props);
|
|
1416
|
+
}), undefined, true, 0);
|
|
1417
|
+
c.props = props;
|
|
1418
|
+
c.observers = null;
|
|
1419
|
+
c.observerSlots = null;
|
|
1420
|
+
c.name = Comp.name;
|
|
1421
|
+
c.component = Comp;
|
|
1422
|
+
updateComputation(c);
|
|
1423
|
+
return c.tValue !== undefined ? c.tValue : c.value;
|
|
1424
|
+
}
|
|
1425
|
+
function registerGraph(value) {
|
|
1426
|
+
if (Owner) {
|
|
1427
|
+
if (Owner.sourceMap)
|
|
1428
|
+
Owner.sourceMap.push(value);
|
|
1429
|
+
else
|
|
1430
|
+
Owner.sourceMap = [value];
|
|
1431
|
+
value.graph = Owner;
|
|
1432
|
+
}
|
|
1433
|
+
if (DevHooks.afterRegisterGraph)
|
|
1434
|
+
DevHooks.afterRegisterGraph(value);
|
|
1435
|
+
}
|
|
1436
|
+
function createContext(defaultValue, options) {
|
|
1437
|
+
const id = Symbol("context");
|
|
1438
|
+
return {
|
|
1439
|
+
id,
|
|
1440
|
+
Provider: createProvider(id, options),
|
|
1441
|
+
defaultValue
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
function useContext(context) {
|
|
1445
|
+
let value;
|
|
1446
|
+
return Owner && Owner.context && (value = Owner.context[context.id]) !== undefined ? value : context.defaultValue;
|
|
1447
|
+
}
|
|
1448
|
+
function children(fn) {
|
|
1449
|
+
const children2 = createMemo(fn);
|
|
1450
|
+
const memo = createMemo(() => resolveChildren(children2()), undefined, {
|
|
1451
|
+
name: "children"
|
|
1452
|
+
});
|
|
1453
|
+
memo.toArray = () => {
|
|
1454
|
+
const c = memo();
|
|
1455
|
+
return Array.isArray(c) ? c : c != null ? [c] : [];
|
|
1456
|
+
};
|
|
1457
|
+
return memo;
|
|
1458
|
+
}
|
|
1459
|
+
function readSignal() {
|
|
1460
|
+
const runningTransition = Transition && Transition.running;
|
|
1461
|
+
if (this.sources && (runningTransition ? this.tState : this.state)) {
|
|
1462
|
+
if ((runningTransition ? this.tState : this.state) === STALE)
|
|
1463
|
+
updateComputation(this);
|
|
1464
|
+
else {
|
|
1465
|
+
const updates = Updates;
|
|
1466
|
+
Updates = null;
|
|
1467
|
+
runUpdates(() => lookUpstream(this), false);
|
|
1468
|
+
Updates = updates;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
if (Listener) {
|
|
1472
|
+
const sSlot = this.observers ? this.observers.length : 0;
|
|
1473
|
+
if (!Listener.sources) {
|
|
1474
|
+
Listener.sources = [this];
|
|
1475
|
+
Listener.sourceSlots = [sSlot];
|
|
1476
|
+
} else {
|
|
1477
|
+
Listener.sources.push(this);
|
|
1478
|
+
Listener.sourceSlots.push(sSlot);
|
|
1479
|
+
}
|
|
1480
|
+
if (!this.observers) {
|
|
1481
|
+
this.observers = [Listener];
|
|
1482
|
+
this.observerSlots = [Listener.sources.length - 1];
|
|
1483
|
+
} else {
|
|
1484
|
+
this.observers.push(Listener);
|
|
1485
|
+
this.observerSlots.push(Listener.sources.length - 1);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
if (runningTransition && Transition.sources.has(this))
|
|
1489
|
+
return this.tValue;
|
|
1490
|
+
return this.value;
|
|
1491
|
+
}
|
|
1492
|
+
function writeSignal(node, value, isComp) {
|
|
1493
|
+
let current = Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value;
|
|
1494
|
+
if (!node.comparator || !node.comparator(current, value)) {
|
|
1495
|
+
if (Transition) {
|
|
1496
|
+
const TransitionRunning = Transition.running;
|
|
1497
|
+
if (TransitionRunning || !isComp && Transition.sources.has(node)) {
|
|
1498
|
+
Transition.sources.add(node);
|
|
1499
|
+
node.tValue = value;
|
|
1500
|
+
}
|
|
1501
|
+
if (!TransitionRunning)
|
|
1502
|
+
node.value = value;
|
|
1503
|
+
} else
|
|
1504
|
+
node.value = value;
|
|
1505
|
+
if (node.observers && node.observers.length) {
|
|
1506
|
+
runUpdates(() => {
|
|
1507
|
+
for (let i = 0;i < node.observers.length; i += 1) {
|
|
1508
|
+
const o = node.observers[i];
|
|
1509
|
+
const TransitionRunning = Transition && Transition.running;
|
|
1510
|
+
if (TransitionRunning && Transition.disposed.has(o))
|
|
1511
|
+
continue;
|
|
1512
|
+
if (TransitionRunning ? !o.tState : !o.state) {
|
|
1513
|
+
if (o.pure)
|
|
1514
|
+
Updates.push(o);
|
|
1515
|
+
else
|
|
1516
|
+
Effects.push(o);
|
|
1517
|
+
if (o.observers)
|
|
1518
|
+
markDownstream(o);
|
|
1519
|
+
}
|
|
1520
|
+
if (!TransitionRunning)
|
|
1521
|
+
o.state = STALE;
|
|
1522
|
+
else
|
|
1523
|
+
o.tState = STALE;
|
|
1524
|
+
}
|
|
1525
|
+
if (Updates.length > 1e6) {
|
|
1526
|
+
Updates = [];
|
|
1527
|
+
if (IS_DEV)
|
|
1528
|
+
throw new Error("Potential Infinite Loop Detected.");
|
|
1529
|
+
throw new Error;
|
|
1530
|
+
}
|
|
1531
|
+
}, false);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
return value;
|
|
1535
|
+
}
|
|
1536
|
+
function updateComputation(node) {
|
|
1537
|
+
if (!node.fn)
|
|
1538
|
+
return;
|
|
1539
|
+
cleanNode(node);
|
|
1540
|
+
const time = ExecCount;
|
|
1541
|
+
runComputation(node, Transition && Transition.running && Transition.sources.has(node) ? node.tValue : node.value, time);
|
|
1542
|
+
if (Transition && !Transition.running && Transition.sources.has(node)) {
|
|
1543
|
+
queueMicrotask(() => {
|
|
1544
|
+
runUpdates(() => {
|
|
1545
|
+
Transition && (Transition.running = true);
|
|
1546
|
+
Listener = Owner = node;
|
|
1547
|
+
runComputation(node, node.tValue, time);
|
|
1548
|
+
Listener = Owner = null;
|
|
1549
|
+
}, false);
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
function runComputation(node, value, time) {
|
|
1554
|
+
let nextValue;
|
|
1555
|
+
const owner = Owner, listener = Listener;
|
|
1556
|
+
Listener = Owner = node;
|
|
1557
|
+
try {
|
|
1558
|
+
nextValue = node.fn(value);
|
|
1559
|
+
} catch (err) {
|
|
1560
|
+
if (node.pure) {
|
|
1561
|
+
if (Transition && Transition.running) {
|
|
1562
|
+
node.tState = STALE;
|
|
1563
|
+
node.tOwned && node.tOwned.forEach(cleanNode);
|
|
1564
|
+
node.tOwned = undefined;
|
|
1565
|
+
} else {
|
|
1566
|
+
node.state = STALE;
|
|
1567
|
+
node.owned && node.owned.forEach(cleanNode);
|
|
1568
|
+
node.owned = null;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
node.updatedAt = time + 1;
|
|
1572
|
+
return handleError(err);
|
|
1573
|
+
} finally {
|
|
1574
|
+
Listener = listener;
|
|
1575
|
+
Owner = owner;
|
|
1576
|
+
}
|
|
1577
|
+
if (!node.updatedAt || node.updatedAt <= time) {
|
|
1578
|
+
if (node.updatedAt != null && "observers" in node) {
|
|
1579
|
+
writeSignal(node, nextValue, true);
|
|
1580
|
+
} else if (Transition && Transition.running && node.pure) {
|
|
1581
|
+
Transition.sources.add(node);
|
|
1582
|
+
node.tValue = nextValue;
|
|
1583
|
+
} else
|
|
1584
|
+
node.value = nextValue;
|
|
1585
|
+
node.updatedAt = time;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function createComputation(fn, init, pure, state = STALE, options) {
|
|
1589
|
+
const c = {
|
|
1590
|
+
fn,
|
|
1591
|
+
state,
|
|
1592
|
+
updatedAt: null,
|
|
1593
|
+
owned: null,
|
|
1594
|
+
sources: null,
|
|
1595
|
+
sourceSlots: null,
|
|
1596
|
+
cleanups: null,
|
|
1597
|
+
value: init,
|
|
1598
|
+
owner: Owner,
|
|
1599
|
+
context: Owner ? Owner.context : null,
|
|
1600
|
+
pure
|
|
1601
|
+
};
|
|
1602
|
+
if (Transition && Transition.running) {
|
|
1603
|
+
c.state = 0;
|
|
1604
|
+
c.tState = state;
|
|
1605
|
+
}
|
|
1606
|
+
if (Owner === null)
|
|
1607
|
+
console.warn("computations created outside a `createRoot` or `render` will never be disposed");
|
|
1608
|
+
else if (Owner !== UNOWNED) {
|
|
1609
|
+
if (Transition && Transition.running && Owner.pure) {
|
|
1610
|
+
if (!Owner.tOwned)
|
|
1611
|
+
Owner.tOwned = [c];
|
|
1612
|
+
else
|
|
1613
|
+
Owner.tOwned.push(c);
|
|
1614
|
+
} else {
|
|
1615
|
+
if (!Owner.owned)
|
|
1616
|
+
Owner.owned = [c];
|
|
1617
|
+
else
|
|
1618
|
+
Owner.owned.push(c);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
if (options && options.name)
|
|
1622
|
+
c.name = options.name;
|
|
1623
|
+
if (ExternalSourceConfig && c.fn) {
|
|
1624
|
+
const [track, trigger] = createSignal(undefined, {
|
|
1625
|
+
equals: false
|
|
1626
|
+
});
|
|
1627
|
+
const ordinary = ExternalSourceConfig.factory(c.fn, trigger);
|
|
1628
|
+
onCleanup(() => ordinary.dispose());
|
|
1629
|
+
const triggerInTransition = () => startTransition(trigger).then(() => inTransition.dispose());
|
|
1630
|
+
const inTransition = ExternalSourceConfig.factory(c.fn, triggerInTransition);
|
|
1631
|
+
c.fn = (x) => {
|
|
1632
|
+
track();
|
|
1633
|
+
return Transition && Transition.running ? inTransition.track(x) : ordinary.track(x);
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
DevHooks.afterCreateOwner && DevHooks.afterCreateOwner(c);
|
|
1637
|
+
return c;
|
|
1638
|
+
}
|
|
1639
|
+
function runTop(node) {
|
|
1640
|
+
const runningTransition = Transition && Transition.running;
|
|
1641
|
+
if ((runningTransition ? node.tState : node.state) === 0)
|
|
1642
|
+
return;
|
|
1643
|
+
if ((runningTransition ? node.tState : node.state) === PENDING)
|
|
1644
|
+
return lookUpstream(node);
|
|
1645
|
+
if (node.suspense && untrack(node.suspense.inFallback))
|
|
1646
|
+
return node.suspense.effects.push(node);
|
|
1647
|
+
const ancestors = [node];
|
|
1648
|
+
while ((node = node.owner) && (!node.updatedAt || node.updatedAt < ExecCount)) {
|
|
1649
|
+
if (runningTransition && Transition.disposed.has(node))
|
|
1650
|
+
return;
|
|
1651
|
+
if (runningTransition ? node.tState : node.state)
|
|
1652
|
+
ancestors.push(node);
|
|
1653
|
+
}
|
|
1654
|
+
for (let i = ancestors.length - 1;i >= 0; i--) {
|
|
1655
|
+
node = ancestors[i];
|
|
1656
|
+
if (runningTransition) {
|
|
1657
|
+
let top = node, prev = ancestors[i + 1];
|
|
1658
|
+
while ((top = top.owner) && top !== prev) {
|
|
1659
|
+
if (Transition.disposed.has(top))
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
if ((runningTransition ? node.tState : node.state) === STALE) {
|
|
1664
|
+
updateComputation(node);
|
|
1665
|
+
} else if ((runningTransition ? node.tState : node.state) === PENDING) {
|
|
1666
|
+
const updates = Updates;
|
|
1667
|
+
Updates = null;
|
|
1668
|
+
runUpdates(() => lookUpstream(node, ancestors[0]), false);
|
|
1669
|
+
Updates = updates;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
function runUpdates(fn, init) {
|
|
1674
|
+
if (Updates)
|
|
1675
|
+
return fn();
|
|
1676
|
+
let wait = false;
|
|
1677
|
+
if (!init)
|
|
1678
|
+
Updates = [];
|
|
1679
|
+
if (Effects)
|
|
1680
|
+
wait = true;
|
|
1681
|
+
else
|
|
1682
|
+
Effects = [];
|
|
1683
|
+
ExecCount++;
|
|
1684
|
+
try {
|
|
1685
|
+
const res = fn();
|
|
1686
|
+
completeUpdates(wait);
|
|
1687
|
+
return res;
|
|
1688
|
+
} catch (err) {
|
|
1689
|
+
if (!wait)
|
|
1690
|
+
Effects = null;
|
|
1691
|
+
Updates = null;
|
|
1692
|
+
handleError(err);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
function completeUpdates(wait) {
|
|
1696
|
+
if (Updates) {
|
|
1697
|
+
if (Scheduler && Transition && Transition.running)
|
|
1698
|
+
scheduleQueue(Updates);
|
|
1699
|
+
else
|
|
1700
|
+
runQueue(Updates);
|
|
1701
|
+
Updates = null;
|
|
1702
|
+
}
|
|
1703
|
+
if (wait)
|
|
1704
|
+
return;
|
|
1705
|
+
let res;
|
|
1706
|
+
if (Transition) {
|
|
1707
|
+
if (!Transition.promises.size && !Transition.queue.size) {
|
|
1708
|
+
const sources = Transition.sources;
|
|
1709
|
+
const disposed = Transition.disposed;
|
|
1710
|
+
Effects.push.apply(Effects, Transition.effects);
|
|
1711
|
+
res = Transition.resolve;
|
|
1712
|
+
for (const e2 of Effects) {
|
|
1713
|
+
"tState" in e2 && (e2.state = e2.tState);
|
|
1714
|
+
delete e2.tState;
|
|
1715
|
+
}
|
|
1716
|
+
Transition = null;
|
|
1717
|
+
runUpdates(() => {
|
|
1718
|
+
for (const d of disposed)
|
|
1719
|
+
cleanNode(d);
|
|
1720
|
+
for (const v of sources) {
|
|
1721
|
+
v.value = v.tValue;
|
|
1722
|
+
if (v.owned) {
|
|
1723
|
+
for (let i = 0, len = v.owned.length;i < len; i++)
|
|
1724
|
+
cleanNode(v.owned[i]);
|
|
1725
|
+
}
|
|
1726
|
+
if (v.tOwned)
|
|
1727
|
+
v.owned = v.tOwned;
|
|
1728
|
+
delete v.tValue;
|
|
1729
|
+
delete v.tOwned;
|
|
1730
|
+
v.tState = 0;
|
|
1731
|
+
}
|
|
1732
|
+
setTransPending(false);
|
|
1733
|
+
}, false);
|
|
1734
|
+
} else if (Transition.running) {
|
|
1735
|
+
Transition.running = false;
|
|
1736
|
+
Transition.effects.push.apply(Transition.effects, Effects);
|
|
1737
|
+
Effects = null;
|
|
1738
|
+
setTransPending(true);
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
const e = Effects;
|
|
1743
|
+
Effects = null;
|
|
1744
|
+
if (e.length)
|
|
1745
|
+
runUpdates(() => runEffects(e), false);
|
|
1746
|
+
else
|
|
1747
|
+
DevHooks.afterUpdate && DevHooks.afterUpdate();
|
|
1748
|
+
if (res)
|
|
1749
|
+
res();
|
|
1750
|
+
}
|
|
1751
|
+
function runQueue(queue) {
|
|
1752
|
+
for (let i = 0;i < queue.length; i++)
|
|
1753
|
+
runTop(queue[i]);
|
|
1754
|
+
}
|
|
1755
|
+
function scheduleQueue(queue) {
|
|
1756
|
+
for (let i = 0;i < queue.length; i++) {
|
|
1757
|
+
const item = queue[i];
|
|
1758
|
+
const tasks = Transition.queue;
|
|
1759
|
+
if (!tasks.has(item)) {
|
|
1760
|
+
tasks.add(item);
|
|
1761
|
+
Scheduler(() => {
|
|
1762
|
+
tasks.delete(item);
|
|
1763
|
+
runUpdates(() => {
|
|
1764
|
+
Transition.running = true;
|
|
1765
|
+
runTop(item);
|
|
1766
|
+
}, false);
|
|
1767
|
+
Transition && (Transition.running = false);
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
function runUserEffects(queue) {
|
|
1773
|
+
let i, userLength = 0;
|
|
1774
|
+
for (i = 0;i < queue.length; i++) {
|
|
1775
|
+
const e = queue[i];
|
|
1776
|
+
if (!e.user)
|
|
1777
|
+
runTop(e);
|
|
1778
|
+
else
|
|
1779
|
+
queue[userLength++] = e;
|
|
1780
|
+
}
|
|
1781
|
+
if (sharedConfig.context) {
|
|
1782
|
+
if (sharedConfig.count) {
|
|
1783
|
+
sharedConfig.effects || (sharedConfig.effects = []);
|
|
1784
|
+
sharedConfig.effects.push(...queue.slice(0, userLength));
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
setHydrateContext();
|
|
1788
|
+
}
|
|
1789
|
+
if (sharedConfig.effects && (sharedConfig.done || !sharedConfig.count)) {
|
|
1790
|
+
queue = [...sharedConfig.effects, ...queue];
|
|
1791
|
+
userLength += sharedConfig.effects.length;
|
|
1792
|
+
delete sharedConfig.effects;
|
|
1793
|
+
}
|
|
1794
|
+
for (i = 0;i < userLength; i++)
|
|
1795
|
+
runTop(queue[i]);
|
|
1796
|
+
}
|
|
1797
|
+
function lookUpstream(node, ignore) {
|
|
1798
|
+
const runningTransition = Transition && Transition.running;
|
|
1799
|
+
if (runningTransition)
|
|
1800
|
+
node.tState = 0;
|
|
1801
|
+
else
|
|
1802
|
+
node.state = 0;
|
|
1803
|
+
for (let i = 0;i < node.sources.length; i += 1) {
|
|
1804
|
+
const source = node.sources[i];
|
|
1805
|
+
if (source.sources) {
|
|
1806
|
+
const state = runningTransition ? source.tState : source.state;
|
|
1807
|
+
if (state === STALE) {
|
|
1808
|
+
if (source !== ignore && (!source.updatedAt || source.updatedAt < ExecCount))
|
|
1809
|
+
runTop(source);
|
|
1810
|
+
} else if (state === PENDING)
|
|
1811
|
+
lookUpstream(source, ignore);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
function markDownstream(node) {
|
|
1816
|
+
const runningTransition = Transition && Transition.running;
|
|
1817
|
+
for (let i = 0;i < node.observers.length; i += 1) {
|
|
1818
|
+
const o = node.observers[i];
|
|
1819
|
+
if (runningTransition ? !o.tState : !o.state) {
|
|
1820
|
+
if (runningTransition)
|
|
1821
|
+
o.tState = PENDING;
|
|
1822
|
+
else
|
|
1823
|
+
o.state = PENDING;
|
|
1824
|
+
if (o.pure)
|
|
1825
|
+
Updates.push(o);
|
|
1826
|
+
else
|
|
1827
|
+
Effects.push(o);
|
|
1828
|
+
o.observers && markDownstream(o);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
function cleanNode(node) {
|
|
1833
|
+
let i;
|
|
1834
|
+
if (node.sources) {
|
|
1835
|
+
while (node.sources.length) {
|
|
1836
|
+
const source = node.sources.pop(), index = node.sourceSlots.pop(), obs = source.observers;
|
|
1837
|
+
if (obs && obs.length) {
|
|
1838
|
+
const n = obs.pop(), s = source.observerSlots.pop();
|
|
1839
|
+
if (index < obs.length) {
|
|
1840
|
+
n.sourceSlots[s] = index;
|
|
1841
|
+
obs[index] = n;
|
|
1842
|
+
source.observerSlots[index] = s;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
if (node.tOwned) {
|
|
1848
|
+
for (i = node.tOwned.length - 1;i >= 0; i--)
|
|
1849
|
+
cleanNode(node.tOwned[i]);
|
|
1850
|
+
delete node.tOwned;
|
|
1851
|
+
}
|
|
1852
|
+
if (Transition && Transition.running && node.pure) {
|
|
1853
|
+
reset(node, true);
|
|
1854
|
+
} else if (node.owned) {
|
|
1855
|
+
for (i = node.owned.length - 1;i >= 0; i--)
|
|
1856
|
+
cleanNode(node.owned[i]);
|
|
1857
|
+
node.owned = null;
|
|
1858
|
+
}
|
|
1859
|
+
if (node.cleanups) {
|
|
1860
|
+
for (i = node.cleanups.length - 1;i >= 0; i--)
|
|
1861
|
+
node.cleanups[i]();
|
|
1862
|
+
node.cleanups = null;
|
|
1863
|
+
}
|
|
1864
|
+
if (Transition && Transition.running)
|
|
1865
|
+
node.tState = 0;
|
|
1866
|
+
else
|
|
1867
|
+
node.state = 0;
|
|
1868
|
+
delete node.sourceMap;
|
|
1869
|
+
}
|
|
1870
|
+
function reset(node, top) {
|
|
1871
|
+
if (!top) {
|
|
1872
|
+
node.tState = 0;
|
|
1873
|
+
Transition.disposed.add(node);
|
|
1874
|
+
}
|
|
1875
|
+
if (node.owned) {
|
|
1876
|
+
for (let i = 0;i < node.owned.length; i++)
|
|
1877
|
+
reset(node.owned[i]);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
function castError(err) {
|
|
1881
|
+
if (err instanceof Error)
|
|
1882
|
+
return err;
|
|
1883
|
+
return new Error(typeof err === "string" ? err : "Unknown error", {
|
|
1884
|
+
cause: err
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
function runErrors(err, fns, owner) {
|
|
1888
|
+
try {
|
|
1889
|
+
for (const f of fns)
|
|
1890
|
+
f(err);
|
|
1891
|
+
} catch (e) {
|
|
1892
|
+
handleError(e, owner && owner.owner || null);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
function handleError(err, owner = Owner) {
|
|
1896
|
+
const fns = ERROR && owner && owner.context && owner.context[ERROR];
|
|
1897
|
+
const error = castError(err);
|
|
1898
|
+
if (!fns)
|
|
1899
|
+
throw error;
|
|
1900
|
+
if (Effects)
|
|
1901
|
+
Effects.push({
|
|
1902
|
+
fn() {
|
|
1903
|
+
runErrors(error, fns, owner);
|
|
1904
|
+
},
|
|
1905
|
+
state: STALE
|
|
1906
|
+
});
|
|
1907
|
+
else
|
|
1908
|
+
runErrors(error, fns, owner);
|
|
1909
|
+
}
|
|
1910
|
+
function resolveChildren(children2) {
|
|
1911
|
+
if (typeof children2 === "function" && !children2.length)
|
|
1912
|
+
return resolveChildren(children2());
|
|
1913
|
+
if (Array.isArray(children2)) {
|
|
1914
|
+
const results = [];
|
|
1915
|
+
for (let i = 0;i < children2.length; i++) {
|
|
1916
|
+
const result = resolveChildren(children2[i]);
|
|
1917
|
+
Array.isArray(result) ? results.push.apply(results, result) : results.push(result);
|
|
1918
|
+
}
|
|
1919
|
+
return results;
|
|
1920
|
+
}
|
|
1921
|
+
return children2;
|
|
1922
|
+
}
|
|
1923
|
+
function createProvider(id, options) {
|
|
1924
|
+
return function provider(props) {
|
|
1925
|
+
let res;
|
|
1926
|
+
createRenderEffect(() => res = untrack(() => {
|
|
1927
|
+
Owner.context = {
|
|
1928
|
+
...Owner.context,
|
|
1929
|
+
[id]: props.value
|
|
1930
|
+
};
|
|
1931
|
+
return children(() => props.children);
|
|
1932
|
+
}), undefined, options);
|
|
1933
|
+
return res;
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
function dispose(d) {
|
|
1937
|
+
for (let i = 0;i < d.length; i++)
|
|
1938
|
+
d[i]();
|
|
1939
|
+
}
|
|
1940
|
+
function mapArray(list, mapFn, options = {}) {
|
|
1941
|
+
let items = [], mapped = [], disposers = [], len = 0, indexes = mapFn.length > 1 ? [] : null;
|
|
1942
|
+
onCleanup(() => dispose(disposers));
|
|
1943
|
+
return () => {
|
|
1944
|
+
let newItems = list() || [], newLen = newItems.length, i, j;
|
|
1945
|
+
newItems[$TRACK];
|
|
1946
|
+
return untrack(() => {
|
|
1947
|
+
let newIndices, newIndicesNext, temp, tempdisposers, tempIndexes, start, end, newEnd, item;
|
|
1948
|
+
if (newLen === 0) {
|
|
1949
|
+
if (len !== 0) {
|
|
1950
|
+
dispose(disposers);
|
|
1951
|
+
disposers = [];
|
|
1952
|
+
items = [];
|
|
1953
|
+
mapped = [];
|
|
1954
|
+
len = 0;
|
|
1955
|
+
indexes && (indexes = []);
|
|
1956
|
+
}
|
|
1957
|
+
if (options.fallback) {
|
|
1958
|
+
items = [FALLBACK];
|
|
1959
|
+
mapped[0] = createRoot((disposer) => {
|
|
1960
|
+
disposers[0] = disposer;
|
|
1961
|
+
return options.fallback();
|
|
1962
|
+
});
|
|
1963
|
+
len = 1;
|
|
1964
|
+
}
|
|
1965
|
+
} else if (len === 0) {
|
|
1966
|
+
mapped = new Array(newLen);
|
|
1967
|
+
for (j = 0;j < newLen; j++) {
|
|
1968
|
+
items[j] = newItems[j];
|
|
1969
|
+
mapped[j] = createRoot(mapper);
|
|
1970
|
+
}
|
|
1971
|
+
len = newLen;
|
|
1972
|
+
} else {
|
|
1973
|
+
temp = new Array(newLen);
|
|
1974
|
+
tempdisposers = new Array(newLen);
|
|
1975
|
+
indexes && (tempIndexes = new Array(newLen));
|
|
1976
|
+
for (start = 0, end = Math.min(len, newLen);start < end && items[start] === newItems[start]; start++)
|
|
1977
|
+
;
|
|
1978
|
+
for (end = len - 1, newEnd = newLen - 1;end >= start && newEnd >= start && items[end] === newItems[newEnd]; end--, newEnd--) {
|
|
1979
|
+
temp[newEnd] = mapped[end];
|
|
1980
|
+
tempdisposers[newEnd] = disposers[end];
|
|
1981
|
+
indexes && (tempIndexes[newEnd] = indexes[end]);
|
|
1982
|
+
}
|
|
1983
|
+
newIndices = new Map;
|
|
1984
|
+
newIndicesNext = new Array(newEnd + 1);
|
|
1985
|
+
for (j = newEnd;j >= start; j--) {
|
|
1986
|
+
item = newItems[j];
|
|
1987
|
+
i = newIndices.get(item);
|
|
1988
|
+
newIndicesNext[j] = i === undefined ? -1 : i;
|
|
1989
|
+
newIndices.set(item, j);
|
|
1990
|
+
}
|
|
1991
|
+
for (i = start;i <= end; i++) {
|
|
1992
|
+
item = items[i];
|
|
1993
|
+
j = newIndices.get(item);
|
|
1994
|
+
if (j !== undefined && j !== -1) {
|
|
1995
|
+
temp[j] = mapped[i];
|
|
1996
|
+
tempdisposers[j] = disposers[i];
|
|
1997
|
+
indexes && (tempIndexes[j] = indexes[i]);
|
|
1998
|
+
j = newIndicesNext[j];
|
|
1999
|
+
newIndices.set(item, j);
|
|
2000
|
+
} else
|
|
2001
|
+
disposers[i]();
|
|
2002
|
+
}
|
|
2003
|
+
for (j = start;j < newLen; j++) {
|
|
2004
|
+
if (j in temp) {
|
|
2005
|
+
mapped[j] = temp[j];
|
|
2006
|
+
disposers[j] = tempdisposers[j];
|
|
2007
|
+
if (indexes) {
|
|
2008
|
+
indexes[j] = tempIndexes[j];
|
|
2009
|
+
indexes[j](j);
|
|
2010
|
+
}
|
|
2011
|
+
} else
|
|
2012
|
+
mapped[j] = createRoot(mapper);
|
|
2013
|
+
}
|
|
2014
|
+
mapped = mapped.slice(0, len = newLen);
|
|
2015
|
+
items = newItems.slice(0);
|
|
2016
|
+
}
|
|
2017
|
+
return mapped;
|
|
2018
|
+
});
|
|
2019
|
+
function mapper(disposer) {
|
|
2020
|
+
disposers[j] = disposer;
|
|
2021
|
+
if (indexes) {
|
|
2022
|
+
const [s, set] = createSignal(j, {
|
|
2023
|
+
name: "index"
|
|
2024
|
+
});
|
|
2025
|
+
indexes[j] = set;
|
|
2026
|
+
return mapFn(newItems[j], s);
|
|
2027
|
+
}
|
|
2028
|
+
return mapFn(newItems[j]);
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
}
|
|
2032
|
+
function createComponent(Comp, props) {
|
|
2033
|
+
if (hydrationEnabled) {
|
|
2034
|
+
if (sharedConfig.context) {
|
|
2035
|
+
const c = sharedConfig.context;
|
|
2036
|
+
setHydrateContext(nextHydrateContext());
|
|
2037
|
+
const r = devComponent(Comp, props || {});
|
|
2038
|
+
setHydrateContext(c);
|
|
2039
|
+
return r;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
return devComponent(Comp, props || {});
|
|
2043
|
+
}
|
|
2044
|
+
function trueFn() {
|
|
2045
|
+
return true;
|
|
2046
|
+
}
|
|
2047
|
+
function resolveSource(s) {
|
|
2048
|
+
return !(s = typeof s === "function" ? s() : s) ? {} : s;
|
|
2049
|
+
}
|
|
2050
|
+
function resolveSources() {
|
|
2051
|
+
for (let i = 0, length = this.length;i < length; ++i) {
|
|
2052
|
+
const v = this[i]();
|
|
2053
|
+
if (v !== undefined)
|
|
2054
|
+
return v;
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
function mergeProps(...sources) {
|
|
2058
|
+
let proxy = false;
|
|
2059
|
+
for (let i = 0;i < sources.length; i++) {
|
|
2060
|
+
const s = sources[i];
|
|
2061
|
+
proxy = proxy || !!s && $PROXY in s;
|
|
2062
|
+
sources[i] = typeof s === "function" ? (proxy = true, createMemo(s)) : s;
|
|
2063
|
+
}
|
|
2064
|
+
if (SUPPORTS_PROXY && proxy) {
|
|
2065
|
+
return new Proxy({
|
|
2066
|
+
get(property) {
|
|
2067
|
+
for (let i = sources.length - 1;i >= 0; i--) {
|
|
2068
|
+
const v = resolveSource(sources[i])[property];
|
|
2069
|
+
if (v !== undefined)
|
|
2070
|
+
return v;
|
|
2071
|
+
}
|
|
2072
|
+
},
|
|
2073
|
+
has(property) {
|
|
2074
|
+
for (let i = sources.length - 1;i >= 0; i--) {
|
|
2075
|
+
if (property in resolveSource(sources[i]))
|
|
2076
|
+
return true;
|
|
2077
|
+
}
|
|
2078
|
+
return false;
|
|
2079
|
+
},
|
|
2080
|
+
keys() {
|
|
2081
|
+
const keys = [];
|
|
2082
|
+
for (let i = 0;i < sources.length; i++)
|
|
2083
|
+
keys.push(...Object.keys(resolveSource(sources[i])));
|
|
2084
|
+
return [...new Set(keys)];
|
|
2085
|
+
}
|
|
2086
|
+
}, propTraps);
|
|
2087
|
+
}
|
|
2088
|
+
const sourcesMap = {};
|
|
2089
|
+
const defined = Object.create(null);
|
|
2090
|
+
for (let i = sources.length - 1;i >= 0; i--) {
|
|
2091
|
+
const source = sources[i];
|
|
2092
|
+
if (!source)
|
|
2093
|
+
continue;
|
|
2094
|
+
const sourceKeys = Object.getOwnPropertyNames(source);
|
|
2095
|
+
for (let i2 = sourceKeys.length - 1;i2 >= 0; i2--) {
|
|
2096
|
+
const key = sourceKeys[i2];
|
|
2097
|
+
if (key === "__proto__" || key === "constructor")
|
|
2098
|
+
continue;
|
|
2099
|
+
const desc = Object.getOwnPropertyDescriptor(source, key);
|
|
2100
|
+
if (!defined[key]) {
|
|
2101
|
+
defined[key] = desc.get ? {
|
|
2102
|
+
enumerable: true,
|
|
2103
|
+
configurable: true,
|
|
2104
|
+
get: resolveSources.bind(sourcesMap[key] = [desc.get.bind(source)])
|
|
2105
|
+
} : desc.value !== undefined ? desc : undefined;
|
|
2106
|
+
} else {
|
|
2107
|
+
const sources2 = sourcesMap[key];
|
|
2108
|
+
if (sources2) {
|
|
2109
|
+
if (desc.get)
|
|
2110
|
+
sources2.push(desc.get.bind(source));
|
|
2111
|
+
else if (desc.value !== undefined)
|
|
2112
|
+
sources2.push(() => desc.value);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
const target = {};
|
|
2118
|
+
const definedKeys = Object.keys(defined);
|
|
2119
|
+
for (let i = definedKeys.length - 1;i >= 0; i--) {
|
|
2120
|
+
const key = definedKeys[i], desc = defined[key];
|
|
2121
|
+
if (desc && desc.get)
|
|
2122
|
+
Object.defineProperty(target, key, desc);
|
|
2123
|
+
else
|
|
2124
|
+
target[key] = desc ? desc.value : undefined;
|
|
2125
|
+
}
|
|
2126
|
+
return target;
|
|
2127
|
+
}
|
|
2128
|
+
function For(props) {
|
|
2129
|
+
const fallback = "fallback" in props && {
|
|
2130
|
+
fallback: () => props.fallback
|
|
2131
|
+
};
|
|
2132
|
+
return createMemo(mapArray(() => props.each, props.children, fallback || undefined), undefined, {
|
|
2133
|
+
name: "value"
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
function Show(props) {
|
|
2137
|
+
const keyed = props.keyed;
|
|
2138
|
+
const conditionValue = createMemo(() => props.when, undefined, {
|
|
2139
|
+
name: "condition value"
|
|
2140
|
+
});
|
|
2141
|
+
const condition = keyed ? conditionValue : createMemo(conditionValue, undefined, {
|
|
2142
|
+
equals: (a, b) => !a === !b,
|
|
2143
|
+
name: "condition"
|
|
2144
|
+
});
|
|
2145
|
+
return createMemo(() => {
|
|
2146
|
+
const c = condition();
|
|
2147
|
+
if (c) {
|
|
2148
|
+
const child = props.children;
|
|
2149
|
+
const fn = typeof child === "function" && child.length > 0;
|
|
2150
|
+
return fn ? untrack(() => child(keyed ? c : () => {
|
|
2151
|
+
if (!untrack(condition))
|
|
2152
|
+
throw narrowedError("Show");
|
|
2153
|
+
return conditionValue();
|
|
2154
|
+
})) : child;
|
|
2155
|
+
}
|
|
2156
|
+
return props.fallback;
|
|
2157
|
+
}, undefined, {
|
|
2158
|
+
name: "value"
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
function Switch(props) {
|
|
2162
|
+
const chs = children(() => props.children);
|
|
2163
|
+
const switchFunc = createMemo(() => {
|
|
2164
|
+
const ch = chs();
|
|
2165
|
+
const mps = Array.isArray(ch) ? ch : [ch];
|
|
2166
|
+
let func = () => {
|
|
2167
|
+
return;
|
|
2168
|
+
};
|
|
2169
|
+
for (let i = 0;i < mps.length; i++) {
|
|
2170
|
+
const index = i;
|
|
2171
|
+
const mp = mps[i];
|
|
2172
|
+
const prevFunc = func;
|
|
2173
|
+
const conditionValue = createMemo(() => prevFunc() ? undefined : mp.when, undefined, {
|
|
2174
|
+
name: "condition value"
|
|
2175
|
+
});
|
|
2176
|
+
const condition = mp.keyed ? conditionValue : createMemo(conditionValue, undefined, {
|
|
2177
|
+
equals: (a, b) => !a === !b,
|
|
2178
|
+
name: "condition"
|
|
2179
|
+
});
|
|
2180
|
+
func = () => prevFunc() || (condition() ? [index, conditionValue, mp] : undefined);
|
|
2181
|
+
}
|
|
2182
|
+
return func;
|
|
2183
|
+
});
|
|
2184
|
+
return createMemo(() => {
|
|
2185
|
+
const sel = switchFunc()();
|
|
2186
|
+
if (!sel)
|
|
2187
|
+
return props.fallback;
|
|
2188
|
+
const [index, conditionValue, mp] = sel;
|
|
2189
|
+
const child = mp.children;
|
|
2190
|
+
const fn = typeof child === "function" && child.length > 0;
|
|
2191
|
+
return fn ? untrack(() => child(mp.keyed ? conditionValue() : () => {
|
|
2192
|
+
if (untrack(switchFunc)()?.[0] !== index)
|
|
2193
|
+
throw narrowedError("Match");
|
|
2194
|
+
return conditionValue();
|
|
2195
|
+
})) : child;
|
|
2196
|
+
}, undefined, {
|
|
2197
|
+
name: "eval conditions"
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
function Match(props) {
|
|
2201
|
+
return props;
|
|
2202
|
+
}
|
|
2203
|
+
var sharedConfig, IS_DEV = true, equalFn = (a, b) => a === b, $PROXY, SUPPORTS_PROXY, $TRACK, $DEVCOMP, signalOptions, ERROR = null, runEffects, STALE = 1, PENDING = 2, UNOWNED, NO_INIT, Owner = null, Transition = null, Scheduler = null, ExternalSourceConfig = null, Listener = null, Updates = null, Effects = null, ExecCount = 0, DevHooks, transPending, setTransPending, SuspenseContext, FALLBACK, hydrationEnabled = false, propTraps, narrowedError = (name) => `Attempting to access a stale value from <${name}> that could possibly be undefined. This may occur because you are reading the accessor returned from the component at a time where it has already been unmounted. We recommend cleaning up any stale timers or async, or reading from the initial condition.`, DEV;
|
|
2204
|
+
var init_dev = __esm(() => {
|
|
2205
|
+
sharedConfig = {
|
|
2206
|
+
context: undefined,
|
|
2207
|
+
registry: undefined,
|
|
2208
|
+
effects: undefined,
|
|
2209
|
+
done: false,
|
|
2210
|
+
getContextId() {
|
|
2211
|
+
return getContextId(this.context.count);
|
|
2212
|
+
},
|
|
2213
|
+
getNextContextId() {
|
|
2214
|
+
return getContextId(this.context.count++);
|
|
2215
|
+
}
|
|
2216
|
+
};
|
|
2217
|
+
$PROXY = Symbol("solid-proxy");
|
|
2218
|
+
SUPPORTS_PROXY = typeof Proxy === "function";
|
|
2219
|
+
$TRACK = Symbol("solid-track");
|
|
2220
|
+
$DEVCOMP = Symbol("solid-dev-component");
|
|
2221
|
+
signalOptions = {
|
|
2222
|
+
equals: equalFn
|
|
2223
|
+
};
|
|
2224
|
+
runEffects = runQueue;
|
|
2225
|
+
UNOWNED = {
|
|
2226
|
+
owned: null,
|
|
2227
|
+
cleanups: null,
|
|
2228
|
+
context: null,
|
|
2229
|
+
owner: null
|
|
2230
|
+
};
|
|
2231
|
+
NO_INIT = {};
|
|
2232
|
+
DevHooks = {
|
|
2233
|
+
afterUpdate: null,
|
|
2234
|
+
afterCreateOwner: null,
|
|
2235
|
+
afterCreateSignal: null,
|
|
2236
|
+
afterRegisterGraph: null
|
|
2237
|
+
};
|
|
2238
|
+
[transPending, setTransPending] = /* @__PURE__ */ createSignal(false);
|
|
2239
|
+
FALLBACK = Symbol("fallback");
|
|
2240
|
+
propTraps = {
|
|
2241
|
+
get(_, property, receiver) {
|
|
2242
|
+
if (property === $PROXY)
|
|
2243
|
+
return receiver;
|
|
2244
|
+
return _.get(property);
|
|
2245
|
+
},
|
|
2246
|
+
has(_, property) {
|
|
2247
|
+
if (property === $PROXY)
|
|
2248
|
+
return true;
|
|
2249
|
+
return _.has(property);
|
|
2250
|
+
},
|
|
2251
|
+
set: trueFn,
|
|
2252
|
+
deleteProperty: trueFn,
|
|
2253
|
+
getOwnPropertyDescriptor(_, property) {
|
|
2254
|
+
return {
|
|
2255
|
+
configurable: true,
|
|
2256
|
+
enumerable: true,
|
|
2257
|
+
get() {
|
|
2258
|
+
return _.get(property);
|
|
2259
|
+
},
|
|
2260
|
+
set: trueFn,
|
|
2261
|
+
deleteProperty: trueFn
|
|
2262
|
+
};
|
|
2263
|
+
},
|
|
2264
|
+
ownKeys(_) {
|
|
2265
|
+
return _.keys();
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
DEV = {
|
|
2269
|
+
hooks: DevHooks,
|
|
2270
|
+
writeSignal,
|
|
2271
|
+
registerGraph
|
|
2272
|
+
};
|
|
2273
|
+
if (globalThis) {
|
|
2274
|
+
if (!globalThis.Solid$$)
|
|
2275
|
+
globalThis.Solid$$ = true;
|
|
2276
|
+
else
|
|
2277
|
+
console.warn("You appear to have multiple instances of Solid. This can lead to unexpected behavior.");
|
|
2278
|
+
}
|
|
2279
|
+
});
|
|
2280
|
+
|
|
2281
|
+
// src/engine/claude-settings.ts
|
|
2282
|
+
import { readFileSync } from "fs";
|
|
2283
|
+
import { homedir as homedir5 } from "os";
|
|
2284
|
+
import { join as join2 } from "path";
|
|
2285
|
+
function readClaudeSettings() {
|
|
2286
|
+
if (cached !== undefined)
|
|
2287
|
+
return cached;
|
|
2288
|
+
try {
|
|
2289
|
+
const raw = readFileSync(SETTINGS_PATH, "utf8");
|
|
2290
|
+
const parsed = JSON.parse(raw);
|
|
2291
|
+
if (!parsed || typeof parsed !== "object") {
|
|
2292
|
+
cached = null;
|
|
2293
|
+
return null;
|
|
2294
|
+
}
|
|
2295
|
+
const obj = parsed;
|
|
2296
|
+
const model = typeof obj.model === "string" && obj.model.length > 0 ? obj.model : undefined;
|
|
2297
|
+
cached = { model };
|
|
2298
|
+
return cached;
|
|
2299
|
+
} catch {
|
|
2300
|
+
cached = null;
|
|
2301
|
+
return null;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
function resolveDefaultModelId() {
|
|
2305
|
+
const settings = readClaudeSettings();
|
|
2306
|
+
if (settings?.model && settings.model.length > 0)
|
|
2307
|
+
return settings.model;
|
|
2308
|
+
return FALLBACK_DEFAULT_MODEL_ID;
|
|
2309
|
+
}
|
|
2310
|
+
var SETTINGS_PATH, cached, FALLBACK_DEFAULT_MODEL_ID = "claude-opus-4-7[1m]";
|
|
2311
|
+
var init_claude_settings = __esm(() => {
|
|
2312
|
+
SETTINGS_PATH = join2(homedir5(), ".claude", "settings.json");
|
|
2313
|
+
});
|
|
2314
|
+
|
|
2315
|
+
// src/env.ts
|
|
2316
|
+
import { homedir as homedir6 } from "os";
|
|
2317
|
+
import { join as join3 } from "path";
|
|
2318
|
+
function isDev() {
|
|
2319
|
+
return process.env.KOBE_DEV === "1";
|
|
2320
|
+
}
|
|
2321
|
+
function homeDir() {
|
|
2322
|
+
return process.env.KOBE_HOME_DIR ?? homedir6();
|
|
2323
|
+
}
|
|
2324
|
+
function kobeStateDir() {
|
|
2325
|
+
return join3(homeDir(), ".kobe");
|
|
2326
|
+
}
|
|
2327
|
+
function kvStatePath() {
|
|
2328
|
+
return join3(homeDir(), ".config", "kobe", "state.json");
|
|
2329
|
+
}
|
|
2330
|
+
function tmuxBin() {
|
|
2331
|
+
return process.env.KOBE_TMUX_BIN ?? "tmux";
|
|
2332
|
+
}
|
|
2333
|
+
var init_env = () => {};
|
|
2334
|
+
|
|
2335
|
+
// src/state/repos.ts
|
|
2336
|
+
var exports_repos = {};
|
|
2337
|
+
__export(exports_repos, {
|
|
2338
|
+
statePath: () => statePath,
|
|
2339
|
+
resolveRepoRoot: () => resolveRepoRoot,
|
|
2340
|
+
removeSavedRepo: () => removeSavedRepo,
|
|
2341
|
+
normalizeSavedRepos: () => normalizeSavedRepos,
|
|
2342
|
+
getSavedRepos: () => getSavedRepos,
|
|
2343
|
+
addSavedRepo: () => addSavedRepo
|
|
2344
|
+
});
|
|
2345
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2346
|
+
import { mkdirSync, readFileSync as readFileSync2, realpathSync, renameSync, writeFileSync } from "fs";
|
|
2347
|
+
import { dirname as dirname2 } from "path";
|
|
2348
|
+
function resolveRepoRoot(absPath) {
|
|
2349
|
+
const r = spawnSync2("git", ["rev-parse", "--show-toplevel"], {
|
|
2350
|
+
cwd: absPath,
|
|
2351
|
+
encoding: "utf8",
|
|
2352
|
+
shell: false
|
|
2353
|
+
});
|
|
2354
|
+
if (r.status !== 0)
|
|
2355
|
+
return absPath;
|
|
2356
|
+
const top = (r.stdout ?? "").trim();
|
|
2357
|
+
if (!top)
|
|
2358
|
+
return absPath;
|
|
2359
|
+
try {
|
|
2360
|
+
if (realpathSync(absPath) === realpathSync(top))
|
|
2361
|
+
return absPath;
|
|
2362
|
+
} catch {}
|
|
2363
|
+
return top;
|
|
2364
|
+
}
|
|
2365
|
+
function statePath() {
|
|
2366
|
+
return kvStatePath();
|
|
2367
|
+
}
|
|
2368
|
+
function load() {
|
|
2369
|
+
try {
|
|
2370
|
+
const text = readFileSync2(statePath(), "utf8");
|
|
2371
|
+
const parsed = JSON.parse(text);
|
|
2372
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2373
|
+
return parsed;
|
|
2374
|
+
}
|
|
2375
|
+
} catch {}
|
|
2376
|
+
return {};
|
|
2377
|
+
}
|
|
2378
|
+
function save(state) {
|
|
2379
|
+
const path4 = statePath();
|
|
2380
|
+
mkdirSync(dirname2(path4), { recursive: true });
|
|
2381
|
+
const tmp = `${path4}.tmp`;
|
|
2382
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2), "utf8");
|
|
2383
|
+
renameSync(tmp, path4);
|
|
2384
|
+
}
|
|
2385
|
+
function getSavedRepos() {
|
|
2386
|
+
const state = load();
|
|
2387
|
+
const raw = state.savedRepos;
|
|
2388
|
+
if (!Array.isArray(raw))
|
|
2389
|
+
return [];
|
|
2390
|
+
return raw.filter((s) => typeof s === "string");
|
|
2391
|
+
}
|
|
2392
|
+
function addSavedRepo(absPath) {
|
|
2393
|
+
const normalized = resolveRepoRoot(absPath);
|
|
2394
|
+
const state = load();
|
|
2395
|
+
const cur = getSavedRepos();
|
|
2396
|
+
if (cur.includes(normalized)) {
|
|
2397
|
+
return { added: false, path: normalized, total: cur.length };
|
|
2398
|
+
}
|
|
2399
|
+
state.savedRepos = [...cur, normalized];
|
|
2400
|
+
save(state);
|
|
2401
|
+
return { added: true, path: normalized, total: cur.length + 1 };
|
|
2402
|
+
}
|
|
2403
|
+
function normalizeSavedRepos() {
|
|
2404
|
+
const state = load();
|
|
2405
|
+
const cur = getSavedRepos();
|
|
2406
|
+
const seen = new Set;
|
|
2407
|
+
const next = [];
|
|
2408
|
+
let changed = false;
|
|
2409
|
+
for (const p of cur) {
|
|
2410
|
+
const top = resolveRepoRoot(p);
|
|
2411
|
+
if (top !== p)
|
|
2412
|
+
changed = true;
|
|
2413
|
+
if (seen.has(top)) {
|
|
2414
|
+
changed = true;
|
|
2415
|
+
continue;
|
|
2416
|
+
}
|
|
2417
|
+
seen.add(top);
|
|
2418
|
+
next.push(top);
|
|
2419
|
+
}
|
|
2420
|
+
if (!changed)
|
|
2421
|
+
return;
|
|
2422
|
+
state.savedRepos = next;
|
|
2423
|
+
save(state);
|
|
2424
|
+
}
|
|
2425
|
+
function removeSavedRepo(absPath) {
|
|
2426
|
+
const state = load();
|
|
2427
|
+
const cur = getSavedRepos();
|
|
2428
|
+
if (!cur.includes(absPath)) {
|
|
2429
|
+
return { removed: false, path: absPath, total: cur.length };
|
|
2430
|
+
}
|
|
2431
|
+
state.savedRepos = cur.filter((p) => p !== absPath);
|
|
2432
|
+
save(state);
|
|
2433
|
+
return { removed: true, path: absPath, total: cur.length - 1 };
|
|
2434
|
+
}
|
|
2435
|
+
var init_repos = __esm(() => {
|
|
2436
|
+
init_env();
|
|
2437
|
+
});
|
|
2438
|
+
|
|
2439
|
+
// src/types/task.ts
|
|
2440
|
+
function nextChatTabSeq(tabs) {
|
|
2441
|
+
let max = 0;
|
|
2442
|
+
for (const t of tabs)
|
|
2443
|
+
if (t.seq > max)
|
|
2444
|
+
max = t.seq;
|
|
2445
|
+
return max + 1;
|
|
2446
|
+
}
|
|
2447
|
+
var toTaskId = (id) => id;
|
|
2448
|
+
|
|
2449
|
+
// src/orchestrator/index/ulid.ts
|
|
2450
|
+
function encodeTime(now, len) {
|
|
2451
|
+
let out = "";
|
|
2452
|
+
let n = now;
|
|
2453
|
+
for (let i = len - 1;i >= 0; i--) {
|
|
2454
|
+
const mod = n % 32;
|
|
2455
|
+
out = ALPHABET[mod] + out;
|
|
2456
|
+
n = (n - mod) / 32;
|
|
2457
|
+
}
|
|
2458
|
+
return out;
|
|
2459
|
+
}
|
|
2460
|
+
function randomIndices(len) {
|
|
2461
|
+
const buf = new Uint8Array(len);
|
|
2462
|
+
crypto.getRandomValues(buf);
|
|
2463
|
+
const out = new Array(len);
|
|
2464
|
+
for (let i = 0;i < len; i++) {
|
|
2465
|
+
out[i] = (buf[i] ?? 0) & 31;
|
|
2466
|
+
}
|
|
2467
|
+
return out;
|
|
2468
|
+
}
|
|
2469
|
+
function incrementIndices(indices) {
|
|
2470
|
+
for (let i = indices.length - 1;i >= 0; i--) {
|
|
2471
|
+
const v = indices[i] ?? 0;
|
|
2472
|
+
if (v < 31) {
|
|
2473
|
+
indices[i] = v + 1;
|
|
2474
|
+
return true;
|
|
2475
|
+
}
|
|
2476
|
+
indices[i] = 0;
|
|
2477
|
+
}
|
|
2478
|
+
return false;
|
|
2479
|
+
}
|
|
2480
|
+
function indicesToString(indices) {
|
|
2481
|
+
let out = "";
|
|
2482
|
+
for (const idx of indices) {
|
|
2483
|
+
out += ALPHABET[idx] ?? "0";
|
|
2484
|
+
}
|
|
2485
|
+
return out;
|
|
2486
|
+
}
|
|
2487
|
+
function ulid(now = Date.now()) {
|
|
2488
|
+
let randIndices;
|
|
2489
|
+
if (now === lastTime) {
|
|
2490
|
+
const next = lastRand.slice();
|
|
2491
|
+
if (!incrementIndices(next)) {
|
|
2492
|
+
randIndices = randomIndices(RAND_LEN);
|
|
2493
|
+
} else {
|
|
2494
|
+
randIndices = next;
|
|
2495
|
+
}
|
|
2496
|
+
} else {
|
|
2497
|
+
randIndices = randomIndices(RAND_LEN);
|
|
2498
|
+
}
|
|
2499
|
+
lastTime = now;
|
|
2500
|
+
lastRand = randIndices;
|
|
2501
|
+
return encodeTime(now, TIME_LEN) + indicesToString(randIndices);
|
|
2502
|
+
}
|
|
2503
|
+
var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ", TIME_LEN = 10, RAND_LEN = 16, lastTime = -1, lastRand;
|
|
2504
|
+
var init_ulid = __esm(() => {
|
|
2505
|
+
lastRand = new Array(RAND_LEN).fill(0);
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2508
|
+
// src/orchestrator/metadata-suggester.ts
|
|
2509
|
+
import { spawn as spawn2 } from "child_process";
|
|
2510
|
+
|
|
2511
|
+
class MetadataSuggester {
|
|
2512
|
+
binaryPromise = null;
|
|
2513
|
+
async suggestBranchSlug(prompt) {
|
|
2514
|
+
return this.runOneShot(buildBranchInstruction, sanitizeKebabSlug, prompt);
|
|
2515
|
+
}
|
|
2516
|
+
async suggestWorktreeSlug(prompt) {
|
|
2517
|
+
return this.runOneShot(buildWorktreeInstruction, sanitizeKebabSlug, prompt);
|
|
2518
|
+
}
|
|
2519
|
+
async suggestTitle(prompt) {
|
|
2520
|
+
return this.runOneShot(buildTitleInstruction, sanitizeTitleText, prompt);
|
|
2521
|
+
}
|
|
2522
|
+
async resolveBinary() {
|
|
2523
|
+
if (!this.binaryPromise) {
|
|
2524
|
+
this.binaryPromise = findClaudeBinary().catch(() => null);
|
|
2525
|
+
}
|
|
2526
|
+
return this.binaryPromise;
|
|
2527
|
+
}
|
|
2528
|
+
async runOneShot(builder, sanitize, prompt) {
|
|
2529
|
+
const trimmed = prompt.trim();
|
|
2530
|
+
if (!trimmed)
|
|
2531
|
+
return null;
|
|
2532
|
+
const binary = await this.resolveBinary();
|
|
2533
|
+
if (!binary)
|
|
2534
|
+
return null;
|
|
2535
|
+
return new Promise((resolve) => {
|
|
2536
|
+
let proc;
|
|
2537
|
+
try {
|
|
2538
|
+
proc = spawn2(binary, ["-p", builder(trimmed)], {
|
|
2539
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2540
|
+
env: process.env
|
|
2541
|
+
});
|
|
2542
|
+
} catch {
|
|
2543
|
+
resolve(null);
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
let buf = "";
|
|
2547
|
+
let settled = false;
|
|
2548
|
+
const settle = (v) => {
|
|
2549
|
+
if (settled)
|
|
2550
|
+
return;
|
|
2551
|
+
settled = true;
|
|
2552
|
+
try {
|
|
2553
|
+
proc.kill();
|
|
2554
|
+
} catch {}
|
|
2555
|
+
resolve(v);
|
|
2556
|
+
};
|
|
2557
|
+
const timer = setTimeout(() => settle(null), SUGGESTION_TIMEOUT_MS);
|
|
2558
|
+
proc.stdout?.on("data", (chunk) => {
|
|
2559
|
+
buf += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
2560
|
+
});
|
|
2561
|
+
proc.on("error", () => {
|
|
2562
|
+
clearTimeout(timer);
|
|
2563
|
+
settle(null);
|
|
2564
|
+
});
|
|
2565
|
+
proc.on("close", () => {
|
|
2566
|
+
clearTimeout(timer);
|
|
2567
|
+
settle(sanitize(buf));
|
|
2568
|
+
});
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
function buildBranchInstruction(prompt) {
|
|
2573
|
+
return [
|
|
2574
|
+
"Generate a short git branch slug for this user task.",
|
|
2575
|
+
"Rules:",
|
|
2576
|
+
"- Lowercase, kebab-case, alphanumeric + hyphens only.",
|
|
2577
|
+
`- Max ${MAX_SLUG_LEN} characters.`,
|
|
2578
|
+
"- Action-oriented (e.g. fix-login-redirect, add-csv-export).",
|
|
2579
|
+
"- Reply with ONLY the slug, no other text, no quotes, no explanation.",
|
|
2580
|
+
"",
|
|
2581
|
+
`User task: ${prompt}`,
|
|
2582
|
+
"",
|
|
2583
|
+
"Branch slug:"
|
|
2584
|
+
].join(`
|
|
2585
|
+
`);
|
|
2586
|
+
}
|
|
2587
|
+
function buildWorktreeInstruction(prompt) {
|
|
2588
|
+
return [
|
|
2589
|
+
"Generate a short directory-name slug for a per-task git worktree.",
|
|
2590
|
+
"Rules:",
|
|
2591
|
+
"- Lowercase, kebab-case, alphanumeric + hyphens only.",
|
|
2592
|
+
`- Max ${MAX_SLUG_LEN} characters.`,
|
|
2593
|
+
"- Topic-oriented; describe the work area, not the action verb.",
|
|
2594
|
+
"- Reply with ONLY the slug, no other text, no quotes, no explanation.",
|
|
2595
|
+
"",
|
|
2596
|
+
`User task: ${prompt}`,
|
|
2597
|
+
"",
|
|
2598
|
+
"Worktree slug:"
|
|
2599
|
+
].join(`
|
|
2600
|
+
`);
|
|
2601
|
+
}
|
|
2602
|
+
function buildTitleInstruction(prompt) {
|
|
2603
|
+
return [
|
|
2604
|
+
"Generate a short sidebar title for this user task.",
|
|
2605
|
+
"Rules:",
|
|
2606
|
+
`- \u2264 ${MAX_TITLE_LEN} characters, single line.`,
|
|
2607
|
+
"- Sentence case, no trailing period.",
|
|
2608
|
+
`- Capture the essential action (e.g. "Fix login redirect", "Add CSV export to settings").`,
|
|
2609
|
+
`- Reply with ONLY the title, no quotes, no explanation, no leading "Title:".`,
|
|
2610
|
+
"",
|
|
2611
|
+
`User task: ${prompt}`,
|
|
2612
|
+
"",
|
|
2613
|
+
"Title:"
|
|
2614
|
+
].join(`
|
|
2615
|
+
`);
|
|
2616
|
+
}
|
|
2617
|
+
function sanitizeKebabSlug(raw) {
|
|
2618
|
+
const firstLine = raw.split(/\r?\n/).map((s) => s.trim()).find((s) => s.length > 0);
|
|
2619
|
+
if (!firstLine)
|
|
2620
|
+
return null;
|
|
2621
|
+
const cleaned = firstLine.toLowerCase().replace(/^["'`]+|["'`]+$/g, "").replace(/^(branch|slug|worktree)[:\s-]+/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, MAX_SLUG_LEN).replace(/-+$/g, "");
|
|
2622
|
+
return cleaned.length > 0 ? cleaned : null;
|
|
2623
|
+
}
|
|
2624
|
+
function sanitizeTitleText(raw) {
|
|
2625
|
+
const firstLine = raw.split(/\r?\n/).map((s) => s.trim()).find((s) => s.length > 0);
|
|
2626
|
+
if (!firstLine)
|
|
2627
|
+
return null;
|
|
2628
|
+
const cleaned = firstLine.replace(/^["'`]+|["'`]+$/g, "").replace(/^title[:\s-]+/i, "").replace(/[\s.!]+$/g, "").slice(0, MAX_TITLE_LEN).trim();
|
|
2629
|
+
return cleaned.length > 0 ? cleaned : null;
|
|
2630
|
+
}
|
|
2631
|
+
var SUGGESTION_TIMEOUT_MS = 30000, MAX_SLUG_LEN = 32, MAX_TITLE_LEN = 60;
|
|
2632
|
+
var init_metadata_suggester = __esm(() => {
|
|
2633
|
+
init_binary();
|
|
2634
|
+
});
|
|
2635
|
+
|
|
2636
|
+
// src/orchestrator/pending-input-broker.ts
|
|
2637
|
+
class InMemoryPendingInputBroker {
|
|
2638
|
+
buckets = new Map;
|
|
2639
|
+
requestTab = new Map;
|
|
2640
|
+
record(taskId, tabKey, requestId, payload) {
|
|
2641
|
+
let bucket = this.buckets.get(taskId);
|
|
2642
|
+
if (!bucket) {
|
|
2643
|
+
bucket = new Map;
|
|
2644
|
+
this.buckets.set(taskId, bucket);
|
|
2645
|
+
}
|
|
2646
|
+
if (bucket.has(requestId))
|
|
2647
|
+
return;
|
|
2648
|
+
bucket.set(requestId, payload);
|
|
2649
|
+
this.requestTab.set(requestId, tabKey);
|
|
2650
|
+
}
|
|
2651
|
+
resolve(taskId, requestId) {
|
|
2652
|
+
const bucket = this.buckets.get(taskId);
|
|
2653
|
+
const payload = bucket?.get(requestId);
|
|
2654
|
+
const tabKey = this.requestTab.get(requestId);
|
|
2655
|
+
if (!bucket || !payload || !tabKey)
|
|
2656
|
+
return null;
|
|
2657
|
+
bucket.delete(requestId);
|
|
2658
|
+
if (bucket.size === 0)
|
|
2659
|
+
this.buckets.delete(taskId);
|
|
2660
|
+
this.requestTab.delete(requestId);
|
|
2661
|
+
return { requestId, payload, tabKey };
|
|
2662
|
+
}
|
|
2663
|
+
snapshot(taskId) {
|
|
2664
|
+
const bucket = this.buckets.get(taskId);
|
|
2665
|
+
if (!bucket)
|
|
2666
|
+
return [];
|
|
2667
|
+
return Array.from(bucket.entries()).map(([requestId, payload]) => ({
|
|
2668
|
+
requestId,
|
|
2669
|
+
payload,
|
|
2670
|
+
tabKey: this.requestTab.get(requestId) ?? ""
|
|
2671
|
+
}));
|
|
2672
|
+
}
|
|
2673
|
+
awaitingTabKeys() {
|
|
2674
|
+
return this.requestTab.values();
|
|
2675
|
+
}
|
|
2676
|
+
clearForTask(taskId) {
|
|
2677
|
+
const bucket = this.buckets.get(taskId);
|
|
2678
|
+
if (!bucket)
|
|
2679
|
+
return;
|
|
2680
|
+
for (const requestId of bucket.keys())
|
|
2681
|
+
this.requestTab.delete(requestId);
|
|
2682
|
+
this.buckets.delete(taskId);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// src/orchestrator/pr/build.ts
|
|
2687
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2688
|
+
function git(cwd, args) {
|
|
2689
|
+
try {
|
|
2690
|
+
const out = spawnSync3("git", args.slice(), {
|
|
2691
|
+
cwd,
|
|
2692
|
+
encoding: "utf8",
|
|
2693
|
+
timeout: GIT_TIMEOUT_MS
|
|
2694
|
+
});
|
|
2695
|
+
if (out.error)
|
|
2696
|
+
return null;
|
|
2697
|
+
if (out.status !== 0)
|
|
2698
|
+
return null;
|
|
2699
|
+
return (out.stdout ?? "").trim();
|
|
2700
|
+
} catch {
|
|
2701
|
+
return null;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
function currentBranch(cwd) {
|
|
2705
|
+
const out = git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
2706
|
+
if (!out)
|
|
2707
|
+
return "HEAD";
|
|
2708
|
+
return out;
|
|
2709
|
+
}
|
|
2710
|
+
function targetBranch(cwd) {
|
|
2711
|
+
const out = git(cwd, ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"]);
|
|
2712
|
+
if (!out)
|
|
2713
|
+
return "main";
|
|
2714
|
+
return out.startsWith("origin/") ? out.slice("origin/".length) : out;
|
|
2715
|
+
}
|
|
2716
|
+
function hasUpstream(cwd) {
|
|
2717
|
+
const out = git(cwd, ["rev-parse", "--abbrev-ref", "@{u}"]);
|
|
2718
|
+
return out !== null && out.length > 0;
|
|
2719
|
+
}
|
|
2720
|
+
function dirtyCount(cwd) {
|
|
2721
|
+
const out = git(cwd, ["status", "--porcelain"]);
|
|
2722
|
+
if (!out)
|
|
2723
|
+
return 0;
|
|
2724
|
+
return out.split(`
|
|
2725
|
+
`).filter((line) => line.length > 0).length;
|
|
2726
|
+
}
|
|
2727
|
+
async function gatherPRState(worktreePath) {
|
|
2728
|
+
return {
|
|
2729
|
+
branch: currentBranch(worktreePath),
|
|
2730
|
+
targetBranch: targetBranch(worktreePath),
|
|
2731
|
+
hasUpstream: hasUpstream(worktreePath),
|
|
2732
|
+
dirtyCount: dirtyCount(worktreePath)
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
var GIT_TIMEOUT_MS = 5000;
|
|
2736
|
+
var init_build = () => {};
|
|
2737
|
+
|
|
2738
|
+
// src/orchestrator/pr/instructions.ts
|
|
2739
|
+
import { promises as fs } from "fs";
|
|
2740
|
+
import path4 from "path";
|
|
2741
|
+
function dirtyCountSentence(n) {
|
|
2742
|
+
if (n <= 0)
|
|
2743
|
+
return "There are no uncommitted changes.";
|
|
2744
|
+
if (n === 1)
|
|
2745
|
+
return "There is 1 uncommitted change.";
|
|
2746
|
+
return `There are ${n} uncommitted changes.`;
|
|
2747
|
+
}
|
|
2748
|
+
function upstreamSentence(hasUpstream2) {
|
|
2749
|
+
return hasUpstream2 ? "The current branch tracks an upstream." : "There is no upstream branch yet.";
|
|
2750
|
+
}
|
|
2751
|
+
function renderPRInstructions(template, state) {
|
|
2752
|
+
const replacements = {
|
|
2753
|
+
branch: state.branch,
|
|
2754
|
+
targetBranch: state.targetBranch,
|
|
2755
|
+
dirtyCountSentence: dirtyCountSentence(state.dirtyCount),
|
|
2756
|
+
upstreamSentence: upstreamSentence(state.hasUpstream)
|
|
2757
|
+
};
|
|
2758
|
+
return template.replace(/\{\{([a-zA-Z][a-zA-Z0-9_]*)\}\}/g, (match, key) => {
|
|
2759
|
+
if (Object.hasOwn(replacements, key)) {
|
|
2760
|
+
return replacements[key];
|
|
2761
|
+
}
|
|
2762
|
+
return match;
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
async function loadPRInstructionsTemplate(worktreePath) {
|
|
2766
|
+
if (!worktreePath)
|
|
2767
|
+
return DEFAULT_PR_INSTRUCTIONS_TEMPLATE;
|
|
2768
|
+
const file = path4.join(worktreePath, ".kobe", "pr-instructions.md");
|
|
2769
|
+
try {
|
|
2770
|
+
const text = await fs.readFile(file, "utf8");
|
|
2771
|
+
if (text.length === 0)
|
|
2772
|
+
return DEFAULT_PR_INSTRUCTIONS_TEMPLATE;
|
|
2773
|
+
return text;
|
|
2774
|
+
} catch {
|
|
2775
|
+
return DEFAULT_PR_INSTRUCTIONS_TEMPLATE;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
var DEFAULT_PR_INSTRUCTIONS_TEMPLATE = `The user likes the current state of the code.
|
|
2779
|
+
|
|
2780
|
+
{{dirtyCountSentence}}
|
|
2781
|
+
The current branch is {{branch}}.
|
|
2782
|
+
The target branch is {{targetBranch}}.
|
|
2783
|
+
|
|
2784
|
+
{{upstreamSentence}}
|
|
2785
|
+
The user requested a PR.
|
|
2786
|
+
|
|
2787
|
+
Follow these steps to create a PR:
|
|
2788
|
+
|
|
2789
|
+
- If you have any skills related to creating PRs, invoke them now. Instructions there should take precedence over these instructions.
|
|
2790
|
+
- Run \`git diff\` to review uncommitted changes.
|
|
2791
|
+
- Commit them. Follow any instructions the user gave you about writing commit messages.
|
|
2792
|
+
- Push to origin.
|
|
2793
|
+
- Use \`gh pr create --base {{targetBranch}}\` to create a PR onto the target branch. Keep the title under 80 characters. Keep the description under five sentences. Describe not just changes made in this session but ALL changes since the branch diverged from the target.
|
|
2794
|
+
|
|
2795
|
+
If any of these steps fail, ask the user for help.`;
|
|
2796
|
+
var init_instructions = () => {};
|
|
2797
|
+
|
|
2798
|
+
// src/orchestrator/pr/index.ts
|
|
2799
|
+
var exports_pr = {};
|
|
2800
|
+
__export(exports_pr, {
|
|
2801
|
+
renderPRInstructions: () => renderPRInstructions,
|
|
2802
|
+
loadPRInstructionsTemplate: () => loadPRInstructionsTemplate,
|
|
2803
|
+
gatherPRState: () => gatherPRState,
|
|
2804
|
+
DEFAULT_PR_INSTRUCTIONS_TEMPLATE: () => DEFAULT_PR_INSTRUCTIONS_TEMPLATE
|
|
2805
|
+
});
|
|
2806
|
+
var init_pr = __esm(() => {
|
|
2807
|
+
init_build();
|
|
2808
|
+
init_instructions();
|
|
2809
|
+
});
|
|
2810
|
+
|
|
2811
|
+
// src/orchestrator/session-pump.ts
|
|
2812
|
+
class SessionPump {
|
|
2813
|
+
env;
|
|
2814
|
+
constructor(env) {
|
|
2815
|
+
this.env = env;
|
|
2816
|
+
}
|
|
2817
|
+
async run(taskId, tabId, handle) {
|
|
2818
|
+
const tabKey = chatRunStateKey(taskId, tabId);
|
|
2819
|
+
let killedForInput = false;
|
|
2820
|
+
let terminalEvent = null;
|
|
2821
|
+
try {
|
|
2822
|
+
for await (const ev of this.env.engine.stream(handle)) {
|
|
2823
|
+
const inputReq = detectUserInputFromEngineEvent(ev);
|
|
2824
|
+
if (inputReq) {
|
|
2825
|
+
this.env.dispatch(taskId, tabId, ev);
|
|
2826
|
+
const requestId = this.env.nextRequestId();
|
|
2827
|
+
this.env.broker.record(taskId, tabKey, requestId, inputReq);
|
|
2828
|
+
this.env.onPendingInputChange?.();
|
|
2829
|
+
this.env.dispatch(taskId, tabId, {
|
|
2830
|
+
type: "user_input.request",
|
|
2831
|
+
requestId,
|
|
2832
|
+
payload: inputReq
|
|
2833
|
+
});
|
|
2834
|
+
killedForInput = true;
|
|
2835
|
+
try {
|
|
2836
|
+
await this.env.engine.stop(handle);
|
|
2837
|
+
} catch {}
|
|
2838
|
+
break;
|
|
2839
|
+
}
|
|
2840
|
+
if (ev.type === "done" || ev.type === "error") {
|
|
2841
|
+
terminalEvent = ev;
|
|
2842
|
+
continue;
|
|
2843
|
+
}
|
|
2844
|
+
this.env.dispatch(taskId, tabId, ev);
|
|
2845
|
+
}
|
|
2846
|
+
} finally {
|
|
2847
|
+
if (!killedForInput) {
|
|
2848
|
+
try {
|
|
2849
|
+
await this.env.engine.stop(handle);
|
|
2850
|
+
} catch {}
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
return { terminalEvent, killedForInput };
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
var init_session_pump = __esm(() => {
|
|
2857
|
+
init_core();
|
|
2858
|
+
});
|
|
2859
|
+
|
|
2860
|
+
// src/orchestrator/core.ts
|
|
2861
|
+
var exports_core = {};
|
|
2862
|
+
__export(exports_core, {
|
|
2863
|
+
summarizeWorktreeError: () => summarizeWorktreeError,
|
|
2864
|
+
renderUserInputResponsePrompt: () => renderUserInputResponsePrompt,
|
|
2865
|
+
detectUserInputFromEngineEvent: () => detectUserInputFromEngineEvent,
|
|
2866
|
+
deriveTitleFromPrompt: () => deriveTitleFromPrompt,
|
|
2867
|
+
chatRunStateKey: () => chatRunStateKey,
|
|
2868
|
+
TaskNotFoundError: () => TaskNotFoundError,
|
|
2869
|
+
TITLE_CHAR_CAP: () => TITLE_CHAR_CAP,
|
|
2870
|
+
PRPreconditionError: () => PRPreconditionError,
|
|
2871
|
+
PLACEHOLDER_TASK_TITLE: () => PLACEHOLDER_TASK_TITLE,
|
|
2872
|
+
Orchestrator: () => Orchestrator,
|
|
2873
|
+
IllegalTransitionError: () => IllegalTransitionError,
|
|
2874
|
+
ConcurrencyCapError: () => ConcurrencyCapError,
|
|
2875
|
+
CannotDeleteMainTaskError: () => CannotDeleteMainTaskError,
|
|
2876
|
+
CONCURRENCY_CAP: () => CONCURRENCY_CAP
|
|
2877
|
+
});
|
|
2878
|
+
function chatRunStateKey(taskId, tabId) {
|
|
2879
|
+
return `${taskId}:${tabId}`;
|
|
2880
|
+
}
|
|
2881
|
+
function tabKey(taskId, tabId) {
|
|
2882
|
+
return `${taskId}:${tabId}`;
|
|
2883
|
+
}
|
|
2884
|
+
function summarizeWorktreeError(raw, repo, baseRef) {
|
|
2885
|
+
const m = raw.toLowerCase();
|
|
2886
|
+
if (m.includes("invalid reference") || m.includes("unknown revision") || m.includes("not a valid object name")) {
|
|
2887
|
+
const ref = baseRef ?? "(none)";
|
|
2888
|
+
return `could not create worktree: base ref '${ref}' does not exist in ${repo}`;
|
|
2889
|
+
}
|
|
2890
|
+
if (m.includes("not a git repository") || m.includes("not in a git directory")) {
|
|
2891
|
+
return `could not create worktree: ${repo} is not a git repository`;
|
|
2892
|
+
}
|
|
2893
|
+
if (m.includes("permission denied") || m.includes("eacces")) {
|
|
2894
|
+
return `could not create worktree: permission denied writing into ${repo}/.claude/worktrees/`;
|
|
2895
|
+
}
|
|
2896
|
+
if (m.includes("already exists") || m.includes("refusing to hijack") || m.includes("is on branch")) {
|
|
2897
|
+
return `could not create worktree: a stale worktree already exists for this task (try removing it under ${repo}/.claude/worktrees/)`;
|
|
2898
|
+
}
|
|
2899
|
+
if (m.includes("enoent") || m.includes("does not exist")) {
|
|
2900
|
+
return `could not create worktree: ${repo} does not exist`;
|
|
2901
|
+
}
|
|
2902
|
+
const fatal = raw.match(/fatal:\s*([^\n]+)/i);
|
|
2903
|
+
if (fatal)
|
|
2904
|
+
return `could not create worktree: ${fatal[1]?.trim() ?? raw}`;
|
|
2905
|
+
return `could not create worktree: ${raw.trim()}`;
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
class Orchestrator {
|
|
2909
|
+
engine;
|
|
2910
|
+
store;
|
|
2911
|
+
worktrees;
|
|
2912
|
+
metadataSuggester;
|
|
2913
|
+
handles = new Map;
|
|
2914
|
+
subscribers = new Map;
|
|
2915
|
+
pumps = new Map;
|
|
2916
|
+
pendingInputBroker = new InMemoryPendingInputBroker;
|
|
2917
|
+
requestIdCounter = 0;
|
|
2918
|
+
sessionPump;
|
|
2919
|
+
pendingWorktreeOpts = new Map;
|
|
2920
|
+
tasksAcc;
|
|
2921
|
+
setTasks;
|
|
2922
|
+
unsubscribeStore;
|
|
2923
|
+
runStateAcc;
|
|
2924
|
+
setRunState;
|
|
2925
|
+
constructor(deps) {
|
|
2926
|
+
this.engine = deps.engine;
|
|
2927
|
+
this.store = deps.store;
|
|
2928
|
+
this.worktrees = deps.worktrees;
|
|
2929
|
+
this.metadataSuggester = deps.metadataSuggester ?? new MetadataSuggester;
|
|
2930
|
+
const [tasks, setTasks] = createSignal(this.store.list());
|
|
2931
|
+
this.tasksAcc = tasks;
|
|
2932
|
+
this.setTasks = (next) => setTasks(() => next);
|
|
2933
|
+
this.unsubscribeStore = this.store.subscribe((snapshot) => {
|
|
2934
|
+
this.setTasks(snapshot.slice());
|
|
2935
|
+
});
|
|
2936
|
+
const [runState, setRunState] = createSignal(new Map);
|
|
2937
|
+
this.runStateAcc = runState;
|
|
2938
|
+
this.setRunState = (next) => setRunState(() => next);
|
|
2939
|
+
this.sessionPump = new SessionPump({
|
|
2940
|
+
engine: this.engine,
|
|
2941
|
+
broker: this.pendingInputBroker,
|
|
2942
|
+
dispatch: (taskId, tabId, ev) => this.dispatchEvent(taskId, tabId, ev),
|
|
2943
|
+
nextRequestId: () => `req-${++this.requestIdCounter}`,
|
|
2944
|
+
onPendingInputChange: () => this.bumpRunState()
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2947
|
+
bumpRunState() {
|
|
2948
|
+
const next = new Map;
|
|
2949
|
+
for (const tabKey2 of this.pendingInputBroker.awaitingTabKeys()) {
|
|
2950
|
+
next.set(tabKey2, "awaiting_input");
|
|
2951
|
+
}
|
|
2952
|
+
for (const key of this.handles.keys()) {
|
|
2953
|
+
if (!next.has(key))
|
|
2954
|
+
next.set(key, "running");
|
|
2955
|
+
}
|
|
2956
|
+
this.setRunState(next);
|
|
2957
|
+
}
|
|
2958
|
+
chatRunStateSignal() {
|
|
2959
|
+
return this.runStateAcc;
|
|
2960
|
+
}
|
|
2961
|
+
tasksSignal() {
|
|
2962
|
+
return this.tasksAcc;
|
|
2963
|
+
}
|
|
2964
|
+
subscribeTasks(listener) {
|
|
2965
|
+
return this.store.subscribe(listener);
|
|
2966
|
+
}
|
|
2967
|
+
dispose() {
|
|
2968
|
+
this.unsubscribeStore();
|
|
2969
|
+
}
|
|
2970
|
+
listTasks() {
|
|
2971
|
+
return this.store.list();
|
|
2972
|
+
}
|
|
2973
|
+
getTask(id) {
|
|
2974
|
+
return this.store.get(id);
|
|
2975
|
+
}
|
|
2976
|
+
peekPendingInput(id) {
|
|
2977
|
+
return this.pendingInputBroker.snapshot(String(id));
|
|
2978
|
+
}
|
|
2979
|
+
async createTask(input) {
|
|
2980
|
+
if (!input.repo)
|
|
2981
|
+
throw new Error("createTask: repo is required");
|
|
2982
|
+
const explicitTitle = input.title?.trim() ?? "";
|
|
2983
|
+
const derivedTitle = explicitTitle || deriveTitleFromPrompt(input.prompt ?? "");
|
|
2984
|
+
const finalTitle = derivedTitle || PLACEHOLDER_TASK_TITLE;
|
|
2985
|
+
const created = await this.store.create({
|
|
2986
|
+
title: finalTitle,
|
|
2987
|
+
repo: input.repo,
|
|
2988
|
+
branch: "",
|
|
2989
|
+
worktreePath: "",
|
|
2990
|
+
sessionId: null,
|
|
2991
|
+
status: "backlog",
|
|
2992
|
+
archived: false
|
|
2993
|
+
});
|
|
2994
|
+
this.pendingWorktreeOpts.set(created.id, {
|
|
2995
|
+
branch: input.branch,
|
|
2996
|
+
baseRef: input.baseRef
|
|
2997
|
+
});
|
|
2998
|
+
return created;
|
|
2999
|
+
}
|
|
3000
|
+
async ensureMainTask(repo) {
|
|
3001
|
+
if (!repo)
|
|
3002
|
+
throw new Error("ensureMainTask: repo is required");
|
|
3003
|
+
const normalized = resolveRepoRoot(repo);
|
|
3004
|
+
const all = this.store.list();
|
|
3005
|
+
const candidates = all.filter((t) => t.kind === "main" && (t.repo === normalized || resolveRepoRoot(t.repo) === normalized));
|
|
3006
|
+
const winner = candidates.find((t) => t.repo === normalized) ?? candidates[0];
|
|
3007
|
+
if (winner) {
|
|
3008
|
+
for (const dup of candidates) {
|
|
3009
|
+
if (dup.id !== winner.id) {
|
|
3010
|
+
try {
|
|
3011
|
+
await this.deleteTask(dup.id);
|
|
3012
|
+
} catch (err) {
|
|
3013
|
+
console.error(`[kobe orchestrator] ensureMainTask: failed to remove duplicate ${dup.id}:`, err);
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
let existing = winner;
|
|
3018
|
+
if (existing.repo !== normalized || existing.worktreePath !== normalized) {
|
|
3019
|
+
existing = await this.store.update(existing.id, {
|
|
3020
|
+
repo: normalized,
|
|
3021
|
+
worktreePath: normalized,
|
|
3022
|
+
title: normalized.split("/").filter(Boolean).pop() ?? normalized
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
3025
|
+
if (existing.archived) {
|
|
3026
|
+
return await this.store.update(existing.id, { archived: false });
|
|
3027
|
+
}
|
|
3028
|
+
return existing;
|
|
3029
|
+
}
|
|
3030
|
+
const basename = normalized.split("/").filter(Boolean).pop() ?? normalized;
|
|
3031
|
+
return await this.store.create({
|
|
3032
|
+
title: basename,
|
|
3033
|
+
repo: normalized,
|
|
3034
|
+
branch: "",
|
|
3035
|
+
worktreePath: normalized,
|
|
3036
|
+
sessionId: null,
|
|
3037
|
+
status: "backlog",
|
|
3038
|
+
archived: false,
|
|
3039
|
+
kind: "main"
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
async ensureWorktree(task) {
|
|
3043
|
+
if (task.worktreePath)
|
|
3044
|
+
return task;
|
|
3045
|
+
const opts = this.pendingWorktreeOpts.get(task.id);
|
|
3046
|
+
const branch = opts?.branch ?? `kobe/tmp-${task.id.slice(-8).toLowerCase()}`;
|
|
3047
|
+
const baseRef = opts?.baseRef;
|
|
3048
|
+
let info;
|
|
3049
|
+
try {
|
|
3050
|
+
info = await this.worktrees.createForTask({
|
|
3051
|
+
repo: task.repo,
|
|
3052
|
+
taskId: task.id,
|
|
3053
|
+
branch,
|
|
3054
|
+
baseRef
|
|
3055
|
+
});
|
|
3056
|
+
} catch (err) {
|
|
3057
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3058
|
+
throw new Error(summarizeWorktreeError(message, task.repo, baseRef ?? null), { cause: err });
|
|
3059
|
+
}
|
|
3060
|
+
this.pendingWorktreeOpts.delete(task.id);
|
|
3061
|
+
return await this.store.update(task.id, {
|
|
3062
|
+
branch: info.branch,
|
|
3063
|
+
worktreePath: info.path
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
async maybeRenameTempBranch(taskId, tabId, prompt) {
|
|
3067
|
+
if (!prompt || prompt.trim().length === 0)
|
|
3068
|
+
return;
|
|
3069
|
+
const task = this.store.get(taskId);
|
|
3070
|
+
if (!task || !task.worktreePath)
|
|
3071
|
+
return;
|
|
3072
|
+
if (!task.branch.startsWith("kobe/tmp-"))
|
|
3073
|
+
return;
|
|
3074
|
+
this.dispatchEvent(taskId, tabId, {
|
|
3075
|
+
type: "system.info",
|
|
3076
|
+
text: "branch: choosing a name\u2026"
|
|
3077
|
+
});
|
|
3078
|
+
const slug = await this.metadataSuggester.suggestBranchSlug(prompt);
|
|
3079
|
+
if (!slug)
|
|
3080
|
+
return;
|
|
3081
|
+
const fresh = this.store.get(taskId);
|
|
3082
|
+
if (!fresh || !fresh.worktreePath)
|
|
3083
|
+
return;
|
|
3084
|
+
if (fresh.branch !== task.branch)
|
|
3085
|
+
return;
|
|
3086
|
+
const newBranch = `kobe/${slug}-${taskId.slice(-4).toLowerCase()}`;
|
|
3087
|
+
if (newBranch === fresh.branch)
|
|
3088
|
+
return;
|
|
3089
|
+
try {
|
|
3090
|
+
await this.worktrees.renameBranch(fresh.worktreePath, fresh.branch, newBranch);
|
|
3091
|
+
await this.store.update(taskId, { branch: newBranch });
|
|
3092
|
+
this.dispatchEvent(taskId, tabId, {
|
|
3093
|
+
type: "system.info",
|
|
3094
|
+
text: `branch: renamed to ${newBranch}`
|
|
3095
|
+
});
|
|
3096
|
+
} catch {}
|
|
3097
|
+
}
|
|
3098
|
+
async maybeUpgradeTitle(taskId, prompt) {
|
|
3099
|
+
if (!prompt || prompt.trim().length === 0)
|
|
3100
|
+
return;
|
|
3101
|
+
const task = this.store.get(taskId);
|
|
3102
|
+
if (!task)
|
|
3103
|
+
return;
|
|
3104
|
+
const derived = deriveTitleFromPrompt(prompt);
|
|
3105
|
+
if (!derived)
|
|
3106
|
+
return;
|
|
3107
|
+
if (task.title !== derived)
|
|
3108
|
+
return;
|
|
3109
|
+
const suggested = await this.metadataSuggester.suggestTitle(prompt);
|
|
3110
|
+
if (!suggested)
|
|
3111
|
+
return;
|
|
3112
|
+
if (suggested === derived)
|
|
3113
|
+
return;
|
|
3114
|
+
const fresh = this.store.get(taskId);
|
|
3115
|
+
if (!fresh)
|
|
3116
|
+
return;
|
|
3117
|
+
if (fresh.title !== derived)
|
|
3118
|
+
return;
|
|
3119
|
+
await this.store.update(taskId, { title: suggested });
|
|
3120
|
+
}
|
|
3121
|
+
async runTask(id, prompt, tabId) {
|
|
3122
|
+
let task = this.requireTask(id);
|
|
3123
|
+
if (task.status === "canceled") {
|
|
3124
|
+
throw new IllegalTransitionError(task.status, "in_progress", String(id));
|
|
3125
|
+
}
|
|
3126
|
+
const isMain = task.kind === "main";
|
|
3127
|
+
const isFirstAllocation = !isMain && !task.worktreePath;
|
|
3128
|
+
if (isFirstAllocation) {
|
|
3129
|
+
task = await this.ensureWorktree(task);
|
|
3130
|
+
const targetTabForInfo = this.resolveTab(task, tabId);
|
|
3131
|
+
this.dispatchEvent(task.id, targetTabForInfo.id, {
|
|
3132
|
+
type: "system.info",
|
|
3133
|
+
text: `worktree: ${task.worktreePath} (branch ${task.branch})`
|
|
3134
|
+
});
|
|
3135
|
+
}
|
|
3136
|
+
if (isFirstAllocation && prompt) {
|
|
3137
|
+
const renameTabId = this.resolveTab(task, tabId).id;
|
|
3138
|
+
this.maybeRenameTempBranch(task.id, renameTabId, prompt);
|
|
3139
|
+
}
|
|
3140
|
+
const targetTab = this.resolveTab(task, tabId);
|
|
3141
|
+
const key = tabKey(task.id, targetTab.id);
|
|
3142
|
+
if (this.handles.has(key) === false) {
|
|
3143
|
+
const running = this.countRunning();
|
|
3144
|
+
if (running >= CONCURRENCY_CAP) {
|
|
3145
|
+
throw new ConcurrencyCapError;
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
const promptToSend = prompt && prompt.length > 0 ? prompt : " ";
|
|
3149
|
+
if (prompt && prompt.trim().length > 0) {
|
|
3150
|
+
this.dispatchEvent(task.id, targetTab.id, { type: "user.inject", text: prompt });
|
|
3151
|
+
}
|
|
3152
|
+
const modelToUse = task.model ?? resolveDefaultModelId();
|
|
3153
|
+
let handle;
|
|
3154
|
+
if (targetTab.sessionId) {
|
|
3155
|
+
handle = await this.engine.resume(targetTab.sessionId, promptToSend, {
|
|
3156
|
+
cwd: task.worktreePath,
|
|
3157
|
+
env: { KOBE_RESUME_CWD: task.worktreePath },
|
|
3158
|
+
permissionMode: task.permissionMode,
|
|
3159
|
+
model: modelToUse
|
|
3160
|
+
});
|
|
3161
|
+
} else {
|
|
3162
|
+
handle = await this.engine.spawn(task.worktreePath, promptToSend, {
|
|
3163
|
+
permissionMode: task.permissionMode,
|
|
3164
|
+
model: modelToUse
|
|
3165
|
+
});
|
|
3166
|
+
await this.updateTab(task.id, targetTab.id, { sessionId: handle.sessionId });
|
|
3167
|
+
if (task.title === PLACEHOLDER_TASK_TITLE && prompt && prompt.trim().length > 0) {
|
|
3168
|
+
const derived = deriveTitleFromPrompt(prompt);
|
|
3169
|
+
if (derived)
|
|
3170
|
+
await this.store.update(task.id, { title: derived });
|
|
3171
|
+
}
|
|
3172
|
+
if (prompt && prompt.trim().length > 0) {
|
|
3173
|
+
this.maybeUpgradeTitle(task.id, prompt);
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
this.handles.set(key, handle);
|
|
3177
|
+
this.bumpRunState();
|
|
3178
|
+
if (task.status !== "in_progress") {
|
|
3179
|
+
await this.store.update(task.id, { status: "in_progress" });
|
|
3180
|
+
}
|
|
3181
|
+
const pump = this.runPumpAndCleanup(task.id, targetTab.id, handle);
|
|
3182
|
+
this.pumps.set(key, pump);
|
|
3183
|
+
pump.catch((err) => {
|
|
3184
|
+
this.dispatchEvent(task.id, targetTab.id, {
|
|
3185
|
+
type: "error",
|
|
3186
|
+
message: `pump failure: ${err instanceof Error ? err.message : String(err)}`
|
|
3187
|
+
});
|
|
3188
|
+
});
|
|
3189
|
+
}
|
|
3190
|
+
async requestPR(id) {
|
|
3191
|
+
const task = this.requireTask(id);
|
|
3192
|
+
if (task.status === "canceled") {
|
|
3193
|
+
throw new PRPreconditionError("Cannot create a PR for a canceled task.");
|
|
3194
|
+
}
|
|
3195
|
+
if (!task.worktreePath) {
|
|
3196
|
+
throw new PRPreconditionError("Task has no worktree yet \u2014 wait for setup to finish.");
|
|
3197
|
+
}
|
|
3198
|
+
if (!task.repo) {
|
|
3199
|
+
throw new PRPreconditionError("Task has no repo path; cannot resolve git state.");
|
|
3200
|
+
}
|
|
3201
|
+
const state = await gatherPRState(task.worktreePath);
|
|
3202
|
+
const template = await loadPRInstructionsTemplate(task.worktreePath);
|
|
3203
|
+
const prompt = renderPRInstructions(template, state);
|
|
3204
|
+
const activeTab = this.resolveTab(task);
|
|
3205
|
+
await this.runTask(task.id, prompt, activeTab.id);
|
|
3206
|
+
}
|
|
3207
|
+
async respondToInput(id, requestId, response) {
|
|
3208
|
+
const task = this.requireTask(id);
|
|
3209
|
+
const resolved = this.pendingInputBroker.resolve(task.id, requestId);
|
|
3210
|
+
if (!resolved)
|
|
3211
|
+
return;
|
|
3212
|
+
const pending = resolved.payload;
|
|
3213
|
+
this.bumpRunState();
|
|
3214
|
+
const tabId = task.activeTabId;
|
|
3215
|
+
this.dispatchEvent(task.id, tabId, { type: "user_input.resolved", requestId, response });
|
|
3216
|
+
const prompt = renderUserInputResponsePrompt(pending, response);
|
|
3217
|
+
if (!prompt)
|
|
3218
|
+
return;
|
|
3219
|
+
await this.runTask(task.id, prompt, tabId);
|
|
3220
|
+
}
|
|
3221
|
+
async interruptTask(id, tabId) {
|
|
3222
|
+
const task = this.requireTask(id);
|
|
3223
|
+
const targetTab = this.resolveTab(task, tabId);
|
|
3224
|
+
const key = tabKey(task.id, targetTab.id);
|
|
3225
|
+
const handle = this.handles.get(key);
|
|
3226
|
+
if (!handle)
|
|
3227
|
+
return;
|
|
3228
|
+
this.dispatchEvent(task.id, targetTab.id, {
|
|
3229
|
+
type: "system.info",
|
|
3230
|
+
text: "(turn interrupted \u2014 sending new prompt)"
|
|
3231
|
+
});
|
|
3232
|
+
try {
|
|
3233
|
+
await this.engine.stop(handle);
|
|
3234
|
+
} finally {
|
|
3235
|
+
this.handles.delete(key);
|
|
3236
|
+
this.bumpRunState();
|
|
3237
|
+
}
|
|
3238
|
+
this.dispatchEvent(task.id, targetTab.id, { type: "done" });
|
|
3239
|
+
}
|
|
3240
|
+
async pauseTask(id) {
|
|
3241
|
+
const task = this.requireTask(id);
|
|
3242
|
+
if (task.status !== "in_progress") {
|
|
3243
|
+
throw new IllegalTransitionError(task.status, "backlog", String(id));
|
|
3244
|
+
}
|
|
3245
|
+
await this.stopAllTabsForTask(task.id);
|
|
3246
|
+
await this.store.update(task.id, { status: "backlog" });
|
|
3247
|
+
}
|
|
3248
|
+
async archiveTask(id, status) {
|
|
3249
|
+
const task = this.requireTask(id);
|
|
3250
|
+
if (status !== "done" && status !== "canceled") {
|
|
3251
|
+
throw new IllegalTransitionError(task.status, status, String(id));
|
|
3252
|
+
}
|
|
3253
|
+
await this.stopAllTabsForTask(task.id);
|
|
3254
|
+
await this.store.archive(task.id, status);
|
|
3255
|
+
}
|
|
3256
|
+
async setArchived(id, archived) {
|
|
3257
|
+
const task = this.requireTask(id);
|
|
3258
|
+
const next = archived ?? !task.archived;
|
|
3259
|
+
if (task.archived === next)
|
|
3260
|
+
return;
|
|
3261
|
+
await this.store.update(task.id, { archived: next });
|
|
3262
|
+
}
|
|
3263
|
+
async setPermissionMode(id, mode) {
|
|
3264
|
+
const task = this.requireTask(id);
|
|
3265
|
+
if (task.permissionMode === mode)
|
|
3266
|
+
return;
|
|
3267
|
+
await this.store.update(task.id, { permissionMode: mode });
|
|
3268
|
+
}
|
|
3269
|
+
async setModel(id, model) {
|
|
3270
|
+
const task = this.requireTask(id);
|
|
3271
|
+
if (task.model === model)
|
|
3272
|
+
return;
|
|
3273
|
+
await this.store.update(task.id, { model });
|
|
3274
|
+
}
|
|
3275
|
+
async setTitle(id, title) {
|
|
3276
|
+
const task = this.requireTask(id);
|
|
3277
|
+
const trimmed = typeof title === "string" ? title.trim() : "";
|
|
3278
|
+
if (trimmed.length === 0) {
|
|
3279
|
+
throw new Error("setTitle: title is required (empty or whitespace-only rejected)");
|
|
3280
|
+
}
|
|
3281
|
+
if (task.title === trimmed)
|
|
3282
|
+
return;
|
|
3283
|
+
await this.store.update(task.id, { title: trimmed });
|
|
3284
|
+
}
|
|
3285
|
+
async setPinned(id, pinned) {
|
|
3286
|
+
const task = this.requireTask(id);
|
|
3287
|
+
if (task.kind === "main")
|
|
3288
|
+
return;
|
|
3289
|
+
const next = pinned ?? !task.pinned;
|
|
3290
|
+
if ((task.pinned ?? false) === next)
|
|
3291
|
+
return;
|
|
3292
|
+
await this.store.update(task.id, { pinned: next });
|
|
3293
|
+
}
|
|
3294
|
+
async setTabTitle(id, tabId, title) {
|
|
3295
|
+
const task = this.requireTask(id);
|
|
3296
|
+
const trimmed = typeof title === "string" ? title.trim() : "";
|
|
3297
|
+
if (trimmed.length === 0) {
|
|
3298
|
+
throw new Error("setTabTitle: title is required (empty or whitespace-only rejected)");
|
|
3299
|
+
}
|
|
3300
|
+
const idx = task.tabs.findIndex((t) => t.id === tabId);
|
|
3301
|
+
if (idx < 0) {
|
|
3302
|
+
throw new Error(`setTabTitle: tab ${tabId} not found on task ${task.id}`);
|
|
3303
|
+
}
|
|
3304
|
+
const current = task.tabs[idx];
|
|
3305
|
+
if (!current)
|
|
3306
|
+
return;
|
|
3307
|
+
if (current.title === trimmed)
|
|
3308
|
+
return;
|
|
3309
|
+
const nextTabs = task.tabs.map((t) => t.id === tabId ? { ...t, title: trimmed } : t);
|
|
3310
|
+
await this.store.update(task.id, { tabs: nextTabs });
|
|
3311
|
+
}
|
|
3312
|
+
async deleteTask(id) {
|
|
3313
|
+
const task = this.store.get(id);
|
|
3314
|
+
if (!task)
|
|
3315
|
+
return;
|
|
3316
|
+
if (task.kind === "main") {
|
|
3317
|
+
throw new CannotDeleteMainTaskError;
|
|
3318
|
+
}
|
|
3319
|
+
if (task.status === "in_progress") {
|
|
3320
|
+
try {
|
|
3321
|
+
await this.pauseTask(task.id);
|
|
3322
|
+
} catch (err) {
|
|
3323
|
+
console.error(`[kobe orchestrator] deleteTask: pauseTask failed for ${task.id}:`, err);
|
|
3324
|
+
this.handles.delete(task.id);
|
|
3325
|
+
this.bumpRunState();
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
if (task.worktreePath) {
|
|
3329
|
+
try {
|
|
3330
|
+
await this.worktrees.remove(task.worktreePath, { force: true });
|
|
3331
|
+
} catch (err) {
|
|
3332
|
+
console.error(`[kobe orchestrator] deleteTask: worktree remove failed for ${task.id}:`, err);
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
if (task.sessionId) {
|
|
3336
|
+
try {
|
|
3337
|
+
await this.engine.deleteHistory(task.sessionId);
|
|
3338
|
+
} catch (err) {
|
|
3339
|
+
console.error(`[kobe orchestrator] deleteTask: deleteHistory failed for ${task.id}:`, err);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
this.pendingInputBroker.clearForTask(task.id);
|
|
3343
|
+
await this.store.remove(task.id);
|
|
3344
|
+
}
|
|
3345
|
+
async readHistory(sessionId) {
|
|
3346
|
+
try {
|
|
3347
|
+
return await this.engine.readHistory(sessionId);
|
|
3348
|
+
} catch {
|
|
3349
|
+
return [];
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
async listSessions(id) {
|
|
3353
|
+
const task = this.requireTask(id);
|
|
3354
|
+
if (!task.worktreePath)
|
|
3355
|
+
return [];
|
|
3356
|
+
try {
|
|
3357
|
+
return await this.engine.listSessions(task.worktreePath);
|
|
3358
|
+
} catch {
|
|
3359
|
+
return [];
|
|
3360
|
+
}
|
|
3361
|
+
}
|
|
3362
|
+
async openSessionInTab(id, sessionId, opts = {}) {
|
|
3363
|
+
const task = this.requireTask(id);
|
|
3364
|
+
const existing = task.tabs.find((t) => t.sessionId === sessionId);
|
|
3365
|
+
if (existing) {
|
|
3366
|
+
await this.setActiveTab(task.id, existing.id);
|
|
3367
|
+
return existing.id;
|
|
3368
|
+
}
|
|
3369
|
+
const tab = {
|
|
3370
|
+
id: ulid(),
|
|
3371
|
+
sessionId,
|
|
3372
|
+
seq: nextChatTabSeq(task.tabs),
|
|
3373
|
+
createdAt: new Date().toISOString(),
|
|
3374
|
+
...opts.title ? { title: opts.title } : {}
|
|
3375
|
+
};
|
|
3376
|
+
await this.store.update(task.id, { tabs: [...task.tabs, tab], activeTabId: tab.id });
|
|
3377
|
+
return tab.id;
|
|
3378
|
+
}
|
|
3379
|
+
subscribeEvents(id, cb, tabId) {
|
|
3380
|
+
const task = this.store.get(id);
|
|
3381
|
+
const taskId = task?.id ?? id;
|
|
3382
|
+
const resolvedTabId = tabId ?? task?.activeTabId ?? String(taskId);
|
|
3383
|
+
const key = tabKey(taskId, resolvedTabId);
|
|
3384
|
+
let set = this.subscribers.get(key);
|
|
3385
|
+
if (!set) {
|
|
3386
|
+
set = new Set;
|
|
3387
|
+
this.subscribers.set(key, set);
|
|
3388
|
+
}
|
|
3389
|
+
set.add(cb);
|
|
3390
|
+
return () => {
|
|
3391
|
+
const cur = this.subscribers.get(key);
|
|
3392
|
+
if (!cur)
|
|
3393
|
+
return;
|
|
3394
|
+
cur.delete(cb);
|
|
3395
|
+
if (cur.size === 0)
|
|
3396
|
+
this.subscribers.delete(key);
|
|
3397
|
+
};
|
|
3398
|
+
}
|
|
3399
|
+
async createTab(id, opts = {}) {
|
|
3400
|
+
const task = this.requireTask(id);
|
|
3401
|
+
const tab = {
|
|
3402
|
+
id: ulid(),
|
|
3403
|
+
sessionId: null,
|
|
3404
|
+
seq: nextChatTabSeq(task.tabs),
|
|
3405
|
+
createdAt: new Date().toISOString(),
|
|
3406
|
+
...opts.title ? { title: opts.title } : {}
|
|
3407
|
+
};
|
|
3408
|
+
const tabs = [...task.tabs, tab];
|
|
3409
|
+
await this.store.update(task.id, { tabs });
|
|
3410
|
+
return tab;
|
|
3411
|
+
}
|
|
3412
|
+
async closeTab(id, tabId) {
|
|
3413
|
+
const task = this.requireTask(id);
|
|
3414
|
+
if (task.tabs.length <= 1) {
|
|
3415
|
+
throw new Error(`closeTab: refusing to close the last tab on task ${task.id}`);
|
|
3416
|
+
}
|
|
3417
|
+
const idx = task.tabs.findIndex((t) => t.id === tabId);
|
|
3418
|
+
if (idx < 0) {
|
|
3419
|
+
throw new Error(`closeTab: tab ${tabId} not found on task ${task.id}`);
|
|
3420
|
+
}
|
|
3421
|
+
await this.stopTab(task.id, tabId);
|
|
3422
|
+
const remaining = task.tabs.filter((t) => t.id !== tabId);
|
|
3423
|
+
let nextActive = task.activeTabId;
|
|
3424
|
+
if (task.activeTabId === tabId) {
|
|
3425
|
+
const prevIdx = Math.max(0, idx - 1);
|
|
3426
|
+
nextActive = remaining[prevIdx]?.id ?? remaining[0]?.id ?? "";
|
|
3427
|
+
}
|
|
3428
|
+
await this.store.update(task.id, { tabs: remaining, activeTabId: nextActive });
|
|
3429
|
+
return nextActive;
|
|
3430
|
+
}
|
|
3431
|
+
async setActiveTab(id, tabId) {
|
|
3432
|
+
const task = this.requireTask(id);
|
|
3433
|
+
if (!task.tabs.some((t) => t.id === tabId)) {
|
|
3434
|
+
throw new Error(`setActiveTab: tab ${tabId} not found on task ${task.id}`);
|
|
3435
|
+
}
|
|
3436
|
+
if (task.activeTabId === tabId)
|
|
3437
|
+
return;
|
|
3438
|
+
await this.store.update(task.id, { activeTabId: tabId });
|
|
3439
|
+
}
|
|
3440
|
+
async _waitForPumpsIdle() {
|
|
3441
|
+
const pumps = Array.from(this.pumps.values());
|
|
3442
|
+
await Promise.allSettled(pumps);
|
|
3443
|
+
}
|
|
3444
|
+
requireTask(id) {
|
|
3445
|
+
const task = this.store.get(id);
|
|
3446
|
+
if (!task)
|
|
3447
|
+
throw new TaskNotFoundError(String(id));
|
|
3448
|
+
return task;
|
|
3449
|
+
}
|
|
3450
|
+
resolveTab(task, tabId) {
|
|
3451
|
+
if (tabId) {
|
|
3452
|
+
const found = task.tabs.find((t) => t.id === tabId);
|
|
3453
|
+
if (!found)
|
|
3454
|
+
throw new Error(`tab not found on task ${task.id}: ${tabId}`);
|
|
3455
|
+
return found;
|
|
3456
|
+
}
|
|
3457
|
+
const active = task.tabs.find((t) => t.id === task.activeTabId) ?? task.tabs[0];
|
|
3458
|
+
if (!active) {
|
|
3459
|
+
throw new Error(`task ${task.id} has no tabs`);
|
|
3460
|
+
}
|
|
3461
|
+
return active;
|
|
3462
|
+
}
|
|
3463
|
+
async updateTab(taskId, tabId, patch) {
|
|
3464
|
+
const cur = this.store.get(taskId);
|
|
3465
|
+
if (!cur)
|
|
3466
|
+
return;
|
|
3467
|
+
const tabs = cur.tabs.map((t) => t.id === tabId ? { ...t, ...patch, id: t.id } : t);
|
|
3468
|
+
await this.store.update(taskId, { tabs });
|
|
3469
|
+
}
|
|
3470
|
+
async stopTab(taskId, tabId) {
|
|
3471
|
+
const key = tabKey(taskId, tabId);
|
|
3472
|
+
const handle = this.handles.get(key);
|
|
3473
|
+
if (!handle)
|
|
3474
|
+
return;
|
|
3475
|
+
try {
|
|
3476
|
+
await this.engine.stop(handle);
|
|
3477
|
+
} finally {
|
|
3478
|
+
this.handles.delete(key);
|
|
3479
|
+
this.bumpRunState();
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
async stopAllTabsForTask(taskId) {
|
|
3483
|
+
const prefix = `${taskId}:`;
|
|
3484
|
+
const keys = Array.from(this.handles.keys()).filter((k) => k.startsWith(prefix));
|
|
3485
|
+
for (const key of keys) {
|
|
3486
|
+
const handle = this.handles.get(key);
|
|
3487
|
+
if (!handle)
|
|
3488
|
+
continue;
|
|
3489
|
+
try {
|
|
3490
|
+
await this.engine.stop(handle);
|
|
3491
|
+
} catch {}
|
|
3492
|
+
this.handles.delete(key);
|
|
3493
|
+
}
|
|
3494
|
+
this.bumpRunState();
|
|
3495
|
+
}
|
|
3496
|
+
countRunning() {
|
|
3497
|
+
return this.handles.size;
|
|
3498
|
+
}
|
|
3499
|
+
dispatchEvent(taskId, tabId, ev) {
|
|
3500
|
+
const set = this.subscribers.get(tabKey(taskId, tabId));
|
|
3501
|
+
if (!set)
|
|
3502
|
+
return;
|
|
3503
|
+
for (const cb of set) {
|
|
3504
|
+
try {
|
|
3505
|
+
cb(ev);
|
|
3506
|
+
} catch (err) {
|
|
3507
|
+
console.error("[kobe orchestrator] subscriber threw:", err);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
async runPumpAndCleanup(taskId, tabId, handle) {
|
|
3512
|
+
const key = tabKey(taskId, tabId);
|
|
3513
|
+
const { terminalEvent, killedForInput } = await this.sessionPump.run(taskId, tabId, handle);
|
|
3514
|
+
this.handles.delete(key);
|
|
3515
|
+
this.pumps.delete(key);
|
|
3516
|
+
this.bumpRunState();
|
|
3517
|
+
const terminal = terminalEvent?.type === "error" ? "error" : terminalEvent ? "done" : null;
|
|
3518
|
+
if (terminal && !killedForInput) {
|
|
3519
|
+
const stillLive = Array.from(this.handles.keys()).some((k) => k.startsWith(`${taskId}:`));
|
|
3520
|
+
if (!stillLive) {
|
|
3521
|
+
try {
|
|
3522
|
+
await this.store.update(taskId, { status: terminal === "done" ? "done" : "error" });
|
|
3523
|
+
} catch {}
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
if (terminalEvent && !killedForInput) {
|
|
3527
|
+
this.dispatchEvent(taskId, tabId, terminalEvent);
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
function detectUserInputFromEngineEvent(ev) {
|
|
3532
|
+
if (ev.type !== "tool.start")
|
|
3533
|
+
return null;
|
|
3534
|
+
if (ev.name === "ExitPlanMode" || ev.name === "ExitPlanModeV2Tool") {
|
|
3535
|
+
const input = ev.input;
|
|
3536
|
+
if (!input || typeof input !== "object")
|
|
3537
|
+
return null;
|
|
3538
|
+
const obj = input;
|
|
3539
|
+
const plan = typeof obj.plan === "string" ? obj.plan : "";
|
|
3540
|
+
const filePath = typeof obj.filePath === "string" ? obj.filePath : null;
|
|
3541
|
+
return { kind: "approve_plan", plan, filePath };
|
|
3542
|
+
}
|
|
3543
|
+
if (ev.name === "AskUserQuestion") {
|
|
3544
|
+
return parseAskUserQuestionInput(ev.input);
|
|
3545
|
+
}
|
|
3546
|
+
return null;
|
|
3547
|
+
}
|
|
3548
|
+
function parseAskUserQuestionInput(input) {
|
|
3549
|
+
if (!input || typeof input !== "object")
|
|
3550
|
+
return null;
|
|
3551
|
+
const obj = input;
|
|
3552
|
+
if (!Array.isArray(obj.questions))
|
|
3553
|
+
return null;
|
|
3554
|
+
const out = [];
|
|
3555
|
+
for (const q of obj.questions) {
|
|
3556
|
+
if (!q || typeof q !== "object")
|
|
3557
|
+
continue;
|
|
3558
|
+
const qo = q;
|
|
3559
|
+
const question = typeof qo.question === "string" ? qo.question : "";
|
|
3560
|
+
if (!question)
|
|
3561
|
+
continue;
|
|
3562
|
+
const header = typeof qo.header === "string" ? qo.header : "";
|
|
3563
|
+
const multiSelect = qo.multiSelect === true;
|
|
3564
|
+
const opts = Array.isArray(qo.options) ? qo.options : [];
|
|
3565
|
+
const options = [];
|
|
3566
|
+
for (const o of opts) {
|
|
3567
|
+
if (!o || typeof o !== "object")
|
|
3568
|
+
continue;
|
|
3569
|
+
const oo = o;
|
|
3570
|
+
const label = typeof oo.label === "string" ? oo.label : "";
|
|
3571
|
+
if (!label)
|
|
3572
|
+
continue;
|
|
3573
|
+
const description = typeof oo.description === "string" ? oo.description : "";
|
|
3574
|
+
options.push({ label, description });
|
|
3575
|
+
}
|
|
3576
|
+
if (options.length === 0)
|
|
3577
|
+
continue;
|
|
3578
|
+
out.push({ question, header, multiSelect, options });
|
|
3579
|
+
}
|
|
3580
|
+
if (out.length === 0)
|
|
3581
|
+
return null;
|
|
3582
|
+
return { kind: "ask_question", questions: out };
|
|
3583
|
+
}
|
|
3584
|
+
function renderUserInputResponsePrompt(req, response) {
|
|
3585
|
+
if (req.kind === "approve_plan" && response.kind === "approve_plan") {
|
|
3586
|
+
if (response.approve) {
|
|
3587
|
+
return "Plan approved. Please proceed with the implementation as outlined.";
|
|
3588
|
+
}
|
|
3589
|
+
return "Plan rejected. Please reconsider the approach and present a revised plan.";
|
|
3590
|
+
}
|
|
3591
|
+
if (req.kind === "ask_question" && response.kind === "ask_question") {
|
|
3592
|
+
const lines = ["You asked:"];
|
|
3593
|
+
for (const q of req.questions) {
|
|
3594
|
+
const ans = response.answers[q.question];
|
|
3595
|
+
lines.push(`- ${q.question} \u2192 ${ans && ans.length > 0 ? ans : "(no answer)"}`);
|
|
3596
|
+
}
|
|
3597
|
+
lines.push("", "Please continue.");
|
|
3598
|
+
return lines.join(`
|
|
3599
|
+
`);
|
|
3600
|
+
}
|
|
3601
|
+
return "";
|
|
3602
|
+
}
|
|
3603
|
+
function deriveTitleFromPrompt(prompt) {
|
|
3604
|
+
if (typeof prompt !== "string")
|
|
3605
|
+
return "";
|
|
3606
|
+
const collapsed = prompt.replace(/\s+/g, " ").trim();
|
|
3607
|
+
if (collapsed.length === 0)
|
|
3608
|
+
return "";
|
|
3609
|
+
if (collapsed.length <= TITLE_CHAR_CAP)
|
|
3610
|
+
return collapsed;
|
|
3611
|
+
return `${collapsed.slice(0, TITLE_CHAR_CAP)}\u2026`;
|
|
3612
|
+
}
|
|
3613
|
+
var CONCURRENCY_CAP = 20, PLACEHOLDER_TASK_TITLE = "(new task)", IllegalTransitionError, ConcurrencyCapError, PRPreconditionError, TaskNotFoundError, CannotDeleteMainTaskError, TITLE_CHAR_CAP = 40;
|
|
3614
|
+
var init_core = __esm(() => {
|
|
3615
|
+
init_dev();
|
|
3616
|
+
init_claude_settings();
|
|
3617
|
+
init_repos();
|
|
3618
|
+
init_ulid();
|
|
3619
|
+
init_metadata_suggester();
|
|
3620
|
+
init_pr();
|
|
3621
|
+
init_session_pump();
|
|
3622
|
+
IllegalTransitionError = class IllegalTransitionError extends Error {
|
|
3623
|
+
from;
|
|
3624
|
+
to;
|
|
3625
|
+
taskId;
|
|
3626
|
+
constructor(from, to, taskId) {
|
|
3627
|
+
super(`illegal transition for task ${taskId}: ${from} -> ${to}`);
|
|
3628
|
+
this.from = from;
|
|
3629
|
+
this.to = to;
|
|
3630
|
+
this.taskId = taskId;
|
|
3631
|
+
this.name = "IllegalTransitionError";
|
|
3632
|
+
}
|
|
3633
|
+
};
|
|
3634
|
+
ConcurrencyCapError = class ConcurrencyCapError extends Error {
|
|
3635
|
+
constructor() {
|
|
3636
|
+
super(`concurrency cap reached: ${CONCURRENCY_CAP} tasks running`);
|
|
3637
|
+
this.name = "ConcurrencyCapError";
|
|
3638
|
+
}
|
|
3639
|
+
};
|
|
3640
|
+
PRPreconditionError = class PRPreconditionError extends Error {
|
|
3641
|
+
constructor(message) {
|
|
3642
|
+
super(message);
|
|
3643
|
+
this.name = "PRPreconditionError";
|
|
3644
|
+
}
|
|
3645
|
+
};
|
|
3646
|
+
TaskNotFoundError = class TaskNotFoundError extends Error {
|
|
3647
|
+
constructor(taskId) {
|
|
3648
|
+
super(`task not found: ${taskId}`);
|
|
3649
|
+
this.name = "TaskNotFoundError";
|
|
3650
|
+
}
|
|
3651
|
+
};
|
|
3652
|
+
CannotDeleteMainTaskError = class CannotDeleteMainTaskError extends Error {
|
|
3653
|
+
constructor() {
|
|
3654
|
+
super("cannot delete a main task; remove the repo from saved repos instead");
|
|
3655
|
+
this.name = "CannotDeleteMainTaskError";
|
|
3656
|
+
}
|
|
3657
|
+
};
|
|
3658
|
+
});
|
|
3659
|
+
|
|
3660
|
+
// src/orchestrator/index/store.ts
|
|
3661
|
+
import { mkdir as mkdir2, open, readFile as readFile3, rename, unlink as unlink3 } from "fs/promises";
|
|
3662
|
+
import { homedir as homedir7 } from "os";
|
|
3663
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
3664
|
+
|
|
3665
|
+
class TaskIndexStore {
|
|
3666
|
+
homeDir;
|
|
3667
|
+
kobeDir;
|
|
3668
|
+
path;
|
|
3669
|
+
tmpPath;
|
|
3670
|
+
cache = { version: 2, tasks: [] };
|
|
3671
|
+
loaded = false;
|
|
3672
|
+
listeners = new Set;
|
|
3673
|
+
saveChain = Promise.resolve();
|
|
3674
|
+
constructor(options = {}) {
|
|
3675
|
+
this.homeDir = options.homeDir ?? homedir7();
|
|
3676
|
+
this.kobeDir = join4(this.homeDir, ".kobe");
|
|
3677
|
+
this.path = join4(this.kobeDir, "tasks.json");
|
|
3678
|
+
this.tmpPath = `${this.path}.tmp`;
|
|
3679
|
+
}
|
|
3680
|
+
subscribe(listener) {
|
|
3681
|
+
this.listeners.add(listener);
|
|
3682
|
+
if (this.loaded) {
|
|
3683
|
+
try {
|
|
3684
|
+
listener(this.cache.tasks.slice());
|
|
3685
|
+
} catch (err) {
|
|
3686
|
+
console.error("[kobe TaskIndexStore] listener threw on subscribe:", err);
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
return () => {
|
|
3690
|
+
this.listeners.delete(listener);
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
get filePath() {
|
|
3694
|
+
return this.path;
|
|
3695
|
+
}
|
|
3696
|
+
get stateDir() {
|
|
3697
|
+
return this.kobeDir;
|
|
3698
|
+
}
|
|
3699
|
+
async load() {
|
|
3700
|
+
let raw;
|
|
3701
|
+
try {
|
|
3702
|
+
raw = await readFile3(this.path, "utf8");
|
|
3703
|
+
} catch (err) {
|
|
3704
|
+
const code = err.code;
|
|
3705
|
+
if (code === "ENOENT") {
|
|
3706
|
+
this.cache = { version: 2, tasks: [] };
|
|
3707
|
+
this.loaded = true;
|
|
3708
|
+
this.notifyListeners();
|
|
3709
|
+
return this.snapshot();
|
|
3710
|
+
}
|
|
3711
|
+
throw err;
|
|
3712
|
+
}
|
|
3713
|
+
let parsed;
|
|
3714
|
+
try {
|
|
3715
|
+
parsed = JSON.parse(raw);
|
|
3716
|
+
} catch (err) {
|
|
3717
|
+
console.warn(`[kobe] tasks.json at ${this.path} is corrupted (${err.message}); recovering with empty index. The stale file is left in place.`);
|
|
3718
|
+
this.cache = { version: 2, tasks: [] };
|
|
3719
|
+
this.loaded = true;
|
|
3720
|
+
this.notifyListeners();
|
|
3721
|
+
return this.snapshot();
|
|
3722
|
+
}
|
|
3723
|
+
this.cache = normalizeIndex(parsed, this.path);
|
|
3724
|
+
this.loaded = true;
|
|
3725
|
+
this.notifyListeners();
|
|
3726
|
+
return this.snapshot();
|
|
3727
|
+
}
|
|
3728
|
+
async save() {
|
|
3729
|
+
this.assertLoaded();
|
|
3730
|
+
const next = this.saveChain.then(() => this.doSave());
|
|
3731
|
+
this.saveChain = next.catch(() => {});
|
|
3732
|
+
return next;
|
|
3733
|
+
}
|
|
3734
|
+
async doSave() {
|
|
3735
|
+
await mkdir2(dirname3(this.path), { recursive: true });
|
|
3736
|
+
const payload = this.snapshot();
|
|
3737
|
+
const json = `${JSON.stringify(payload, null, 2)}
|
|
3738
|
+
`;
|
|
3739
|
+
const handle = await open(this.tmpPath, "w", 420);
|
|
3740
|
+
try {
|
|
3741
|
+
await handle.writeFile(json, "utf8");
|
|
3742
|
+
await handle.sync();
|
|
3743
|
+
} finally {
|
|
3744
|
+
await handle.close();
|
|
3745
|
+
}
|
|
3746
|
+
await rename(this.tmpPath, this.path);
|
|
3747
|
+
}
|
|
3748
|
+
get(id) {
|
|
3749
|
+
this.assertLoaded();
|
|
3750
|
+
return this.cache.tasks.find((t) => t.id === id);
|
|
3751
|
+
}
|
|
3752
|
+
list() {
|
|
3753
|
+
this.assertLoaded();
|
|
3754
|
+
return this.cache.tasks.slice();
|
|
3755
|
+
}
|
|
3756
|
+
async create(partial) {
|
|
3757
|
+
this.assertLoaded();
|
|
3758
|
+
const now = new Date().toISOString();
|
|
3759
|
+
const { tabs: tabsIn, activeTabId: activeIn, sessionId, ...rest } = partial;
|
|
3760
|
+
let tabs;
|
|
3761
|
+
let activeTabId;
|
|
3762
|
+
if (tabsIn && tabsIn.length > 0) {
|
|
3763
|
+
tabs = tabsIn;
|
|
3764
|
+
activeTabId = activeIn && tabsIn.some((t) => t.id === activeIn) ? activeIn : tabsIn[0]?.id ?? "";
|
|
3765
|
+
} else {
|
|
3766
|
+
const tabId = ulid();
|
|
3767
|
+
tabs = [{ id: tabId, sessionId: sessionId ?? null, seq: 1, createdAt: now }];
|
|
3768
|
+
activeTabId = tabId;
|
|
3769
|
+
}
|
|
3770
|
+
const firstSession = tabs[0]?.sessionId ?? null;
|
|
3771
|
+
const task = {
|
|
3772
|
+
archived: false,
|
|
3773
|
+
...rest,
|
|
3774
|
+
sessionId: firstSession,
|
|
3775
|
+
tabs,
|
|
3776
|
+
activeTabId,
|
|
3777
|
+
id: toTaskId(ulid()),
|
|
3778
|
+
createdAt: now,
|
|
3779
|
+
updatedAt: now
|
|
3780
|
+
};
|
|
3781
|
+
this.cache.tasks.push(task);
|
|
3782
|
+
await this.save();
|
|
3783
|
+
this.notifyListeners();
|
|
3784
|
+
return task;
|
|
3785
|
+
}
|
|
3786
|
+
async update(id, patch) {
|
|
3787
|
+
this.assertLoaded();
|
|
3788
|
+
const idx = this.cache.tasks.findIndex((t) => t.id === id);
|
|
3789
|
+
if (idx < 0) {
|
|
3790
|
+
throw new Error(`task not found: ${id}`);
|
|
3791
|
+
}
|
|
3792
|
+
const existing = this.cache.tasks[idx];
|
|
3793
|
+
if (!existing) {
|
|
3794
|
+
throw new Error(`task not found: ${id}`);
|
|
3795
|
+
}
|
|
3796
|
+
const { id: _ignoredId, createdAt: _ignoredCreatedAt, ...mutable } = patch;
|
|
3797
|
+
const sessionIdPatched = "sessionId" in patch;
|
|
3798
|
+
const tabsPatched = "tabs" in patch;
|
|
3799
|
+
let mergedTabs = "tabs" in mutable && Array.isArray(mutable.tabs) ? mutable.tabs : existing.tabs;
|
|
3800
|
+
if (sessionIdPatched && !tabsPatched) {
|
|
3801
|
+
const newSessionId = patch.sessionId ?? null;
|
|
3802
|
+
const activeId = mutable.activeTabId ?? existing.activeTabId;
|
|
3803
|
+
mergedTabs = mergedTabs.map((t) => t.id === activeId ? { ...t, sessionId: newSessionId } : t);
|
|
3804
|
+
}
|
|
3805
|
+
const merged = {
|
|
3806
|
+
...existing,
|
|
3807
|
+
...mutable,
|
|
3808
|
+
tabs: mergedTabs,
|
|
3809
|
+
id: existing.id,
|
|
3810
|
+
createdAt: existing.createdAt,
|
|
3811
|
+
updatedAt: new Date().toISOString()
|
|
3812
|
+
};
|
|
3813
|
+
const activeTab = merged.tabs.find((t) => t.id === merged.activeTabId) ?? merged.tabs[0];
|
|
3814
|
+
const next = activeTab && activeTab.sessionId !== merged.sessionId ? { ...merged, sessionId: activeTab.sessionId } : merged;
|
|
3815
|
+
this.cache.tasks[idx] = next;
|
|
3816
|
+
await this.save();
|
|
3817
|
+
this.notifyListeners();
|
|
3818
|
+
return next;
|
|
3819
|
+
}
|
|
3820
|
+
async archive(id, status = "done") {
|
|
3821
|
+
return this.update(id, { status });
|
|
3822
|
+
}
|
|
3823
|
+
async remove(id) {
|
|
3824
|
+
this.assertLoaded();
|
|
3825
|
+
const idx = this.cache.tasks.findIndex((t) => t.id === id);
|
|
3826
|
+
if (idx < 0)
|
|
3827
|
+
return;
|
|
3828
|
+
this.cache.tasks.splice(idx, 1);
|
|
3829
|
+
await this.save();
|
|
3830
|
+
this.notifyListeners();
|
|
3831
|
+
}
|
|
3832
|
+
async _unlinkForTests() {
|
|
3833
|
+
try {
|
|
3834
|
+
await unlink3(this.path);
|
|
3835
|
+
} catch (err) {
|
|
3836
|
+
if (err.code !== "ENOENT")
|
|
3837
|
+
throw err;
|
|
3838
|
+
}
|
|
3839
|
+
try {
|
|
3840
|
+
await unlink3(this.tmpPath);
|
|
3841
|
+
} catch (err) {
|
|
3842
|
+
if (err.code !== "ENOENT")
|
|
3843
|
+
throw err;
|
|
3844
|
+
}
|
|
3845
|
+
this.cache = { version: 2, tasks: [] };
|
|
3846
|
+
this.loaded = false;
|
|
3847
|
+
}
|
|
3848
|
+
assertLoaded() {
|
|
3849
|
+
if (!this.loaded) {
|
|
3850
|
+
throw new Error("TaskIndexStore: call load() before any other method");
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
snapshot() {
|
|
3854
|
+
return {
|
|
3855
|
+
version: 2,
|
|
3856
|
+
tasks: this.cache.tasks.slice()
|
|
3857
|
+
};
|
|
3858
|
+
}
|
|
3859
|
+
notifyListeners() {
|
|
3860
|
+
if (this.listeners.size === 0)
|
|
3861
|
+
return;
|
|
3862
|
+
const snapshot = this.cache.tasks.slice();
|
|
3863
|
+
for (const listener of this.listeners) {
|
|
3864
|
+
try {
|
|
3865
|
+
listener(snapshot);
|
|
3866
|
+
} catch (err) {
|
|
3867
|
+
console.error("[kobe TaskIndexStore] listener threw on notify:", err);
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
function normalizeIndex(parsed, source) {
|
|
3873
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3874
|
+
console.warn(`[kobe] tasks.json at ${source} is not an object; recovering with empty index.`);
|
|
3875
|
+
return { version: 2, tasks: [] };
|
|
3876
|
+
}
|
|
3877
|
+
const obj = parsed;
|
|
3878
|
+
const version = obj.version;
|
|
3879
|
+
if (version !== undefined && version !== 1 && version !== 2) {
|
|
3880
|
+
console.warn(`[kobe] tasks.json at ${source} has unsupported version=${String(version)}; recovering with empty index.`);
|
|
3881
|
+
return { version: 2, tasks: [] };
|
|
3882
|
+
}
|
|
3883
|
+
const rawTasks = Array.isArray(obj.tasks) ? obj.tasks : [];
|
|
3884
|
+
const tasks = [];
|
|
3885
|
+
for (const entry of rawTasks) {
|
|
3886
|
+
const task = coerceTask(entry);
|
|
3887
|
+
if (task)
|
|
3888
|
+
tasks.push(task);
|
|
3889
|
+
else {
|
|
3890
|
+
console.warn(`[kobe] dropping malformed task entry from ${source}: ${JSON.stringify(entry)}`);
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
return { version: 2, tasks };
|
|
3894
|
+
}
|
|
3895
|
+
function coerceTask(value) {
|
|
3896
|
+
if (!value || typeof value !== "object")
|
|
3897
|
+
return null;
|
|
3898
|
+
const v = value;
|
|
3899
|
+
if (typeof v.id !== "string" || typeof v.title !== "string" || typeof v.repo !== "string" || typeof v.branch !== "string" || typeof v.worktreePath !== "string" || !(v.sessionId === null || typeof v.sessionId === "string" || v.sessionId === undefined) || typeof v.status !== "string" || typeof v.createdAt !== "string" || typeof v.updatedAt !== "string") {
|
|
3900
|
+
return null;
|
|
3901
|
+
}
|
|
3902
|
+
if (!isTaskStatus(v.status))
|
|
3903
|
+
return null;
|
|
3904
|
+
const legacySessionId = (v.sessionId === undefined ? null : v.sessionId) ?? null;
|
|
3905
|
+
const rawTabs = Array.isArray(v.tabs) ? v.tabs : null;
|
|
3906
|
+
let tabs = null;
|
|
3907
|
+
if (rawTabs) {
|
|
3908
|
+
tabs = [];
|
|
3909
|
+
let nextSeqForMissing = 1;
|
|
3910
|
+
for (const t of rawTabs) {
|
|
3911
|
+
if (!t || typeof t !== "object")
|
|
3912
|
+
continue;
|
|
3913
|
+
const tt = t;
|
|
3914
|
+
if (typeof tt.id !== "string")
|
|
3915
|
+
continue;
|
|
3916
|
+
if (!(tt.sessionId === null || typeof tt.sessionId === "string"))
|
|
3917
|
+
continue;
|
|
3918
|
+
if (typeof tt.createdAt !== "string")
|
|
3919
|
+
continue;
|
|
3920
|
+
const persistedSeq = typeof tt.seq === "number" && Number.isFinite(tt.seq) && tt.seq > 0 ? tt.seq : null;
|
|
3921
|
+
const seq = persistedSeq ?? nextSeqForMissing++;
|
|
3922
|
+
if (persistedSeq !== null && persistedSeq >= nextSeqForMissing) {
|
|
3923
|
+
nextSeqForMissing = persistedSeq + 1;
|
|
3924
|
+
}
|
|
3925
|
+
const tab = {
|
|
3926
|
+
id: tt.id,
|
|
3927
|
+
sessionId: tt.sessionId ?? null,
|
|
3928
|
+
seq,
|
|
3929
|
+
createdAt: tt.createdAt,
|
|
3930
|
+
...typeof tt.title === "string" ? { title: tt.title } : {}
|
|
3931
|
+
};
|
|
3932
|
+
tabs.push(tab);
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
let finalTabs;
|
|
3936
|
+
let finalActive;
|
|
3937
|
+
if (tabs && tabs.length > 0) {
|
|
3938
|
+
finalTabs = tabs;
|
|
3939
|
+
const persisted = typeof v.activeTabId === "string" ? v.activeTabId : null;
|
|
3940
|
+
finalActive = persisted && tabs.some((t) => t.id === persisted) ? persisted : tabs[0]?.id ?? "";
|
|
3941
|
+
} else {
|
|
3942
|
+
const synthesizedId = ulid();
|
|
3943
|
+
finalTabs = [{ id: synthesizedId, sessionId: legacySessionId, seq: 1, createdAt: v.createdAt }];
|
|
3944
|
+
finalActive = synthesizedId;
|
|
3945
|
+
}
|
|
3946
|
+
const activeTab = finalTabs.find((t) => t.id === finalActive) ?? finalTabs[0];
|
|
3947
|
+
const aliasSessionId = activeTab?.sessionId ?? legacySessionId;
|
|
3948
|
+
return {
|
|
3949
|
+
id: toTaskId(v.id),
|
|
3950
|
+
title: v.title,
|
|
3951
|
+
repo: v.repo,
|
|
3952
|
+
branch: v.branch,
|
|
3953
|
+
worktreePath: v.worktreePath,
|
|
3954
|
+
sessionId: aliasSessionId,
|
|
3955
|
+
tabs: finalTabs,
|
|
3956
|
+
activeTabId: finalActive,
|
|
3957
|
+
status: v.status,
|
|
3958
|
+
archived: typeof v.archived === "boolean" ? v.archived : false,
|
|
3959
|
+
pinned: typeof v.pinned === "boolean" ? v.pinned : false,
|
|
3960
|
+
kind: v.kind === "main" ? "main" : "task",
|
|
3961
|
+
permissionMode: isPermissionMode(v.permissionMode) ? v.permissionMode : undefined,
|
|
3962
|
+
model: typeof v.model === "string" ? v.model : undefined,
|
|
3963
|
+
createdAt: v.createdAt,
|
|
3964
|
+
updatedAt: v.updatedAt
|
|
3965
|
+
};
|
|
3966
|
+
}
|
|
3967
|
+
function isPermissionMode(v) {
|
|
3968
|
+
return v === "default" || v === "plan";
|
|
3969
|
+
}
|
|
3970
|
+
function isTaskStatus(s) {
|
|
3971
|
+
return s === "backlog" || s === "in_progress" || s === "in_review" || s === "done" || s === "canceled" || s === "error";
|
|
3972
|
+
}
|
|
3973
|
+
var init_store = __esm(() => {
|
|
3974
|
+
init_ulid();
|
|
3975
|
+
});
|
|
3976
|
+
|
|
3977
|
+
// src/orchestrator/worktree/git.ts
|
|
3978
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
3979
|
+
function git2(args, opts) {
|
|
3980
|
+
if (!opts.cwd) {
|
|
3981
|
+
throw new Error("git(): cwd is required; refusing to inherit from process.cwd()");
|
|
3982
|
+
}
|
|
3983
|
+
const proc = spawnSync4("git", [...args], {
|
|
3984
|
+
cwd: opts.cwd,
|
|
3985
|
+
env: opts.env ? { ...process.env, ...opts.env } : process.env,
|
|
3986
|
+
encoding: "utf8",
|
|
3987
|
+
shell: false
|
|
3988
|
+
});
|
|
3989
|
+
const result = {
|
|
3990
|
+
stdout: proc.stdout ?? "",
|
|
3991
|
+
stderr: proc.stderr ?? "",
|
|
3992
|
+
exitCode: proc.status ?? -1
|
|
3993
|
+
};
|
|
3994
|
+
if (result.exitCode !== 0 && !opts.allowFail) {
|
|
3995
|
+
throw new GitCommandError(args, opts.cwd, result);
|
|
3996
|
+
}
|
|
3997
|
+
return result;
|
|
3998
|
+
}
|
|
3999
|
+
var GitCommandError;
|
|
4000
|
+
var init_git = __esm(() => {
|
|
4001
|
+
GitCommandError = class GitCommandError extends Error {
|
|
4002
|
+
args;
|
|
4003
|
+
cwd;
|
|
4004
|
+
exitCode;
|
|
4005
|
+
stdout;
|
|
4006
|
+
stderr;
|
|
4007
|
+
constructor(args, cwd, result) {
|
|
4008
|
+
super(`git ${args.join(" ")} (cwd=${cwd}) exited with code ${result.exitCode}: ${result.stderr.trim() || result.stdout.trim()}`);
|
|
4009
|
+
this.name = "GitCommandError";
|
|
4010
|
+
this.args = args;
|
|
4011
|
+
this.cwd = cwd;
|
|
4012
|
+
this.exitCode = result.exitCode;
|
|
4013
|
+
this.stdout = result.stdout;
|
|
4014
|
+
this.stderr = result.stderr;
|
|
4015
|
+
}
|
|
4016
|
+
};
|
|
4017
|
+
});
|
|
4018
|
+
|
|
4019
|
+
// src/orchestrator/worktree/paths.ts
|
|
4020
|
+
import fs2 from "fs";
|
|
4021
|
+
import path5 from "path";
|
|
4022
|
+
function worktreeRootFor(repo) {
|
|
4023
|
+
if (!path5.isAbsolute(repo)) {
|
|
4024
|
+
throw new Error(`worktreeRootFor: repo must be an absolute path, got: ${repo}`);
|
|
4025
|
+
}
|
|
4026
|
+
return path5.join(repo, KOBE_WORKTREE_ROOT_SUBPATH);
|
|
4027
|
+
}
|
|
4028
|
+
function worktreePathFor(repo, taskId) {
|
|
4029
|
+
if (!taskId || /[/\\\0]/.test(taskId)) {
|
|
4030
|
+
throw new Error(`worktreePathFor: invalid taskId: ${JSON.stringify(taskId)}`);
|
|
4031
|
+
}
|
|
4032
|
+
return path5.join(worktreeRootFor(repo), taskId);
|
|
4033
|
+
}
|
|
4034
|
+
function isKobeManagedPath(repo, candidate) {
|
|
4035
|
+
if (!path5.isAbsolute(repo) || !path5.isAbsolute(candidate))
|
|
4036
|
+
return false;
|
|
4037
|
+
const root = canonicalize(worktreeRootFor(repo));
|
|
4038
|
+
const target = canonicalize(candidate);
|
|
4039
|
+
const rel = path5.relative(root, target);
|
|
4040
|
+
return rel !== "" && !rel.startsWith("..") && !path5.isAbsolute(rel);
|
|
4041
|
+
}
|
|
4042
|
+
function canonicalize(p) {
|
|
4043
|
+
try {
|
|
4044
|
+
return fs2.realpathSync(p);
|
|
4045
|
+
} catch {
|
|
4046
|
+
return path5.resolve(p);
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
var KOBE_WORKTREE_ROOT_SUBPATH = ".claude/worktrees";
|
|
4050
|
+
var init_paths = () => {};
|
|
4051
|
+
|
|
4052
|
+
// src/orchestrator/worktree/manager.ts
|
|
4053
|
+
import fs3 from "fs";
|
|
4054
|
+
import path6 from "path";
|
|
4055
|
+
|
|
4056
|
+
class GitWorktreeManager {
|
|
4057
|
+
async create(repo, branch, worktreePath, baseRef) {
|
|
4058
|
+
requireAbsolute("repo", repo);
|
|
4059
|
+
requireAbsolute("path", worktreePath);
|
|
4060
|
+
if (!branch)
|
|
4061
|
+
throw new Error("create(): branch must be a non-empty string");
|
|
4062
|
+
if (fs3.existsSync(worktreePath)) {
|
|
4063
|
+
const existing = await this.tryDescribe(repo, worktreePath);
|
|
4064
|
+
if (existing) {
|
|
4065
|
+
if (existing.branch !== branch) {
|
|
4066
|
+
throw new Error(`worktree at ${worktreePath} is on branch '${existing.branch}', refusing to hijack to '${branch}'`);
|
|
4067
|
+
}
|
|
4068
|
+
return existing;
|
|
4069
|
+
}
|
|
4070
|
+
throw new Error(`create(): ${worktreePath} exists but is not a registered git worktree`);
|
|
4071
|
+
}
|
|
4072
|
+
fs3.mkdirSync(path6.dirname(worktreePath), { recursive: true });
|
|
4073
|
+
const branchExists = this.branchExists(repo, branch);
|
|
4074
|
+
const args = branchExists ? ["worktree", "add", worktreePath, branch] : baseRef ? ["worktree", "add", "-b", branch, worktreePath, baseRef] : ["worktree", "add", "-b", branch, worktreePath];
|
|
4075
|
+
git2(args, { cwd: repo });
|
|
4076
|
+
const info = await this.tryDescribe(repo, worktreePath);
|
|
4077
|
+
if (!info) {
|
|
4078
|
+
throw new Error(`create(): git reported success but ${worktreePath} is not a worktree`);
|
|
4079
|
+
}
|
|
4080
|
+
if (info.branch !== branch) {
|
|
4081
|
+
throw new Error(`create(): post-condition failed \u2014 expected branch '${branch}' at ${worktreePath}, got '${info.branch}'`);
|
|
4082
|
+
}
|
|
4083
|
+
return info;
|
|
4084
|
+
}
|
|
4085
|
+
async createForTask(args) {
|
|
4086
|
+
const target = worktreePathFor(args.repo, args.taskId);
|
|
4087
|
+
return this.create(args.repo, args.branch, target, args.baseRef);
|
|
4088
|
+
}
|
|
4089
|
+
async remove(worktreePath, opts) {
|
|
4090
|
+
requireAbsolute("path", worktreePath);
|
|
4091
|
+
const force = opts?.force === true;
|
|
4092
|
+
if (!fs3.existsSync(worktreePath)) {
|
|
4093
|
+
const repo2 = this.findRepoFor(worktreePath);
|
|
4094
|
+
if (repo2)
|
|
4095
|
+
git2(["worktree", "prune"], { cwd: repo2, allowFail: true });
|
|
4096
|
+
return;
|
|
4097
|
+
}
|
|
4098
|
+
const repo = this.findRepoFor(worktreePath);
|
|
4099
|
+
if (!repo) {
|
|
4100
|
+
throw new Error(`remove(): ${worktreePath} is not a git worktree`);
|
|
4101
|
+
}
|
|
4102
|
+
if (!force) {
|
|
4103
|
+
const dirty = await this.isDirty(worktreePath);
|
|
4104
|
+
if (dirty) {
|
|
4105
|
+
throw new Error(`remove(): refusing to remove dirty worktree at ${worktreePath} (pass { force: true } to override)`);
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
const args = force ? ["worktree", "remove", "--force", worktreePath] : ["worktree", "remove", worktreePath];
|
|
4109
|
+
git2(args, { cwd: repo });
|
|
4110
|
+
git2(["worktree", "prune"], { cwd: repo, allowFail: true });
|
|
4111
|
+
}
|
|
4112
|
+
async list(repo) {
|
|
4113
|
+
requireAbsolute("repo", repo);
|
|
4114
|
+
const out = git2(["worktree", "list", "--porcelain"], { cwd: repo });
|
|
4115
|
+
const all = parsePorcelain(out.stdout);
|
|
4116
|
+
const callerRoot = worktreeRootFor(repo);
|
|
4117
|
+
const canonRoot = canonicalize2(callerRoot);
|
|
4118
|
+
const infos = [];
|
|
4119
|
+
for (const entry of all) {
|
|
4120
|
+
if (!entry.path)
|
|
4121
|
+
continue;
|
|
4122
|
+
if (!isKobeManagedPath(repo, entry.path))
|
|
4123
|
+
continue;
|
|
4124
|
+
if (!entry.branch || entry.detached)
|
|
4125
|
+
continue;
|
|
4126
|
+
const canonEntry = canonicalize2(entry.path);
|
|
4127
|
+
const rel = path6.relative(canonRoot, canonEntry);
|
|
4128
|
+
const callerPath = path6.join(callerRoot, rel);
|
|
4129
|
+
const dirty = await this.isDirty(entry.path);
|
|
4130
|
+
infos.push({
|
|
4131
|
+
path: callerPath,
|
|
4132
|
+
branch: entry.branch,
|
|
4133
|
+
head: entry.head ?? "",
|
|
4134
|
+
dirty
|
|
4135
|
+
});
|
|
4136
|
+
}
|
|
4137
|
+
return infos;
|
|
4138
|
+
}
|
|
4139
|
+
async isDirty(worktreePath) {
|
|
4140
|
+
requireAbsolute("path", worktreePath);
|
|
4141
|
+
const out = git2(["status", "--porcelain"], { cwd: worktreePath });
|
|
4142
|
+
return out.stdout.length > 0;
|
|
4143
|
+
}
|
|
4144
|
+
async currentBranch(worktreePath) {
|
|
4145
|
+
requireAbsolute("path", worktreePath);
|
|
4146
|
+
const out = git2(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: worktreePath });
|
|
4147
|
+
const name = out.stdout.trim();
|
|
4148
|
+
if (!name || name === "HEAD") {
|
|
4149
|
+
throw new Error(`currentBranch(): ${worktreePath} is in detached-HEAD state`);
|
|
4150
|
+
}
|
|
4151
|
+
return name;
|
|
4152
|
+
}
|
|
4153
|
+
async renameBranch(worktreePath, from, to) {
|
|
4154
|
+
requireAbsolute("path", worktreePath);
|
|
4155
|
+
if (from === to)
|
|
4156
|
+
return;
|
|
4157
|
+
const repo = this.findRepoFor(worktreePath);
|
|
4158
|
+
if (!repo)
|
|
4159
|
+
throw new Error(`renameBranch(): ${worktreePath} is not a git worktree`);
|
|
4160
|
+
git2(["branch", "-m", from, to], { cwd: repo });
|
|
4161
|
+
}
|
|
4162
|
+
async tryDescribe(repo, worktreePath) {
|
|
4163
|
+
const out = git2(["worktree", "list", "--porcelain"], { cwd: repo });
|
|
4164
|
+
const entries = parsePorcelain(out.stdout);
|
|
4165
|
+
const target = canonicalize2(worktreePath);
|
|
4166
|
+
const match = entries.find((e) => e.path && canonicalize2(e.path) === target);
|
|
4167
|
+
if (!match || !match.path || !match.branch || match.detached)
|
|
4168
|
+
return null;
|
|
4169
|
+
return {
|
|
4170
|
+
path: worktreePath,
|
|
4171
|
+
branch: match.branch,
|
|
4172
|
+
head: match.head ?? "",
|
|
4173
|
+
dirty: await this.isDirty(match.path)
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
branchExists(repo, branch) {
|
|
4177
|
+
const ref = `refs/heads/${branch}`;
|
|
4178
|
+
const out = git2(["show-ref", "--verify", "--quiet", ref], { cwd: repo, allowFail: true });
|
|
4179
|
+
return out.exitCode === 0;
|
|
4180
|
+
}
|
|
4181
|
+
findRepoFor(worktreePath) {
|
|
4182
|
+
try {
|
|
4183
|
+
const out = git2(["rev-parse", "--git-common-dir"], { cwd: worktreePath, allowFail: true });
|
|
4184
|
+
if (out.exitCode !== 0)
|
|
4185
|
+
return null;
|
|
4186
|
+
const gitDir = out.stdout.trim();
|
|
4187
|
+
if (!gitDir)
|
|
4188
|
+
return null;
|
|
4189
|
+
const absolute = path6.isAbsolute(gitDir) ? gitDir : path6.resolve(worktreePath, gitDir);
|
|
4190
|
+
const base = path6.basename(absolute);
|
|
4191
|
+
return base === ".git" ? path6.dirname(absolute) : absolute;
|
|
4192
|
+
} catch (err) {
|
|
4193
|
+
if (err instanceof GitCommandError)
|
|
4194
|
+
return null;
|
|
4195
|
+
throw err;
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
function parsePorcelain(out) {
|
|
4200
|
+
const records = [];
|
|
4201
|
+
let current = null;
|
|
4202
|
+
for (const rawLine of out.split(`
|
|
4203
|
+
`)) {
|
|
4204
|
+
const line = rawLine.replace(/\r$/, "");
|
|
4205
|
+
if (line === "") {
|
|
4206
|
+
if (current)
|
|
4207
|
+
records.push(current);
|
|
4208
|
+
current = null;
|
|
4209
|
+
continue;
|
|
4210
|
+
}
|
|
4211
|
+
if (!current)
|
|
4212
|
+
current = {};
|
|
4213
|
+
if (line.startsWith("worktree ")) {
|
|
4214
|
+
current.path = line.slice("worktree ".length);
|
|
4215
|
+
} else if (line.startsWith("HEAD ")) {
|
|
4216
|
+
current.head = line.slice("HEAD ".length);
|
|
4217
|
+
} else if (line.startsWith("branch ")) {
|
|
4218
|
+
const ref = line.slice("branch ".length);
|
|
4219
|
+
current.branch = ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref;
|
|
4220
|
+
} else if (line === "detached") {
|
|
4221
|
+
current.detached = true;
|
|
4222
|
+
} else if (line === "bare") {
|
|
4223
|
+
current.bare = true;
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
if (current)
|
|
4227
|
+
records.push(current);
|
|
4228
|
+
return records;
|
|
4229
|
+
}
|
|
4230
|
+
function requireAbsolute(name, value) {
|
|
4231
|
+
if (!value || !path6.isAbsolute(value)) {
|
|
4232
|
+
throw new Error(`${name} must be an absolute path, got: ${JSON.stringify(value)}`);
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
function canonicalize2(p) {
|
|
4236
|
+
try {
|
|
4237
|
+
return fs3.realpathSync(p);
|
|
4238
|
+
} catch {
|
|
4239
|
+
return path6.resolve(p);
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
var init_manager = __esm(() => {
|
|
4243
|
+
init_git();
|
|
4244
|
+
init_paths();
|
|
4245
|
+
});
|
|
4246
|
+
|
|
4247
|
+
// src/daemon/paths.ts
|
|
4248
|
+
import { homedir as homedir9, tmpdir } from "os";
|
|
4249
|
+
import { join as join5 } from "path";
|
|
4250
|
+
function defaultDaemonSocketPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir9()) {
|
|
4251
|
+
const runtimeDir = process.env.XDG_RUNTIME_DIR;
|
|
4252
|
+
if (runtimeDir && runtimeDir.length > 0)
|
|
4253
|
+
return join5(runtimeDir, "kobe.sock");
|
|
4254
|
+
return join5(homeDir2, ".kobe", "daemon.sock");
|
|
4255
|
+
}
|
|
4256
|
+
function defaultDaemonPidPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir9()) {
|
|
4257
|
+
return join5(homeDir2, ".kobe", "daemon.pid");
|
|
4258
|
+
}
|
|
4259
|
+
var init_paths2 = () => {};
|
|
4260
|
+
|
|
4261
|
+
// src/bin/kobed.ts
|
|
4262
|
+
init_client();
|
|
4263
|
+
|
|
4264
|
+
// src/core/index.ts
|
|
4265
|
+
init_claude_code_local();
|
|
4266
|
+
init_bridge();
|
|
4267
|
+
init_core();
|
|
4268
|
+
init_store();
|
|
4269
|
+
init_manager();
|
|
4270
|
+
import { homedir as homedir8 } from "os";
|
|
4271
|
+
async function createKobeCore(options = {}) {
|
|
4272
|
+
const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir8();
|
|
4273
|
+
const store = new TaskIndexStore({ homeDir: homeDir2 });
|
|
4274
|
+
await store.load();
|
|
4275
|
+
const worktrees = new GitWorktreeManager;
|
|
4276
|
+
const engine = options.engine ?? new ClaudeCodeLocal;
|
|
4277
|
+
const orchestrator = new Orchestrator({ engine, store, worktrees });
|
|
4278
|
+
const bridge = options.startMcpBridge === false ? null : await startBridge(orchestrator, { homeDir: homeDir2 });
|
|
4279
|
+
return {
|
|
4280
|
+
homeDir: homeDir2,
|
|
4281
|
+
orchestrator,
|
|
4282
|
+
store,
|
|
4283
|
+
worktrees,
|
|
4284
|
+
bridge,
|
|
4285
|
+
async close() {
|
|
4286
|
+
await bridge?.close();
|
|
4287
|
+
orchestrator.dispose();
|
|
4288
|
+
}
|
|
4289
|
+
};
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
// src/bin/kobed.ts
|
|
4293
|
+
init_paths2();
|
|
4294
|
+
|
|
4295
|
+
// src/daemon/server.ts
|
|
4296
|
+
init_paths2();
|
|
4297
|
+
import { mkdir as mkdir3, readFile as readFile4, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
|
|
4298
|
+
import { createServer as createServer2 } from "net";
|
|
4299
|
+
import { dirname as dirname4 } from "path";
|
|
4300
|
+
async function startDaemonServer(orch, options = {}) {
|
|
4301
|
+
const socketPath = options.socketPath ?? defaultDaemonSocketPath(options.homeDir);
|
|
4302
|
+
const pidPath = options.pidPath ?? defaultDaemonPidPath(options.homeDir);
|
|
4303
|
+
const startedAt = options.startedAt ?? new Date;
|
|
4304
|
+
const clients = new Set;
|
|
4305
|
+
let nextClientId = 1;
|
|
4306
|
+
await mkdir3(dirname4(socketPath), { recursive: true });
|
|
4307
|
+
await mkdir3(dirname4(pidPath), { recursive: true });
|
|
4308
|
+
await unlink4(socketPath).catch(() => {});
|
|
4309
|
+
const server = createServer2((socket) => {
|
|
4310
|
+
const client = {
|
|
4311
|
+
id: nextClientId++,
|
|
4312
|
+
connectedAt: new Date,
|
|
4313
|
+
socket,
|
|
4314
|
+
buffer: "",
|
|
4315
|
+
subscriptions: new Map
|
|
4316
|
+
};
|
|
4317
|
+
clients.add(client);
|
|
4318
|
+
socket.on("data", (chunk) => {
|
|
4319
|
+
client.buffer += chunk.toString("utf8");
|
|
4320
|
+
drainClientBuffer(orch, serverApi, client);
|
|
4321
|
+
});
|
|
4322
|
+
socket.on("error", () => {});
|
|
4323
|
+
socket.on("close", () => {
|
|
4324
|
+
for (const unsub of client.subscriptions.values())
|
|
4325
|
+
unsub();
|
|
4326
|
+
client.subscriptions.clear();
|
|
4327
|
+
clients.delete(client);
|
|
4328
|
+
});
|
|
4329
|
+
});
|
|
4330
|
+
const serverApi = {
|
|
4331
|
+
socketPath,
|
|
4332
|
+
pidPath,
|
|
4333
|
+
startedAt,
|
|
4334
|
+
clients,
|
|
4335
|
+
async close() {
|
|
4336
|
+
broadcast(clients, { type: "event", name: "daemon.stopping", payload: {} });
|
|
4337
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
4338
|
+
for (const client of Array.from(clients)) {
|
|
4339
|
+
for (const unsub of client.subscriptions.values())
|
|
4340
|
+
unsub();
|
|
4341
|
+
client.subscriptions.clear();
|
|
4342
|
+
client.socket.end();
|
|
4343
|
+
}
|
|
4344
|
+
await unlink4(socketPath).catch(() => {});
|
|
4345
|
+
await unlink4(pidPath).catch(() => {});
|
|
4346
|
+
}
|
|
4347
|
+
};
|
|
4348
|
+
await new Promise((resolve, reject) => {
|
|
4349
|
+
server.once("error", reject);
|
|
4350
|
+
server.listen(socketPath, () => {
|
|
4351
|
+
server.removeListener("error", reject);
|
|
4352
|
+
resolve();
|
|
4353
|
+
});
|
|
4354
|
+
});
|
|
4355
|
+
await writeFile3(pidPath, `${process.pid}
|
|
4356
|
+
`, "utf8");
|
|
4357
|
+
async function stopSoon() {
|
|
4358
|
+
await options.onStop?.();
|
|
4359
|
+
setTimeout(() => {
|
|
4360
|
+
serverApi.close();
|
|
4361
|
+
}, 0).unref();
|
|
4362
|
+
}
|
|
4363
|
+
async function dispatch2(req, client) {
|
|
4364
|
+
const payload = objectPayload(req.payload);
|
|
4365
|
+
switch (req.name) {
|
|
4366
|
+
case "hello": {
|
|
4367
|
+
const tasks = orch.listTasks();
|
|
4368
|
+
const pending = {};
|
|
4369
|
+
for (const task of tasks) {
|
|
4370
|
+
const entries = orch.peekPendingInput(task.id);
|
|
4371
|
+
if (entries.length > 0)
|
|
4372
|
+
pending[task.id] = entries;
|
|
4373
|
+
}
|
|
4374
|
+
const runState = {};
|
|
4375
|
+
for (const [key, value] of orch.chatRunStateSignal()())
|
|
4376
|
+
runState[key] = value;
|
|
4377
|
+
return {
|
|
4378
|
+
protocolVersion: DAEMON_PROTOCOL_VERSION,
|
|
4379
|
+
daemonPid: process.pid,
|
|
4380
|
+
clientId: client.id,
|
|
4381
|
+
tasks: tasks.map(serializeTask),
|
|
4382
|
+
pending,
|
|
4383
|
+
runState
|
|
4384
|
+
};
|
|
4385
|
+
}
|
|
4386
|
+
case "daemon.status":
|
|
4387
|
+
return {
|
|
4388
|
+
daemonPid: process.pid,
|
|
4389
|
+
uptimeMs: Date.now() - startedAt.getTime(),
|
|
4390
|
+
startedAt: startedAt.toISOString(),
|
|
4391
|
+
attachedClients: clients.size,
|
|
4392
|
+
taskCount: orch.listTasks().length,
|
|
4393
|
+
socketPath
|
|
4394
|
+
};
|
|
4395
|
+
case "daemon.stop":
|
|
4396
|
+
await stopSoon();
|
|
4397
|
+
return {};
|
|
4398
|
+
case "task.list":
|
|
4399
|
+
return { tasks: orch.listTasks().map(serializeTask) };
|
|
4400
|
+
case "task.spawn": {
|
|
4401
|
+
const repo = requireString2(payload, "repo");
|
|
4402
|
+
const task = await orch.createTask({
|
|
4403
|
+
repo,
|
|
4404
|
+
prompt: optionalString2(payload, "prompt"),
|
|
4405
|
+
title: optionalString2(payload, "title"),
|
|
4406
|
+
branch: optionalString2(payload, "branch"),
|
|
4407
|
+
baseRef: optionalString2(payload, "baseRef")
|
|
4408
|
+
});
|
|
4409
|
+
for (const c of clients)
|
|
4410
|
+
subscribeClientToTask(orch, c, task);
|
|
4411
|
+
broadcast(clients, { type: "event", name: "task.created", payload: { task: serializeTask(task) } });
|
|
4412
|
+
return { taskId: task.id, task: serializeTask(task) };
|
|
4413
|
+
}
|
|
4414
|
+
case "task.archive": {
|
|
4415
|
+
const taskId = requireString2(payload, "taskId");
|
|
4416
|
+
const archived = optionalBoolean(payload, "archived");
|
|
4417
|
+
await orch.setArchived(taskId, archived);
|
|
4418
|
+
const task = orch.getTask(taskId);
|
|
4419
|
+
if (task)
|
|
4420
|
+
broadcast(clients, { type: "event", name: "task.updated", payload: { taskId, task: serializeTask(task) } });
|
|
4421
|
+
return {};
|
|
4422
|
+
}
|
|
4423
|
+
case "task.rename": {
|
|
4424
|
+
const taskId = requireString2(payload, "taskId");
|
|
4425
|
+
await orch.setTitle(taskId, requireString2(payload, "title"));
|
|
4426
|
+
const task = orch.getTask(taskId);
|
|
4427
|
+
if (task)
|
|
4428
|
+
broadcast(clients, { type: "event", name: "task.updated", payload: { taskId, task: serializeTask(task) } });
|
|
4429
|
+
return {};
|
|
4430
|
+
}
|
|
4431
|
+
case "task.delete": {
|
|
4432
|
+
const taskId = requireString2(payload, "taskId");
|
|
4433
|
+
await orch.deleteTask(taskId);
|
|
4434
|
+
for (const c of clients)
|
|
4435
|
+
unsubscribeClientFromTask(c, taskId);
|
|
4436
|
+
broadcast(clients, { type: "event", name: "task.deleted", payload: { taskId } });
|
|
4437
|
+
return {};
|
|
4438
|
+
}
|
|
4439
|
+
case "task.pin": {
|
|
4440
|
+
const taskId = requireString2(payload, "taskId");
|
|
4441
|
+
await orch.setPinned(taskId, optionalBoolean(payload, "pinned"));
|
|
4442
|
+
broadcastTaskUpdated(orch, clients, taskId);
|
|
4443
|
+
return {};
|
|
4444
|
+
}
|
|
4445
|
+
case "task.permissionMode": {
|
|
4446
|
+
const taskId = requireString2(payload, "taskId");
|
|
4447
|
+
const mode = optionalString2(payload, "mode");
|
|
4448
|
+
if (mode !== undefined && mode !== "default" && mode !== "plan")
|
|
4449
|
+
throw new Error("mode must be default or plan");
|
|
4450
|
+
await orch.setPermissionMode(taskId, mode);
|
|
4451
|
+
broadcastTaskUpdated(orch, clients, taskId);
|
|
4452
|
+
return {};
|
|
4453
|
+
}
|
|
4454
|
+
case "task.model": {
|
|
4455
|
+
const taskId = requireString2(payload, "taskId");
|
|
4456
|
+
await orch.setModel(taskId, optionalString2(payload, "model"));
|
|
4457
|
+
broadcastTaskUpdated(orch, clients, taskId);
|
|
4458
|
+
return {};
|
|
4459
|
+
}
|
|
4460
|
+
case "task.ensureMain": {
|
|
4461
|
+
const repo = requireString2(payload, "repo");
|
|
4462
|
+
const prior = orch.listTasks().find((t) => t.kind === "main" && t.repo === repo);
|
|
4463
|
+
const task = await orch.ensureMainTask(repo);
|
|
4464
|
+
if (!prior) {
|
|
4465
|
+
for (const c of clients)
|
|
4466
|
+
subscribeClientToTask(orch, c, task);
|
|
4467
|
+
broadcast(clients, { type: "event", name: "task.created", payload: { task: serializeTask(task) } });
|
|
4468
|
+
} else if (prior.archived && !task.archived) {
|
|
4469
|
+
broadcastTaskUpdated(orch, clients, task.id);
|
|
4470
|
+
}
|
|
4471
|
+
return { task: serializeTask(task) };
|
|
4472
|
+
}
|
|
4473
|
+
case "chat.tab.create": {
|
|
4474
|
+
const taskId = requireString2(payload, "taskId");
|
|
4475
|
+
const tab = await orch.createTab(taskId, { title: optionalString2(payload, "title") });
|
|
4476
|
+
for (const c of clients)
|
|
4477
|
+
subscribeClientToTab(orch, c, taskId, tab.id);
|
|
4478
|
+
broadcastTaskUpdated(orch, clients, taskId);
|
|
4479
|
+
return { tab };
|
|
4480
|
+
}
|
|
4481
|
+
case "chat.tab.close": {
|
|
4482
|
+
const taskId = requireString2(payload, "taskId");
|
|
4483
|
+
const nextActive = await orch.closeTab(taskId, requireString2(payload, "tabId"));
|
|
4484
|
+
broadcastTaskUpdated(orch, clients, taskId);
|
|
4485
|
+
return { nextActive };
|
|
4486
|
+
}
|
|
4487
|
+
case "chat.tab.activate": {
|
|
4488
|
+
const taskId = requireString2(payload, "taskId");
|
|
4489
|
+
await orch.setActiveTab(taskId, requireString2(payload, "tabId"));
|
|
4490
|
+
broadcastTaskUpdated(orch, clients, taskId);
|
|
4491
|
+
return {};
|
|
4492
|
+
}
|
|
4493
|
+
case "chat.tab.rename": {
|
|
4494
|
+
const taskId = requireString2(payload, "taskId");
|
|
4495
|
+
await orch.setTabTitle(taskId, requireString2(payload, "tabId"), requireString2(payload, "title"));
|
|
4496
|
+
broadcastTaskUpdated(orch, clients, taskId);
|
|
4497
|
+
return {};
|
|
4498
|
+
}
|
|
4499
|
+
case "chat.sessions": {
|
|
4500
|
+
const sessions = await orch.listSessions(requireString2(payload, "taskId"));
|
|
4501
|
+
return { sessions };
|
|
4502
|
+
}
|
|
4503
|
+
case "chat.session.open": {
|
|
4504
|
+
const taskId = requireString2(payload, "taskId");
|
|
4505
|
+
const tabId = await orch.openSessionInTab(taskId, requireString2(payload, "sessionId"), {
|
|
4506
|
+
title: optionalString2(payload, "title")
|
|
4507
|
+
});
|
|
4508
|
+
for (const c of clients)
|
|
4509
|
+
subscribeClientToTab(orch, c, taskId, tabId);
|
|
4510
|
+
broadcastTaskUpdated(orch, clients, taskId);
|
|
4511
|
+
return { tabId };
|
|
4512
|
+
}
|
|
4513
|
+
case "chat.interrupt": {
|
|
4514
|
+
await orch.interruptTask(requireString2(payload, "taskId"), optionalString2(payload, "tabId"));
|
|
4515
|
+
return {};
|
|
4516
|
+
}
|
|
4517
|
+
case "chat.input.pending": {
|
|
4518
|
+
return { pending: orch.peekPendingInput(requireString2(payload, "taskId")) };
|
|
4519
|
+
}
|
|
4520
|
+
case "chat.input.respond": {
|
|
4521
|
+
await orch.respondToInput(requireString2(payload, "taskId"), requireString2(payload, "requestId"), requireUserInputResponse(payload.response));
|
|
4522
|
+
return {};
|
|
4523
|
+
}
|
|
4524
|
+
case "pr.request": {
|
|
4525
|
+
await orch.requestPR(requireString2(payload, "taskId"));
|
|
4526
|
+
return {};
|
|
4527
|
+
}
|
|
4528
|
+
case "chat.history": {
|
|
4529
|
+
const taskId = requireString2(payload, "taskId");
|
|
4530
|
+
const sessionId = optionalString2(payload, "sessionId");
|
|
4531
|
+
const limit = optionalNumber(payload, "limit") ?? 50;
|
|
4532
|
+
const before = optionalString2(payload, "before");
|
|
4533
|
+
const result = await readTaskHistory(orch, taskId, sessionId, limit, before);
|
|
4534
|
+
return {
|
|
4535
|
+
messages: serializeMessages(result.messages),
|
|
4536
|
+
nextBefore: result.nextBefore,
|
|
4537
|
+
hasMore: result.hasMore
|
|
4538
|
+
};
|
|
4539
|
+
}
|
|
4540
|
+
case "chat.send": {
|
|
4541
|
+
const taskId = requireString2(payload, "taskId");
|
|
4542
|
+
const tabId = optionalString2(payload, "tabId");
|
|
4543
|
+
const text = optionalString2(payload, "text");
|
|
4544
|
+
await orch.runTask(taskId, text, tabId);
|
|
4545
|
+
const task = orch.getTask(taskId);
|
|
4546
|
+
if (task)
|
|
4547
|
+
broadcast(clients, {
|
|
4548
|
+
type: "event",
|
|
4549
|
+
name: "engine.status",
|
|
4550
|
+
payload: { taskId, tabId: tabId ?? task.activeTabId, status: "running" }
|
|
4551
|
+
});
|
|
4552
|
+
return {};
|
|
4553
|
+
}
|
|
4554
|
+
case "subscribe": {
|
|
4555
|
+
const taskIds = normalizeTaskIds(payload.taskIds);
|
|
4556
|
+
const tasks = taskIds === "all" ? orch.listTasks() : taskIds.map((id) => orch.getTask(id)).filter((t) => Boolean(t));
|
|
4557
|
+
for (const task of tasks)
|
|
4558
|
+
subscribeClientToTask(orch, client, task);
|
|
4559
|
+
return {};
|
|
4560
|
+
}
|
|
4561
|
+
default:
|
|
4562
|
+
throw new Error(`unknown daemon request: ${req.name}`);
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
4565
|
+
async function handleRequest(req, client) {
|
|
4566
|
+
try {
|
|
4567
|
+
const payload = await dispatch2(req, client);
|
|
4568
|
+
writeFrame(client, { type: "response", id: req.id, name: req.name, payload });
|
|
4569
|
+
} catch (err) {
|
|
4570
|
+
writeFrame(client, {
|
|
4571
|
+
type: "response",
|
|
4572
|
+
id: req.id,
|
|
4573
|
+
name: req.name,
|
|
4574
|
+
error: {
|
|
4575
|
+
message: err instanceof Error ? err.message : String(err),
|
|
4576
|
+
name: err instanceof Error ? err.name : undefined
|
|
4577
|
+
}
|
|
4578
|
+
});
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
function drainClientBuffer(orch2, _server, client) {
|
|
4582
|
+
let nl = client.buffer.indexOf(`
|
|
4583
|
+
`);
|
|
4584
|
+
while (nl !== -1) {
|
|
4585
|
+
const line = client.buffer.slice(0, nl);
|
|
4586
|
+
client.buffer = client.buffer.slice(nl + 1);
|
|
4587
|
+
if (line.trim().length > 0) {
|
|
4588
|
+
try {
|
|
4589
|
+
const frame = JSON.parse(line);
|
|
4590
|
+
if (frame.type !== "request")
|
|
4591
|
+
throw new Error("daemon only accepts request frames from clients");
|
|
4592
|
+
handleRequest(frame, client);
|
|
4593
|
+
} catch (err) {
|
|
4594
|
+
writeFrame(client, {
|
|
4595
|
+
type: "response",
|
|
4596
|
+
id: "parse-error",
|
|
4597
|
+
error: { message: err instanceof Error ? err.message : String(err) }
|
|
4598
|
+
});
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4601
|
+
nl = client.buffer.indexOf(`
|
|
4602
|
+
`);
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
return serverApi;
|
|
4606
|
+
}
|
|
4607
|
+
async function readPidFile(pidPath) {
|
|
4608
|
+
try {
|
|
4609
|
+
const raw = await readFile4(pidPath, "utf8");
|
|
4610
|
+
const pid = Number(raw.trim());
|
|
4611
|
+
return Number.isFinite(pid) ? pid : null;
|
|
4612
|
+
} catch {
|
|
4613
|
+
return null;
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
function subscribeClientToTask(orch, client, task) {
|
|
4617
|
+
for (const tab of task.tabs)
|
|
4618
|
+
subscribeClientToTab(orch, client, task.id, tab.id);
|
|
4619
|
+
}
|
|
4620
|
+
function subscribeClientToTab(orch, client, taskId, tabId) {
|
|
4621
|
+
const key = `${taskId}:${tabId}`;
|
|
4622
|
+
if (client.subscriptions.has(key))
|
|
4623
|
+
return;
|
|
4624
|
+
const unsub = orch.subscribeEvents(taskId, (ev) => writeFrame(client, normalizeEventForWire(taskId, tabId, ev)), tabId);
|
|
4625
|
+
client.subscriptions.set(key, unsub);
|
|
4626
|
+
}
|
|
4627
|
+
function broadcastTaskUpdated(orch, clients, taskId) {
|
|
4628
|
+
const task = orch.getTask(taskId);
|
|
4629
|
+
if (!task)
|
|
4630
|
+
return;
|
|
4631
|
+
broadcast(clients, { type: "event", name: "task.updated", payload: { taskId, task: serializeTask(task) } });
|
|
4632
|
+
}
|
|
4633
|
+
function unsubscribeClientFromTask(client, taskId) {
|
|
4634
|
+
const prefix = `${taskId}:`;
|
|
4635
|
+
for (const [key, unsub] of client.subscriptions) {
|
|
4636
|
+
if (!key.startsWith(prefix))
|
|
4637
|
+
continue;
|
|
4638
|
+
unsub();
|
|
4639
|
+
client.subscriptions.delete(key);
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
4642
|
+
async function readTaskHistory(orch, taskId, requestedSessionId, limit, before) {
|
|
4643
|
+
const task = orch.getTask(taskId);
|
|
4644
|
+
const sessionId = requestedSessionId ?? task?.tabs.find((t) => t.id === task.activeTabId)?.sessionId ?? task?.sessionId;
|
|
4645
|
+
if (!sessionId)
|
|
4646
|
+
return { messages: [], nextBefore: null, hasMore: false };
|
|
4647
|
+
const messages = await orch.readHistory(sessionId);
|
|
4648
|
+
const beforeIdx = before ? messages.findIndex((m) => `${m.timestamp}:${m.sessionId}` === before) : -1;
|
|
4649
|
+
const end = beforeIdx >= 0 ? beforeIdx : messages.length;
|
|
4650
|
+
const start = Math.max(0, end - limit);
|
|
4651
|
+
const page = messages.slice(start, end);
|
|
4652
|
+
const hasMore = start > 0;
|
|
4653
|
+
const first = page[0];
|
|
4654
|
+
const nextBefore = hasMore && first ? `${first.timestamp}:${first.sessionId}` : null;
|
|
4655
|
+
return { messages: page, nextBefore, hasMore };
|
|
4656
|
+
}
|
|
4657
|
+
function writeFrame(client, frame) {
|
|
4658
|
+
client.socket.write(frameToLine(frame));
|
|
4659
|
+
}
|
|
4660
|
+
function broadcast(clients, frame) {
|
|
4661
|
+
for (const client of clients)
|
|
4662
|
+
writeFrame(client, frame);
|
|
4663
|
+
}
|
|
4664
|
+
function objectPayload(payload) {
|
|
4665
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload))
|
|
4666
|
+
return {};
|
|
4667
|
+
return payload;
|
|
4668
|
+
}
|
|
4669
|
+
function requireString2(payload, key) {
|
|
4670
|
+
const value = payload[key];
|
|
4671
|
+
if (typeof value !== "string" || value.length === 0)
|
|
4672
|
+
throw new Error(`${key} is required`);
|
|
4673
|
+
return value;
|
|
4674
|
+
}
|
|
4675
|
+
function optionalString2(payload, key) {
|
|
4676
|
+
const value = payload[key];
|
|
4677
|
+
if (value === undefined || value === null || value === "")
|
|
4678
|
+
return;
|
|
4679
|
+
if (typeof value !== "string")
|
|
4680
|
+
throw new Error(`${key} must be a string`);
|
|
4681
|
+
return value;
|
|
4682
|
+
}
|
|
4683
|
+
function optionalBoolean(payload, key) {
|
|
4684
|
+
const value = payload[key];
|
|
4685
|
+
if (value === undefined || value === null)
|
|
4686
|
+
return;
|
|
4687
|
+
if (typeof value !== "boolean")
|
|
4688
|
+
throw new Error(`${key} must be a boolean`);
|
|
4689
|
+
return value;
|
|
4690
|
+
}
|
|
4691
|
+
function optionalNumber(payload, key) {
|
|
4692
|
+
const value = payload[key];
|
|
4693
|
+
if (value === undefined || value === null)
|
|
4694
|
+
return;
|
|
4695
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
4696
|
+
throw new Error(`${key} must be a number`);
|
|
4697
|
+
return value;
|
|
4698
|
+
}
|
|
4699
|
+
function normalizeTaskIds(value) {
|
|
4700
|
+
if (value === undefined || value === null || value === "all")
|
|
4701
|
+
return "all";
|
|
4702
|
+
if (Array.isArray(value) && value.every((v) => typeof v === "string"))
|
|
4703
|
+
return value;
|
|
4704
|
+
throw new Error("taskIds must be 'all' or string[]");
|
|
4705
|
+
}
|
|
4706
|
+
function requireUserInputResponse(value) {
|
|
4707
|
+
if (!value || typeof value !== "object")
|
|
4708
|
+
throw new Error("response is required");
|
|
4709
|
+
const obj = value;
|
|
4710
|
+
if (obj.kind === "approve_plan") {
|
|
4711
|
+
if (typeof obj.approve !== "boolean")
|
|
4712
|
+
throw new Error("response.approve must be a boolean");
|
|
4713
|
+
return { kind: "approve_plan", approve: obj.approve };
|
|
4714
|
+
}
|
|
4715
|
+
if (obj.kind === "ask_question") {
|
|
4716
|
+
if (!obj.answers || typeof obj.answers !== "object" || Array.isArray(obj.answers)) {
|
|
4717
|
+
throw new Error("response.answers must be an object");
|
|
4718
|
+
}
|
|
4719
|
+
const answers = {};
|
|
4720
|
+
for (const [key, answer] of Object.entries(obj.answers)) {
|
|
4721
|
+
if (typeof answer === "string")
|
|
4722
|
+
answers[key] = answer;
|
|
4723
|
+
}
|
|
4724
|
+
return { kind: "ask_question", answers };
|
|
4725
|
+
}
|
|
4726
|
+
throw new Error("response.kind must be approve_plan or ask_question");
|
|
4727
|
+
}
|
|
4728
|
+
|
|
4729
|
+
// src/bin/kobed.ts
|
|
4730
|
+
async function main() {
|
|
4731
|
+
const [, , command = "status"] = process.argv;
|
|
4732
|
+
const socketPath = defaultDaemonSocketPath();
|
|
4733
|
+
const pidPath = defaultDaemonPidPath();
|
|
4734
|
+
if (command === "status") {
|
|
4735
|
+
const client = new KobeDaemonClient(socketPath);
|
|
4736
|
+
try {
|
|
4737
|
+
const status = await client.request("daemon.status");
|
|
4738
|
+
console.log(JSON.stringify(status, null, 2));
|
|
4739
|
+
} catch {
|
|
4740
|
+
const pid = await readPidFile(pidPath);
|
|
4741
|
+
if (pid)
|
|
4742
|
+
console.log(`kobed: no daemon socket at ${socketPath} (stale pidfile pid=${pid})`);
|
|
4743
|
+
else
|
|
4744
|
+
console.log(`kobed: no daemon running at ${socketPath}`);
|
|
4745
|
+
process.exitCode = 1;
|
|
4746
|
+
} finally {
|
|
4747
|
+
client.close();
|
|
4748
|
+
}
|
|
4749
|
+
return;
|
|
4750
|
+
}
|
|
4751
|
+
if (command === "stop") {
|
|
4752
|
+
const client = new KobeDaemonClient(socketPath);
|
|
4753
|
+
try {
|
|
4754
|
+
await client.request("daemon.stop");
|
|
4755
|
+
console.log("kobed: stop requested");
|
|
4756
|
+
} finally {
|
|
4757
|
+
client.close();
|
|
4758
|
+
}
|
|
4759
|
+
return;
|
|
4760
|
+
}
|
|
4761
|
+
if (command === "restart") {
|
|
4762
|
+
const oldPid = await readPidFile(pidPath);
|
|
4763
|
+
const client = new KobeDaemonClient(socketPath);
|
|
4764
|
+
try {
|
|
4765
|
+
await client.request("daemon.stop");
|
|
4766
|
+
} catch {} finally {
|
|
4767
|
+
client.close();
|
|
4768
|
+
}
|
|
4769
|
+
if (oldPid && oldPid !== process.pid) {
|
|
4770
|
+
const deadline = Date.now() + 5000;
|
|
4771
|
+
while (Date.now() < deadline) {
|
|
4772
|
+
try {
|
|
4773
|
+
process.kill(oldPid, 0);
|
|
4774
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
4775
|
+
} catch {
|
|
4776
|
+
break;
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
}
|
|
4780
|
+
} else if (command !== "start") {
|
|
4781
|
+
console.error("usage: kobed start|stop|status|restart");
|
|
4782
|
+
process.exit(2);
|
|
4783
|
+
}
|
|
4784
|
+
const core = await createKobeCore();
|
|
4785
|
+
const server = await startDaemonServer(core.orchestrator, {
|
|
4786
|
+
socketPath,
|
|
4787
|
+
pidPath,
|
|
4788
|
+
homeDir: core.homeDir,
|
|
4789
|
+
onStop: async () => {
|
|
4790
|
+
await core.close();
|
|
4791
|
+
}
|
|
4792
|
+
});
|
|
4793
|
+
console.log(`kobed: listening on ${server.socketPath}`);
|
|
4794
|
+
const shutdown = async () => {
|
|
4795
|
+
await server.close();
|
|
4796
|
+
await core.close();
|
|
4797
|
+
process.exit(0);
|
|
4798
|
+
};
|
|
4799
|
+
process.once("SIGINT", () => void shutdown());
|
|
4800
|
+
process.once("SIGTERM", () => void shutdown());
|
|
4801
|
+
}
|
|
4802
|
+
main().catch((err) => {
|
|
4803
|
+
console.error("kobed failed:", err instanceof Error ? err.message : String(err));
|
|
4804
|
+
process.exit(1);
|
|
4805
|
+
});
|