@sma1lboy/kobe 0.8.69 → 0.8.71

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.
@@ -18,7 +18,7 @@ function bunTerminalDriver(spawn) {
18
18
  });
19
19
  return {
20
20
  pid: proc.pid,
21
- exited: proc.exited,
21
+ exited: proc.exited.then(() => ({ code: proc.exitCode ?? null, signal: proc.signalCode ?? null }), () => ({ code: null, signal: null })),
22
22
  write: (data) => proc.terminal?.write(data),
23
23
  resize: (cols, rows) => proc.terminal?.resize(cols, rows),
24
24
  close: () => proc.terminal?.close(),
@@ -42,7 +42,7 @@ async function nodePtyDriver(spawn) {
42
42
  settle = resolve;
43
43
  });
44
44
  child.onData((data) => request.onData(data));
45
- child.onExit(({ exitCode }) => settle(exitCode));
45
+ child.onExit(({ exitCode }) => settle({ code: exitCode, signal: null }));
46
46
  return {
47
47
  pid: child.pid,
48
48
  exited,
@@ -57,7 +57,7 @@ async function nodePtyDriver(spawn) {
57
57
  // ../kobe-daemon/src/daemon/pty-server.ts
58
58
  import { mkdir, unlink, writeFile } from "node:fs/promises";
59
59
  import { createServer } from "node:net";
60
- import { dirname } from "node:path";
60
+ import { dirname as dirname2 } from "node:path";
61
61
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
62
62
 
63
63
  // ../kobe-daemon/src/daemon/client-writer.ts
@@ -236,6 +236,9 @@ function defaultPtyHostPidPath(homeDir = process.env.KOBE_HOME_DIR ?? homedir())
236
236
  return override;
237
237
  return join(homeDir, ".kobe", "pty.pid");
238
238
  }
239
+ function defaultPtyExitsPath(homeDir = process.env.KOBE_HOME_DIR ?? homedir()) {
240
+ return join(homeDir, ".kobe", "pty-exits.json");
241
+ }
239
242
  // ../kobe-plugin-sdk/src/contract.ts
240
243
  var DAEMON_CHANNELS = [
241
244
  "task.snapshot",
@@ -269,6 +272,51 @@ function frameToLine(frame) {
269
272
  `;
270
273
  }
271
274
 
275
+ // ../kobe-daemon/src/daemon/pty-exit-store.ts
276
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
277
+ import { dirname } from "node:path";
278
+ var MAX_RECORDS = 50;
279
+ var TAIL_LINES = 40;
280
+ var TAIL_LINE_CHARS = 500;
281
+ var ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?|\x1b[@-_]/g;
282
+ function plainTail(raw) {
283
+ const plain = raw.replace(ANSI_RE, "").replace(/\r\n/g, `
284
+ `);
285
+ const lines = plain.split(`
286
+ `).map((line) => (line.split("\r").pop() ?? "").slice(0, TAIL_LINE_CHARS));
287
+ while (lines.length > 0 && (lines[lines.length - 1] ?? "").trim() === "")
288
+ lines.pop();
289
+ return lines.slice(-TAIL_LINES);
290
+ }
291
+ function readPtyExitRecords(path = defaultPtyExitsPath()) {
292
+ try {
293
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
294
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
295
+ return {};
296
+ return parsed;
297
+ } catch {
298
+ return {};
299
+ }
300
+ }
301
+ function recordPtyExit(info, path = defaultPtyExitsPath()) {
302
+ if (info.key.startsWith("::"))
303
+ return;
304
+ if (info.exit.code === 0 && info.exit.signal === null)
305
+ return;
306
+ const records = readPtyExitRecords(path);
307
+ records[info.key] = {
308
+ key: info.key,
309
+ pid: info.pid,
310
+ code: info.exit.code,
311
+ signal: info.exit.signal,
312
+ at: info.exit.at,
313
+ tail: plainTail(info.tail)
314
+ };
315
+ const newest = Object.values(records).sort((a, b) => a.at < b.at ? 1 : -1).slice(0, MAX_RECORDS);
316
+ mkdirSync(dirname(path), { recursive: true });
317
+ writeFileSync(path, JSON.stringify(Object.fromEntries(newest.map((r) => [r.key, r])), null, 2), "utf8");
318
+ }
319
+
272
320
  // ../kobe-daemon/src/daemon/pty-host.ts
273
321
  import { StringDecoder } from "node:string_decoder";
274
322
 
@@ -344,6 +392,24 @@ function embeddedTerminalEnv(base, overrides = {}) {
344
392
  }
345
393
 
346
394
  // ../kobe-daemon/src/daemon/pty-observability.ts
395
+ function describeExit(exit) {
396
+ if (exit?.signal)
397
+ return ` (signal ${exit.signal})`;
398
+ if (exit && exit.code !== null)
399
+ return ` (code ${exit.code})`;
400
+ return " (cause unknown)";
401
+ }
402
+ function ringTail(chunks, bytes, maxBytes) {
403
+ const skip = Math.max(0, bytes - maxBytes);
404
+ let seen = 0;
405
+ const parts = [];
406
+ for (const chunk of chunks) {
407
+ if (seen + chunk.byteLength > skip)
408
+ parts.push(seen >= skip ? chunk : chunk.subarray(skip - seen));
409
+ seen += chunk.byteLength;
410
+ }
411
+ return Buffer.concat(parts).toString("utf8");
412
+ }
347
413
  var OSC_TITLE_RE = /\x1b\][02];([^\x07\x1b]*)(?:\x07|\x1b\\)/g;
348
414
  var TITLE_CARRY_CAP = 1024;
349
415
  function titleCarryFrom(rest) {
@@ -370,8 +436,9 @@ function scanOscTitle(session, buf) {
370
436
  session.titleCarry = carry;
371
437
  }
372
438
  function peekRing(session, sinceOffset) {
373
- if (!session)
374
- return { exists: false, alive: false, pid: null, offset: 0, data: "", sinceValid: false };
439
+ if (!session) {
440
+ return { exists: false, alive: false, pid: null, offset: 0, data: "", sinceValid: false, exit: null };
441
+ }
375
442
  const windowStart = session.totalBytes - session.bytes;
376
443
  let buf = Buffer.concat(session.chunks);
377
444
  let sinceValid = false;
@@ -385,7 +452,8 @@ function peekRing(session, sinceOffset) {
385
452
  pid: session.proc?.pid ?? null,
386
453
  offset: session.totalBytes,
387
454
  data: buf.toString("base64"),
388
- sinceValid
455
+ sinceValid,
456
+ exit: session.exit ?? null
389
457
  };
390
458
  }
391
459
 
@@ -417,6 +485,7 @@ function signalProcessGroup(pid, signal, fallback, platform = process.platform)
417
485
  // ../kobe-daemon/src/daemon/pty-host.ts
418
486
  var DEFAULT_SCROLLBACK_CAP = 512 * 1024;
419
487
  var TERMINATION_GRACE_MS = 500;
488
+ var EXIT_TAIL_BYTES = 16 * 1024;
420
489
 
421
490
  class PtyHost {
422
491
  sessions = new Map;
@@ -434,7 +503,7 @@ class PtyHost {
434
503
  created = true;
435
504
  session = this.adoptSpare(key, spec) ?? this.spawn(key, spec);
436
505
  this.sessions.set(key, session);
437
- } else if (session.alive && (session.cols !== spec.cols || session.rows !== spec.rows)) {
506
+ } else if (session.alive && spec.cols !== undefined && spec.rows !== undefined && (session.cols !== spec.cols || session.rows !== spec.rows)) {
438
507
  this.resize(key, spec.cols, spec.rows);
439
508
  }
440
509
  session.sinks.set(token, sink);
@@ -482,11 +551,13 @@ class PtyHost {
482
551
  return null;
483
552
  this.spare = null;
484
553
  spare.key = key;
485
- if (spare.cols !== spec.cols || spare.rows !== spec.rows) {
486
- spare.cols = spec.cols;
487
- spare.rows = spec.rows;
554
+ const cols = spec.cols ?? spare.cols;
555
+ const rows = spec.rows ?? spare.rows;
556
+ if (spare.cols !== cols || spare.rows !== rows) {
557
+ spare.cols = cols;
558
+ spare.rows = rows;
488
559
  try {
489
- spare.proc?.resize(spec.cols, spec.rows);
560
+ spare.proc?.resize(cols, rows);
490
561
  } catch {
491
562
  this.markExited(spare);
492
563
  return null;
@@ -494,7 +565,7 @@ class PtyHost {
494
565
  }
495
566
  this.opts.log?.("pty", `adopted warm shell for ${key} (pid ${spare.proc?.pid})`);
496
567
  this.opts.onSessionStart?.();
497
- this.warm(spec.cwd, spare.command[0], spec.cols, spec.rows);
568
+ this.warm(spec.cwd, spare.command[0], cols, rows);
498
569
  return spare;
499
570
  }
500
571
  write(key, data) {
@@ -548,8 +619,10 @@ class PtyHost {
548
619
  pid: s.proc?.pid ?? null,
549
620
  command: s.command,
550
621
  title: s.title,
622
+ totalBytes: s.totalBytes,
551
623
  parked: s.parked,
552
- parkedScreenBytes: s.parkedScreenBytes
624
+ parkedScreenBytes: s.parkedScreenBytes,
625
+ exit: s.exit
553
626
  }));
554
627
  }
555
628
  peek(key, sinceOffset) {
@@ -599,6 +672,8 @@ class PtyHost {
599
672
  }
600
673
  spawn(key, spec, spare = false) {
601
674
  const argv = spec.command && spec.command.length > 0 ? [...spec.command] : [spec.shell ?? resolveLoginShell()];
675
+ const cols = spec.cols ?? 80;
676
+ const rows = spec.rows ?? 24;
602
677
  const session = {
603
678
  key,
604
679
  cwd: spec.cwd,
@@ -607,15 +682,16 @@ class PtyHost {
607
682
  chunks: [],
608
683
  bytes: 0,
609
684
  totalBytes: 0,
610
- cols: spec.cols,
611
- rows: spec.rows,
685
+ cols,
686
+ rows,
612
687
  command: argv,
613
688
  title: "",
614
689
  titleCarry: "",
615
690
  titleDecoder: new StringDecoder("utf8"),
616
691
  sinks: new Map,
617
692
  parked: false,
618
- parkedScreenBytes: 0
693
+ parkedScreenBytes: 0,
694
+ exit: null
619
695
  };
620
696
  try {
621
697
  session.proc = (this.opts.driver ?? bunTerminalDriver())({
@@ -623,16 +699,16 @@ class PtyHost {
623
699
  cwd: spec.cwd,
624
700
  env: embeddedTerminalEnv(process.env, {
625
701
  TERM: "xterm-256color",
626
- COLUMNS: String(spec.cols),
627
- LINES: String(spec.rows),
702
+ COLUMNS: String(cols),
703
+ LINES: String(rows),
628
704
  BASH_SILENCE_DEPRECATION_WARNING: "1",
629
705
  KOBE_TERMINAL_PTY: "1"
630
706
  }),
631
- cols: spec.cols,
632
- rows: spec.rows,
707
+ cols,
708
+ rows,
633
709
  onData: (data) => this.onData(session, data)
634
710
  });
635
- session.proc.exited.then(() => this.markExited(session), () => this.markExited(session));
711
+ session.proc.exited.then((exit) => this.markExited(session, exit), () => this.markExited(session));
636
712
  this.opts.log?.("pty", `spawned ${argv[0]} for ${key} (pid ${session.proc.pid})`);
637
713
  if (!spare)
638
714
  this.opts.onSessionStart?.();
@@ -663,21 +739,30 @@ class PtyHost {
663
739
  for (const sink of session.sinks.values())
664
740
  sink(frame);
665
741
  }
666
- markExited(session) {
742
+ markExited(session, exit) {
667
743
  if (!session.alive)
668
744
  return;
669
745
  session.alive = false;
746
+ session.exit = { code: exit?.code ?? null, signal: exit?.signal ?? null, at: new Date().toISOString() };
670
747
  try {
671
748
  session.proc?.close();
672
749
  } catch {}
673
750
  const frame = {
674
751
  type: "event",
675
752
  name: "pty.exit",
676
- payload: { key: session.key, pid: session.proc?.pid ?? null }
753
+ payload: { key: session.key, pid: session.proc?.pid ?? null, ...session.exit }
677
754
  };
678
755
  for (const sink of session.sinks.values())
679
756
  sink(frame);
680
- this.opts.log?.("pty", `session ${session.key} exited`);
757
+ this.opts.log?.("pty", `session ${session.key} exited${describeExit(session.exit)}`);
758
+ try {
759
+ this.opts.onSessionExit?.({
760
+ key: session.key,
761
+ pid: session.proc?.pid ?? null,
762
+ exit: session.exit,
763
+ tail: ringTail(session.chunks, session.bytes, EXIT_TAIL_BYTES)
764
+ });
765
+ } catch {}
681
766
  this.opts.onSessionEnd?.();
682
767
  }
683
768
  async endChild(session) {
@@ -736,13 +821,20 @@ async function startPtyHostServer(options = {}) {
736
821
  if (ptys.liveCount() === 0)
737
822
  armIdle();
738
823
  },
824
+ onSessionExit: (info) => {
825
+ try {
826
+ recordPtyExit(info);
827
+ } catch (err) {
828
+ log("pty", `exit record write failed for ${info.key}: ${err instanceof Error ? err.message : String(err)}`);
829
+ }
830
+ },
739
831
  driver: options.driver,
740
832
  log
741
833
  });
742
834
  const pipeSocket = isWindowsPipePath(socketPath);
743
835
  if (!pipeSocket)
744
- await mkdir(dirname(socketPath), { recursive: true });
745
- await mkdir(dirname(pidPath), { recursive: true });
836
+ await mkdir(dirname2(socketPath), { recursive: true });
837
+ await mkdir(dirname2(pidPath), { recursive: true });
746
838
  const server = createServer((socket) => {
747
839
  const client = {
748
840
  socket,
@@ -797,8 +889,8 @@ async function startPtyHostServer(options = {}) {
797
889
  cwd: requireString(payload, "cwd"),
798
890
  command: Array.isArray(payload.command) ? payload.command.filter((c) => typeof c === "string") : undefined,
799
891
  shell: typeof payload.shell === "string" ? payload.shell : undefined,
800
- cols: typeof payload.cols === "number" ? payload.cols : 80,
801
- rows: typeof payload.rows === "number" ? payload.rows : 24
892
+ cols: typeof payload.cols === "number" ? payload.cols : undefined,
893
+ rows: typeof payload.rows === "number" ? payload.rows : undefined
802
894
  }, client, (frame) => writeFrame(client, frame), typeof payload.sinceOffset === "number" ? payload.sinceOffset : undefined, typeof payload.sincePid === "number" ? payload.sincePid : undefined);
803
895
  }
804
896
  case "pty.write": {
@@ -3,7 +3,7 @@ name: kobe
3
3
  description: Use when controlling kobe tasks, parallel coding attempts, hosted agent sessions, task lifecycle, or the daemon-owned issue tracker from a shell.
4
4
  ---
5
5
 
6
- <!-- kobe-skill-version: 13 — bump in lockstep with KOBE_SKILL_VERSION (src/lib/skill-install.ts). -->
6
+ <!-- kobe-skill-version: 14 — bump in lockstep with KOBE_SKILL_VERSION (src/lib/skill-install.ts). -->
7
7
 
8
8
  # kobe shell control
9
9
 
@@ -140,7 +140,7 @@ logs, dashboards), don't scatter panes for work `add`/`fan-out` should own.
140
140
  | `pin --task-id ID [--pinned=false]` | Pin/unpin |
141
141
  | `set-active --task-id ID` / `--none` | Change shared active task |
142
142
  | `ensure-worktree --task-id ID` | Materialize without starting an engine |
143
- | `land --task-id ID [--strategy merge\|squash] [--delete-branch] [--then-archive]` | Merge the task's branch into the base repo's current branch |
143
+ | `land --task-id ID [--strategy merge\|squash] [--delete-branch] [--then-archive] [--remove-worktree]` | Merge the task's branch into the base repo's current branch; `--remove-worktree` cleans up the Worktree after (branch stays; dirty/self/base refused, outcome in the result's `worktree` field) |
144
144
  | `delete --task-id ID [--force]` | Destructive task + Worktree removal |
145
145
  | `discover-adoptable --repo PATH` | Find untracked Worktrees |
146
146
  | `adopt --repo PATH --worktree PATH` | Import a Worktree |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@sma1lboy/kobe",
4
- "version": "0.8.69",
4
+ "version": "0.8.71",
5
5
  "description": "TUI orchestrator for Claude Code (codename)",
6
6
  "type": "module",
7
7
  "packageManager": "bun@1.3.13",