kitty-hive 0.6.12 → 0.7.1

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/index.js CHANGED
@@ -86,6 +86,115 @@ async function cmdCodexChannel() {
86
86
  // Block until child exits (handled above)
87
87
  await new Promise(() => { });
88
88
  }
89
+ // codex-pane <subcommand>: launcher-facing utilities for path B (visible codex
90
+ // via `codex --remote`). Currently has one subcommand:
91
+ // ws --key <K> | --id <I> Print the daemon's ws_url + thread_id as JSON,
92
+ // so a launcher (kitty-kitty etc.) can spawn
93
+ // `codex --remote <ws_url>` inside a pane. Polls
94
+ // with exponential backoff until status='ready'
95
+ // or hard timeout.
96
+ async function cmdCodexPane() {
97
+ const sub = args[1];
98
+ if (sub !== 'ws') {
99
+ console.error('Usage: kitty-hive codex-pane ws --key <K> | --id <I> [--port 4123] [--timeout-ms 5000]');
100
+ process.exit(2);
101
+ }
102
+ const { port, dbPath } = parseFlags(1);
103
+ let agentKey = '';
104
+ let agentId = '';
105
+ let timeoutMs = 5000;
106
+ for (let i = 2; i < args.length; i++) {
107
+ if (args[i] === '--key' && args[i + 1]) {
108
+ agentKey = args[i + 1];
109
+ i++;
110
+ }
111
+ else if (args[i] === '--id' && args[i + 1]) {
112
+ agentId = args[i + 1];
113
+ i++;
114
+ }
115
+ else if (args[i] === '--timeout-ms' && args[i + 1]) {
116
+ timeoutMs = parseInt(args[i + 1], 10) || 5000;
117
+ i++;
118
+ }
119
+ else if (args[i] === '--port' || args[i] === '-p') {
120
+ i++;
121
+ }
122
+ else if (args[i] === '--db') {
123
+ i++;
124
+ }
125
+ }
126
+ if (!agentKey && !agentId) {
127
+ console.error('Usage: kitty-hive codex-pane ws --key <K> | --id <I>');
128
+ process.exit(2);
129
+ }
130
+ // Backoff: 200ms / 400 / 800 / 1600 / 3200 / cap at remaining time
131
+ const start = Date.now();
132
+ const delays = [200, 400, 800, 1600, 3200];
133
+ let attempt = 0;
134
+ let lastBody = null;
135
+ while (true) {
136
+ try {
137
+ const url = new URL(`http://127.0.0.1:${port}/admin/codex-daemons`);
138
+ const res = await fetch(url);
139
+ if (res.ok) {
140
+ const { daemons } = await res.json();
141
+ const match = daemons.find(d => (agentId && d.agent_id === agentId) ||
142
+ // No external_key in admin snapshot, so we have to look up by id.
143
+ // For agent_key path, resolve via DB locally.
144
+ false);
145
+ let chosen = match;
146
+ if (!chosen && agentKey) {
147
+ // Resolve key → id via DB (use the same dbPath as serve uses)
148
+ const db = initDB(dbPath);
149
+ const row = db.prepare('SELECT id FROM agents WHERE external_key = ?').get(agentKey);
150
+ if (row)
151
+ chosen = daemons.find(d => d.agent_id === row.id);
152
+ }
153
+ if (chosen) {
154
+ if (chosen.ready && chosen.ws_url && chosen.thread_id) {
155
+ console.log(JSON.stringify({
156
+ status: 'ready',
157
+ agent_id: chosen.agent_id,
158
+ display_name: chosen.display_name,
159
+ ws_url: chosen.ws_url,
160
+ thread_id: chosen.thread_id,
161
+ pid: chosen.pid,
162
+ }));
163
+ process.exit(0);
164
+ }
165
+ // daemon row exists but its codex app-server hasn't reported ready
166
+ // back yet — wrap in the contract shape so callers can switch on
167
+ // `status` consistently (no raw daemon snapshot leaking out).
168
+ lastBody = {
169
+ status: 'starting',
170
+ agent_id: chosen.agent_id,
171
+ display_name: chosen.display_name,
172
+ pid: chosen.pid,
173
+ uptime_ms: chosen.uptime_ms,
174
+ restart_count: chosen.restart_count,
175
+ };
176
+ }
177
+ else {
178
+ lastBody = { status: 'not_supervised', error: 'no daemon for that agent_key/id (agent not registered with tool=codex, or supervisor not running)' };
179
+ }
180
+ }
181
+ }
182
+ catch (err) {
183
+ lastBody = { status: 'error', error: String(err?.message || err) };
184
+ }
185
+ if (Date.now() - start >= timeoutMs)
186
+ break;
187
+ const delay = delays[Math.min(attempt, delays.length - 1)];
188
+ attempt++;
189
+ await new Promise(r => setTimeout(r, Math.min(delay, Math.max(50, timeoutMs - (Date.now() - start)))));
190
+ }
191
+ // Timeout — emit what we last saw, exit non-zero
192
+ console.log(JSON.stringify(lastBody || {
193
+ status: 'timeout',
194
+ error: `daemon did not reach ready within ${timeoutMs}ms`,
195
+ }));
196
+ process.exit(1);
197
+ }
89
198
  const INIT_TOOLS = ['claude', 'cursor', 'vscode'];
90
199
  // codex is config'd via its own CLI (`codex mcp add ... --url ...`) writing
91
200
  // to ~/.codex/config.toml — we shell out instead of editing toml ourselves.
@@ -397,6 +506,36 @@ async function cmdStatus() {
397
506
  console.log(renderTable(['ID', 'NAME', 'PEER', 'STATUS', 'LAST SEEN'], rrows, ' '));
398
507
  }
399
508
  }
509
+ // Codex daemon supervisor snapshot — fetched live via admin endpoint
510
+ // so we get the actual running state, not just the DB intent.
511
+ try {
512
+ const daemonsRes = await fetch(`http://localhost:${port}/admin/codex-daemons`);
513
+ if (daemonsRes.ok) {
514
+ const { daemons } = await daemonsRes.json();
515
+ const codexAgents = agents.filter((a) => a.tool === 'codex');
516
+ if (codexAgents.length > 0 || daemons.length > 0) {
517
+ console.log(`\n🤖 Codex daemons`);
518
+ const liveByName = new Map(daemons.map(d => [d.display_name, d]));
519
+ const rows = codexAgents.map((a) => {
520
+ const live = liveByName.get(a.display_name);
521
+ if (live) {
522
+ const min = Math.floor(live.uptime_ms / 60000);
523
+ const uptimeStr = min < 60 ? `${min}m` : `${Math.floor(min / 60)}h${min % 60}m`;
524
+ return [a.display_name, `pid=${live.pid}`, `up ${uptimeStr}`, `restarts=${live.restart_count}`];
525
+ }
526
+ return [a.display_name, '(not running)', '', ''];
527
+ });
528
+ // Also surface zombie daemons (running but agent row deleted)
529
+ for (const d of daemons) {
530
+ if (!codexAgents.some((a) => a.display_name === d.display_name)) {
531
+ rows.push([`${d.display_name} ⚠️ orphan`, `pid=${d.pid}`, '', `restarts=${d.restart_count}`]);
532
+ }
533
+ }
534
+ console.log(renderTable(['NAME', 'PID', 'UPTIME', 'RESTARTS'], rows, ' '));
535
+ }
536
+ }
537
+ }
538
+ catch { /* serve might not expose admin yet — silently skip */ }
400
539
  }
401
540
  catch {
402
541
  console.log(`\n⚠️ Cannot read database`);
@@ -425,7 +564,7 @@ async function cmdDbClear() {
425
564
  console.log('✅ Database cleared.');
426
565
  }
427
566
  async function cmdAgentRemove() {
428
- const { dbPath } = parseFlags(1);
567
+ const { dbPath, port } = parseFlags(1);
429
568
  // Flags: --key <K> look up by external_key (idempotent: missing = exit 0)
430
569
  // --yes skip confirmation (scripts)
431
570
  // --transfer-to <a> transfer hosted teams to this agent before deleting
@@ -635,6 +774,27 @@ async function cmdAgentRemove() {
635
774
  db.prepare('DELETE FROM teams WHERE host_agent_id = ?').run(agent.id);
636
775
  db.prepare('DELETE FROM agents WHERE id = ?').run(agent.id);
637
776
  console.log(`✅ Removed agent "${name}".`);
777
+ // Tell the live supervisor (if any) to kill the daemon for this agent.
778
+ // Without this, a deleted tool=codex agent's daemon keeps running on its
779
+ // old ws_url, causing routing schisms when another agent later self-
780
+ // registers with the same name. Mirrors the agent-register notify path.
781
+ // Best-effort: failures silently ignored — agent row is already gone.
782
+ try {
783
+ const notifyRes = await fetch(`http://127.0.0.1:${port}/admin/notify-agent-removed`, {
784
+ method: 'POST',
785
+ headers: { 'Content-Type': 'application/json' },
786
+ body: JSON.stringify({ agent_id: agent.id }),
787
+ signal: AbortSignal.timeout(2000),
788
+ });
789
+ if (notifyRes.ok) {
790
+ const { killed } = await notifyRes.json();
791
+ if (killed)
792
+ console.error(` → supervisor killed codex daemon`);
793
+ }
794
+ }
795
+ catch {
796
+ // serve not running, or admin endpoint not reachable — fine, no daemon to kill anyway
797
+ }
638
798
  }
639
799
  async function cmdAgentRegister() {
640
800
  // Idempotent upsert by external_key. Designed for `spawn('kitty-hive',
@@ -647,6 +807,7 @@ async function cmdAgentRegister() {
647
807
  let roles = '';
648
808
  let tool = '';
649
809
  let agentIdOverride = '';
810
+ let projectDir = '';
650
811
  for (let i = 2; i < args.length; i++) {
651
812
  if (args[i] === '--key' && args[i + 1]) {
652
813
  externalKey = args[i + 1];
@@ -672,12 +833,18 @@ async function cmdAgentRegister() {
672
833
  agentIdOverride = args[i + 1];
673
834
  i++;
674
835
  }
836
+ else if (args[i] === '--project-dir' && args[i + 1]) {
837
+ projectDir = args[i + 1];
838
+ i++;
839
+ }
675
840
  }
676
841
  if (!externalKey && !agentIdOverride && !displayName) {
677
- console.error('Usage: kitty-hive agent register --key <K> --display-name <N> [--roles R] [--tool T]');
842
+ console.error('Usage: kitty-hive agent register --key <K> --display-name <N> [--roles R] [--tool T] [--project-dir P]');
678
843
  console.error(' All three of --key/--id/--display-name optional, but at least one required.');
844
+ console.error(' --project-dir: working directory hint for codex agents (passed as cwd when supervisor spawns codex app-server).');
679
845
  process.exit(1);
680
846
  }
847
+ const { port } = parseFlags(1);
681
848
  initDB(dbPath);
682
849
  const { handleStart } = await import('./tools/start.js');
683
850
  try {
@@ -687,11 +854,37 @@ async function cmdAgentRegister() {
687
854
  name: displayName || undefined,
688
855
  roles: roles || undefined,
689
856
  tool: tool || undefined,
857
+ projectDir: projectDir || undefined,
690
858
  });
691
859
  // Stdout: one line, just the agent_id (script-friendly)
692
860
  console.log(result.agent_id);
693
861
  // Stderr: human context
694
- console.error(`✅ ${result.display_name} (${result.agent_id})${externalKey ? ` key=${externalKey}` : ''}`);
862
+ console.error(`✅ ${result.display_name} (${result.agent_id})${externalKey ? ` key=${externalKey}` : ''}${projectDir ? ` cwd=${projectDir}` : ''}`);
863
+ // If serve is running and we registered a tool=codex agent, notify the
864
+ // supervisor so it can spawn a daemon for it on the fly (without this,
865
+ // dynamic spawn requires a serve restart because the in-process
866
+ // onAgentCreated hook lives in serve's process, not this CLI process).
867
+ // Best-effort: failures are silently ignored — agent row is already written.
868
+ if (tool === 'codex') {
869
+ try {
870
+ const notifyRes = await fetch(`http://127.0.0.1:${port}/admin/notify-agent-created`, {
871
+ method: 'POST',
872
+ headers: { 'Content-Type': 'application/json' },
873
+ body: JSON.stringify({ agent_id: result.agent_id }),
874
+ // Short timeout: register shouldn't block waiting for a dead serve
875
+ signal: AbortSignal.timeout(2000),
876
+ });
877
+ if (notifyRes.ok) {
878
+ const { spawned } = await notifyRes.json();
879
+ if (spawned)
880
+ console.error(` → supervisor spawning codex daemon`);
881
+ }
882
+ }
883
+ catch {
884
+ // serve not running, or admin endpoint not reachable — that's fine,
885
+ // daemon will spawn at the next serve boot.
886
+ }
887
+ }
695
888
  }
696
889
  catch (err) {
697
890
  console.error(`Failed: ${err.message ?? err}`);
@@ -2278,6 +2471,9 @@ switch (command) {
2278
2471
  case 'codex-channel':
2279
2472
  run(cmdCodexChannel);
2280
2473
  break;
2474
+ case 'codex-pane':
2475
+ run(cmdCodexPane);
2476
+ break;
2281
2477
  case 'status':
2282
2478
  run(cmdStatus);
2283
2479
  break;