@sma1lboy/kobe 0.5.8 → 0.5.9

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 CHANGED
@@ -362,7 +362,8 @@ var init_binary = __esm(() => {
362
362
  });
363
363
 
364
364
  // src/engine/claude-code-local/history.ts
365
- import { readFile, readdir, unlink } from "fs/promises";
365
+ import { randomUUID } from "crypto";
366
+ import { appendFile, mkdir, readFile, readdir, unlink, writeFile } from "fs/promises";
366
367
  import { homedir as homedir3 } from "os";
367
368
  import path2 from "path";
368
369
  function encodeCwd(cwd) {
@@ -459,6 +460,76 @@ function extractUsage(v) {
459
460
  function isObject(v) {
460
461
  return typeof v === "object" && v !== null && !Array.isArray(v);
461
462
  }
463
+ async function appendInterruptedUserPrompt(sessionId, cwd, prompt, deps = defaultDeps2) {
464
+ if (!prompt || prompt.trim().length === 0)
465
+ return;
466
+ const projectDir = path2.join(deps.projectsDir(), encodeCwd(cwd));
467
+ const filePath = path2.join(projectDir, `${sessionId}.jsonl`);
468
+ let lines = [];
469
+ try {
470
+ const raw = await readFile(filePath, "utf8");
471
+ lines = raw.split(`
472
+ `).filter((l) => l.length > 0);
473
+ } catch (err) {
474
+ if (err.code !== "ENOENT")
475
+ throw err;
476
+ await mkdir(projectDir, { recursive: true });
477
+ }
478
+ let lastConvIdx = -1;
479
+ let lastConvRecord = null;
480
+ let lastConvRole = null;
481
+ for (let i = lines.length - 1;i >= 0; i--) {
482
+ let parsed;
483
+ try {
484
+ parsed = JSON.parse(lines[i]);
485
+ } catch {
486
+ continue;
487
+ }
488
+ if (!isObject(parsed))
489
+ continue;
490
+ const inner = isObject(parsed.message) ? parsed.message : parsed;
491
+ const role = inner.role;
492
+ if (role === "user" || role === "assistant") {
493
+ lastConvIdx = i;
494
+ lastConvRecord = parsed;
495
+ lastConvRole = role;
496
+ break;
497
+ }
498
+ }
499
+ const now = new Date().toISOString();
500
+ if (lastConvRole === "user" && lastConvRecord && lastConvIdx >= 0) {
501
+ const inner = isObject(lastConvRecord.message) ? lastConvRecord.message : lastConvRecord;
502
+ const existing = typeof inner.content === "string" ? inner.content : "";
503
+ if (existing === prompt || existing.endsWith(`
504
+
505
+ ${prompt}`))
506
+ return;
507
+ inner.content = existing.length > 0 ? `${existing}
508
+
509
+ ${prompt}` : prompt;
510
+ lastConvRecord.timestamp = now;
511
+ lines[lastConvIdx] = JSON.stringify(lastConvRecord);
512
+ await writeFile(filePath, `${lines.join(`
513
+ `)}
514
+ `);
515
+ return;
516
+ }
517
+ const parentUuid = lastConvRecord && typeof lastConvRecord.uuid === "string" ? lastConvRecord.uuid : null;
518
+ const record = {
519
+ type: "user",
520
+ message: { role: "user", content: prompt },
521
+ uuid: randomUUID(),
522
+ parentUuid,
523
+ sessionId,
524
+ cwd,
525
+ timestamp: now,
526
+ isSidechain: false,
527
+ userType: "external",
528
+ version: "1.0.0"
529
+ };
530
+ await appendFile(filePath, `${JSON.stringify(record)}
531
+ `);
532
+ }
462
533
  var defaultDeps2;
463
534
  var init_history = __esm(() => {
464
535
  defaultDeps2 = {
@@ -866,13 +937,21 @@ class ClaudeCodeLocal {
866
937
  }
867
938
  async stop(handle) {
868
939
  const sid = handle.sessionId;
869
- await this.registry.kill(sid, this.stopGraceMs);
870
940
  const session = this.running.get(sid);
941
+ const shouldRescue = !!session && !session.completedNaturally && session.prompt.trim().length > 0;
942
+ const rescuePrompt = session?.prompt ?? "";
943
+ const rescueCwd = session?.cwd ?? handle.cwd;
944
+ await this.registry.kill(sid, this.stopGraceMs);
871
945
  if (session) {
872
946
  session.closed = true;
873
947
  this.notify(session);
874
948
  this.running.delete(sid);
875
949
  }
950
+ if (shouldRescue) {
951
+ try {
952
+ await appendInterruptedUserPrompt(sid, rescueCwd, rescuePrompt);
953
+ } catch {}
954
+ }
876
955
  }
877
956
  async start(args) {
878
957
  const binaryPath = await this.binaryPathResolver();
@@ -905,14 +984,17 @@ class ClaudeCodeLocal {
905
984
  spawned,
906
985
  queue,
907
986
  waiters: [],
908
- closed: false
987
+ closed: false,
988
+ completedNaturally: false,
989
+ prompt: args.prompt
909
990
  };
910
991
  this.running.set(sessionId, session);
911
992
  this.registry.register({
912
993
  sessionId,
913
994
  cwd: args.cwd,
914
995
  proc: spawned.proc,
915
- startedAt: Date.now()
996
+ startedAt: Date.now(),
997
+ prompt: args.prompt
916
998
  });
917
999
  resolveHandle({ sessionId, cwd: args.cwd });
918
1000
  };
@@ -934,6 +1016,9 @@ class ClaudeCodeLocal {
934
1016
  try {
935
1017
  for await (const ev of events) {
936
1018
  queue.push(ev);
1019
+ if (ev.type === "done" && session) {
1020
+ session.completedNaturally = true;
1021
+ }
937
1022
  if (session)
938
1023
  this.notify(session);
939
1024
  }
@@ -988,11 +1073,11 @@ var init_claude_code_local = __esm(() => {
988
1073
  });
989
1074
 
990
1075
  // src/orchestrator/bridge/server.ts
991
- import { mkdir, unlink as unlink2 } from "fs/promises";
1076
+ import { mkdir as mkdir2, unlink as unlink2 } from "fs/promises";
992
1077
  import { createServer } from "net";
993
1078
  import { dirname as dirname2 } from "path";
994
1079
  async function startBridgeServer(orch, socketPath) {
995
- await mkdir(dirname2(socketPath), { recursive: true });
1080
+ await mkdir2(dirname2(socketPath), { recursive: true });
996
1081
  await unlink2(socketPath).catch(() => {});
997
1082
  const conns = new Set;
998
1083
  const server = createServer((conn) => {
@@ -1125,7 +1210,7 @@ var exports_bridge = {};
1125
1210
  __export(exports_bridge, {
1126
1211
  startBridge: () => startBridge
1127
1212
  });
1128
- import { writeFile } from "fs/promises";
1213
+ import { writeFile as writeFile2 } from "fs/promises";
1129
1214
  import { homedir as homedir5 } from "os";
1130
1215
  import { join as join3 } from "path";
1131
1216
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -1145,7 +1230,7 @@ async function startBridge(orch, opts = {}) {
1145
1230
  }
1146
1231
  }
1147
1232
  };
1148
- await writeFile(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
1233
+ await writeFile2(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), "utf8");
1149
1234
  process.env.KOBE_MCP_CONFIG = mcpConfigPath;
1150
1235
  return {
1151
1236
  socketPath,
@@ -3034,6 +3119,7 @@ class Orchestrator {
3034
3119
  worktrees;
3035
3120
  metadataSuggester;
3036
3121
  handles = new Map;
3122
+ firstSpawnLatches = new Map;
3037
3123
  subscribers = new Map;
3038
3124
  pumps = new Map;
3039
3125
  pendingInputBroker = new InMemoryPendingInputBroker;
@@ -3266,8 +3352,16 @@ class Orchestrator {
3266
3352
  const renameTabId = this.resolveTab(task, tabId).id;
3267
3353
  this.maybeRenameTempBranch(task.id, renameTabId, prompt);
3268
3354
  }
3269
- const targetTab = this.resolveTab(task, tabId);
3355
+ let targetTab = this.resolveTab(task, tabId);
3270
3356
  const key = tabKey(task.id, targetTab.id);
3357
+ if (!targetTab.sessionId) {
3358
+ const inflight = this.firstSpawnLatches.get(key);
3359
+ if (inflight) {
3360
+ await inflight.catch(() => {});
3361
+ task = this.requireTask(id);
3362
+ targetTab = this.resolveTab(task, tabId);
3363
+ }
3364
+ }
3271
3365
  if (this.handles.has(key) === false) {
3272
3366
  const running = this.countRunning();
3273
3367
  if (running >= CONCURRENCY_CAP) {
@@ -3288,18 +3382,28 @@ class Orchestrator {
3288
3382
  model: modelToUse
3289
3383
  });
3290
3384
  } else {
3291
- handle = await this.engine.spawn(task.worktreePath, promptToSend, {
3292
- permissionMode: task.permissionMode,
3293
- model: modelToUse
3385
+ let releaseLatch = () => {};
3386
+ const latch = new Promise((resolve2) => {
3387
+ releaseLatch = resolve2;
3294
3388
  });
3295
- await this.updateTab(task.id, targetTab.id, { sessionId: handle.sessionId });
3296
- if (task.title === PLACEHOLDER_TASK_TITLE && prompt && prompt.trim().length > 0) {
3297
- const derived = deriveTitleFromPrompt(prompt);
3298
- if (derived)
3299
- await this.store.update(task.id, { title: derived });
3300
- }
3301
- if (prompt && prompt.trim().length > 0) {
3302
- this.maybeUpgradeTitle(task.id, prompt);
3389
+ this.firstSpawnLatches.set(key, latch);
3390
+ try {
3391
+ handle = await this.engine.spawn(task.worktreePath, promptToSend, {
3392
+ permissionMode: task.permissionMode,
3393
+ model: modelToUse
3394
+ });
3395
+ await this.updateTab(task.id, targetTab.id, { sessionId: handle.sessionId });
3396
+ if (task.title === PLACEHOLDER_TASK_TITLE && prompt && prompt.trim().length > 0) {
3397
+ const derived = deriveTitleFromPrompt(prompt);
3398
+ if (derived)
3399
+ await this.store.update(task.id, { title: derived });
3400
+ }
3401
+ if (prompt && prompt.trim().length > 0) {
3402
+ this.maybeUpgradeTitle(task.id, prompt);
3403
+ }
3404
+ } finally {
3405
+ releaseLatch();
3406
+ this.firstSpawnLatches.delete(key);
3303
3407
  }
3304
3408
  }
3305
3409
  this.handles.set(key, handle);
@@ -3366,6 +3470,12 @@ class Orchestrator {
3366
3470
  }
3367
3471
  this.dispatchEvent(task.id, targetTab.id, { type: "done" });
3368
3472
  }
3473
+ async steerTask(id, prompt, tabId) {
3474
+ const task = this.requireTask(id);
3475
+ const targetTab = this.resolveTab(task, tabId);
3476
+ await this.interruptTask(task.id, targetTab.id);
3477
+ await this.runTask(task.id, prompt, targetTab.id);
3478
+ }
3369
3479
  async pauseTask(id) {
3370
3480
  const task = this.requireTask(id);
3371
3481
  if (task.status !== "in_progress") {
@@ -3796,7 +3906,7 @@ var init_core = __esm(() => {
3796
3906
  });
3797
3907
 
3798
3908
  // src/orchestrator/index/store.ts
3799
- import { mkdir as mkdir2, open, readFile as readFile3, rename, unlink as unlink3 } from "fs/promises";
3909
+ import { mkdir as mkdir3, open, readFile as readFile3, rename, unlink as unlink3 } from "fs/promises";
3800
3910
  import { homedir as homedir8 } from "os";
3801
3911
  import { dirname as dirname4, join as join6 } from "path";
3802
3912
 
@@ -3870,7 +3980,7 @@ class TaskIndexStore {
3870
3980
  return next;
3871
3981
  }
3872
3982
  async doSave() {
3873
- await mkdir2(dirname4(this.path), { recursive: true });
3983
+ await mkdir3(dirname4(this.path), { recursive: true });
3874
3984
  const payload = this.snapshot();
3875
3985
  const json = `${JSON.stringify(payload, null, 2)}
3876
3986
  `;
@@ -4421,7 +4531,7 @@ init_paths();
4421
4531
  // src/daemon/server.ts
4422
4532
  init_repos();
4423
4533
  init_paths();
4424
- import { mkdir as mkdir3, readFile as readFile5, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
4534
+ import { mkdir as mkdir4, readFile as readFile5, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
4425
4535
  import { createServer as createServer2 } from "net";
4426
4536
  import { dirname as dirname5 } from "path";
4427
4537
 
@@ -4761,8 +4871,8 @@ async function startDaemonServer(orch, options = {}) {
4761
4871
  const startedAt = options.startedAt ?? new Date;
4762
4872
  const clients = new Set;
4763
4873
  let nextClientId = 1;
4764
- await mkdir3(dirname5(socketPath), { recursive: true });
4765
- await mkdir3(dirname5(pidPath), { recursive: true });
4874
+ await mkdir4(dirname5(socketPath), { recursive: true });
4875
+ await mkdir4(dirname5(pidPath), { recursive: true });
4766
4876
  await unlink4(socketPath).catch(() => {});
4767
4877
  const server = createServer2((socket) => {
4768
4878
  const client = {
@@ -4820,7 +4930,7 @@ async function startDaemonServer(orch, options = {}) {
4820
4930
  resolve2();
4821
4931
  });
4822
4932
  });
4823
- await writeFile3(pidPath, `${process.pid}
4933
+ await writeFile4(pidPath, `${process.pid}
4824
4934
  `, "utf8");
4825
4935
  async function stopSoon() {
4826
4936
  await options.onStop?.();
@@ -4990,6 +5100,10 @@ async function startDaemonServer(orch, options = {}) {
4990
5100
  await orch.interruptTask(requireString2(payload, "taskId"), optionalString2(payload, "tabId"));
4991
5101
  return {};
4992
5102
  }
5103
+ case "chat.steer": {
5104
+ await orch.steerTask(requireString2(payload, "taskId"), requireString2(payload, "text"), optionalString2(payload, "tabId"));
5105
+ return {};
5106
+ }
4993
5107
  case "chat.input.pending": {
4994
5108
  return { pending: orch.peekPendingInput(requireString2(payload, "taskId")) };
4995
5109
  }