@justin06lee/yagami 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -122,7 +122,7 @@ const engine = new YagamiEngine({
122
122
  const { response, costUsd } = await engine.complete({ messages: [{ role: "user", content: "hello" }] });
123
123
  ```
124
124
 
125
- Every provider implements one small `Provider` contract (`run(turn)` → normalized `session`/`text`/`thinking`/`done` events, plus `listModels()` and `version()`), so adding a harness that isn't ACP-capable is one file. Failures are typed: `AuthRequiredError` (carries the login command), `ProviderNotInstalledError` (carries the install hint), `ProviderError`.
125
+ Every provider implements one small `Provider` contract (`run(turn)` → normalized `session`/`text`/`thinking`/`done` events, plus `listModels()` and `version()`), so adding a harness that isn't ACP-capable is one file. Failures are typed: `AuthRequiredError` (carries the login command), `ProviderNotInstalledError` (carries the install hint), `ProviderError`. Every CLI yagami starts is ended with everything it started in turn — a launcher that re-executes itself, an npm shim, an agent's MCP servers — and nothing is left waiting on an agent that never answers: an ACP handshake has 30 seconds, a model-list or version probe 20, and a turn aborted before its session exists closes the agent. A consumer that stops reading a turn early ends its process too.
126
126
 
127
127
  ### Building a UI on Claude Code
128
128
 
@@ -183,10 +183,10 @@ function resolveClaudeExecutable(explicit) {
183
183
  }
184
184
 
185
185
  // src/version.ts
186
- var VERSION = "0.8.1";
186
+ var VERSION = "0.8.2";
187
187
 
188
188
  // src/core/providers/acp.ts
189
- import { spawn, spawnSync } from "child_process";
189
+ import { spawn, spawnSync as spawnSync2 } from "child_process";
190
190
  import * as fs2 from "fs";
191
191
  import * as os2 from "os";
192
192
  import * as path2 from "path";
@@ -288,12 +288,75 @@ function elicitationResponse(response) {
288
288
  return response.action === "accept" ? { action: "accept", content: response.values ?? null } : { action: response.action };
289
289
  }
290
290
 
291
+ // src/core/providers/process.ts
292
+ import { execFileSync, spawnSync } from "child_process";
293
+ var GRACE_MS = 3e3;
294
+ var ending = /* @__PURE__ */ new WeakSet();
295
+ function descendants(root) {
296
+ let listing;
297
+ try {
298
+ listing = execFileSync("ps", ["-A", "-o", "pid=,ppid="], { encoding: "utf8", timeout: 2e3 });
299
+ } catch {
300
+ return [];
301
+ }
302
+ const children = /* @__PURE__ */ new Map();
303
+ for (const line of listing.split("\n")) {
304
+ const [pid, ppid] = line.trim().split(/\s+/).map(Number);
305
+ if (!pid || ppid === void 0 || Number.isNaN(ppid)) continue;
306
+ const list = children.get(ppid);
307
+ if (list) list.push(pid);
308
+ else children.set(ppid, [pid]);
309
+ }
310
+ const found = [];
311
+ const stack = [root];
312
+ while (stack.length > 0) {
313
+ for (const child of children.get(stack.pop()) ?? []) {
314
+ found.push(child);
315
+ stack.push(child);
316
+ }
317
+ }
318
+ return found;
319
+ }
320
+ function isAlive(pid) {
321
+ try {
322
+ process.kill(pid, 0);
323
+ return true;
324
+ } catch (err) {
325
+ return err.code === "EPERM";
326
+ }
327
+ }
328
+ function signal(pid, sig) {
329
+ try {
330
+ process.kill(pid, sig);
331
+ } catch {
332
+ }
333
+ }
334
+ function killTree(child, graceMs = GRACE_MS) {
335
+ if (!child || child.pid === void 0 || ending.has(child)) return;
336
+ if (child.exitCode !== null || child.signalCode !== null) return;
337
+ ending.add(child);
338
+ const pid = child.pid;
339
+ if (process.platform === "win32") {
340
+ spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
341
+ return;
342
+ }
343
+ const tree = [pid, ...descendants(pid)];
344
+ for (const member of tree) signal(member, "SIGTERM");
345
+ const timer = setTimeout(() => {
346
+ for (const member of tree) if (isAlive(member)) signal(member, "SIGKILL");
347
+ }, graceMs);
348
+ timer.unref?.();
349
+ }
350
+
291
351
  // src/core/providers/queue.ts
292
352
  var AsyncQueue = class {
293
353
  buffer = [];
294
354
  waiting = null;
295
355
  ended = false;
296
356
  error = void 0;
357
+ /** Called once if the consumer stops iterating before the queue ends —
358
+ * the producer's cue to stop whatever is feeding it. */
359
+ onReturn;
297
360
  push(value) {
298
361
  if (this.ended) return;
299
362
  if (this.waiting) {
@@ -341,7 +404,10 @@ var AsyncQueue = class {
341
404
  });
342
405
  },
343
406
  return: () => {
407
+ const stop = this.ended ? void 0 : this.onReturn;
408
+ this.onReturn = void 0;
344
409
  this.ended = true;
410
+ stop?.();
345
411
  return Promise.resolve({ value: void 0, done: true });
346
412
  }
347
413
  };
@@ -349,6 +415,8 @@ var AsyncQueue = class {
349
415
  };
350
416
 
351
417
  // src/core/providers/acp.ts
418
+ var HANDSHAKE_TIMEOUT_MS = 3e4;
419
+ var PROBE_TIMEOUT_MS = 2e4;
352
420
  function rejectOption(p) {
353
421
  const pick = p.options.find((o) => o.kind === "reject_once") ?? p.options.find((o) => o.kind === "reject_always") ?? p.options[0];
354
422
  if (!pick) return { outcome: { outcome: "cancelled" } };
@@ -377,6 +445,8 @@ var AcpProvider = class {
377
445
  appName;
378
446
  modelConfigId;
379
447
  connectImpl;
448
+ handshakeTimeoutMs;
449
+ probeTimeoutMs;
380
450
  constructor(options) {
381
451
  this.id = options.id;
382
452
  this.label = options.label;
@@ -390,6 +460,8 @@ var AcpProvider = class {
390
460
  this.appName = options.appName ?? "yagami";
391
461
  this.modelConfigId = options.modelConfigId ?? "model";
392
462
  this.connectImpl = options.connect ?? ((cwd) => this.spawnConnection(cwd));
463
+ this.handshakeTimeoutMs = options.handshakeTimeoutMs ?? HANDSHAKE_TIMEOUT_MS;
464
+ this.probeTimeoutMs = options.probeTimeoutMs ?? PROBE_TIMEOUT_MS;
393
465
  fs2.mkdirSync(this.workDir, { recursive: true });
394
466
  }
395
467
  spawnConnection(cwd) {
@@ -425,14 +497,23 @@ var AcpProvider = class {
425
497
  stream
426
498
  );
427
499
  let settled = false;
500
+ const handshake = setTimeout(() => {
501
+ if (settled) return;
502
+ settled = true;
503
+ killTree(child);
504
+ reject(new ProviderError(this.id, `${this.label} did not finish the ACP handshake within ${Math.round(this.handshakeTimeoutMs / 1e3)}s`));
505
+ }, this.handshakeTimeoutMs);
506
+ handshake.unref?.();
428
507
  child.on("error", (err) => {
429
508
  if (settled) return;
430
509
  settled = true;
510
+ clearTimeout(handshake);
431
511
  reject(classifyProviderFailure(this.id, this.loginCommand, err));
432
512
  });
433
513
  child.on("exit", (code) => {
434
514
  if (settled) return;
435
515
  settled = true;
516
+ clearTimeout(handshake);
436
517
  reject(classifyProviderFailure(this.id, this.loginCommand, new Error(`${this.executable} exited with code ${code}${stderr ? `: ${stderr.trim().slice(-400)}` : ""}`)));
437
518
  });
438
519
  agent.initialize({
@@ -448,20 +529,20 @@ var AcpProvider = class {
448
529
  }).then((init) => {
449
530
  if (settled) return;
450
531
  settled = true;
532
+ clearTimeout(handshake);
451
533
  resolve({
452
534
  agent,
453
535
  init,
454
536
  setHandlers: (h) => {
455
537
  handlers = h;
456
538
  },
457
- close: () => {
458
- child.kill("SIGTERM");
459
- }
539
+ close: () => killTree(child)
460
540
  });
461
541
  }).catch((err) => {
462
542
  if (settled) return;
463
543
  settled = true;
464
- child.kill("SIGTERM");
544
+ clearTimeout(handshake);
545
+ killTree(child);
465
546
  reject(this.classify(err, stderr));
466
547
  });
467
548
  });
@@ -482,7 +563,13 @@ var AcpProvider = class {
482
563
  const onAbort = () => {
483
564
  if (sessionId) void conn.agent.cancel({ sessionId }).catch(() => {
484
565
  });
566
+ else conn.close();
485
567
  };
568
+ if (req.signal?.aborted) {
569
+ conn.close();
570
+ return;
571
+ }
572
+ req.signal?.addEventListener("abort", onAbort, { once: true });
486
573
  try {
487
574
  let configOptions;
488
575
  let modes;
@@ -528,7 +615,6 @@ var AcpProvider = class {
528
615
  }
529
616
  }
530
617
  });
531
- req.signal?.addEventListener("abort", onAbort, { once: true });
532
618
  conn.agent.prompt({ sessionId, prompt: toAcpBlocks(req.prompt, req.media ?? [], this.id) }).then((res) => {
533
619
  queue.push({
534
620
  type: "done",
@@ -581,9 +667,34 @@ var AcpProvider = class {
581
667
  throw this.classify(err);
582
668
  });
583
669
  }
584
- async listModels() {
670
+ /**
671
+ * Run a short question against a fresh agent and close it, whatever
672
+ * happens. The deadline covers the whole exchange: an agent that answers
673
+ * the handshake and then sits on newSession forever (Gemini, signed out
674
+ * or mid-update) used to hold the probe open — and its process alive —
675
+ * for as long as the host ran.
676
+ */
677
+ async probe(ask) {
585
678
  const conn = await this.connectImpl(this.workDir);
679
+ let timer;
586
680
  try {
681
+ return await Promise.race([
682
+ ask(conn),
683
+ new Promise((_, reject) => {
684
+ timer = setTimeout(
685
+ () => reject(new ProviderError(this.id, `${this.label} did not answer within ${Math.round(this.probeTimeoutMs / 1e3)}s`)),
686
+ this.probeTimeoutMs
687
+ );
688
+ timer.unref?.();
689
+ })
690
+ ]);
691
+ } finally {
692
+ if (timer) clearTimeout(timer);
693
+ conn.close();
694
+ }
695
+ }
696
+ async listModels() {
697
+ return this.probe(async (conn) => {
587
698
  const created = await conn.agent.newSession({ cwd: this.workDir, mcpServers: [] }).catch((err) => {
588
699
  throw this.classify(err);
589
700
  });
@@ -603,9 +714,7 @@ var AcpProvider = class {
603
714
  ...efforts.length > 0 ? { reasoning_efforts: efforts } : {},
604
715
  ...effortOption?.type === "select" ? { default_reasoning_effort: effortOption.currentValue } : {}
605
716
  }));
606
- } finally {
607
- conn.close();
608
- }
717
+ });
609
718
  }
610
719
  /**
611
720
  * The agent's self-reported name/version from the ACP handshake. When the
@@ -628,7 +737,7 @@ var AcpProvider = class {
628
737
  }
629
738
  let plain;
630
739
  try {
631
- const out = spawnSync(this.executable, ["--version"], { encoding: "utf8", timeout: 1e4 });
740
+ const out = spawnSync2(this.executable, ["--version"], { encoding: "utf8", timeout: 1e4 });
632
741
  plain = out.stdout?.trim().split("\n")[0] || void 0;
633
742
  } catch {
634
743
  plain = void 0;
@@ -925,7 +1034,7 @@ function jsonLinesOnly(input, onNoise) {
925
1034
 
926
1035
  // src/core/providers/claude.ts
927
1036
  import { createRequire } from "module";
928
- import { spawnSync as spawnSync2 } from "child_process";
1037
+ import { spawnSync as spawnSync3 } from "child_process";
929
1038
  import * as fs3 from "fs";
930
1039
  import * as os3 from "os";
931
1040
  import * as path3 from "path";
@@ -1094,7 +1203,7 @@ var ClaudeProvider = class {
1094
1203
  }
1095
1204
  async version() {
1096
1205
  try {
1097
- const out = spawnSync2(this.executable, ["--version"], { encoding: "utf8", timeout: 1e4 });
1206
+ const out = spawnSync3(this.executable, ["--version"], { encoding: "utf8", timeout: 1e4 });
1098
1207
  return out.stdout?.trim().split("\n")[0] || void 0;
1099
1208
  } catch {
1100
1209
  return void 0;
@@ -1168,7 +1277,7 @@ function mediaPrompt(text, media) {
1168
1277
  }
1169
1278
 
1170
1279
  // src/core/providers/codex.ts
1171
- import { spawn as spawn4, spawnSync as spawnSync3 } from "child_process";
1280
+ import { spawn as spawn4, spawnSync as spawnSync4 } from "child_process";
1172
1281
  import * as fs4 from "fs";
1173
1282
  import * as os4 from "os";
1174
1283
  import * as path4 from "path";
@@ -1553,17 +1662,17 @@ var CodexAgentSession = class {
1553
1662
  }
1554
1663
  }
1555
1664
  // ── approvals: forwarded to the host, answered like the TUI would ──
1556
- async decide(request, signal) {
1557
- if (signal?.aborted) return "deny";
1665
+ async decide(request, signal2) {
1666
+ if (signal2?.aborted) return "deny";
1558
1667
  try {
1559
- const decision = await this.config.options.permissions.decide(request, signal);
1668
+ const decision = await this.config.options.permissions.decide(request, signal2);
1560
1669
  this.push({ type: "permission", request, decision });
1561
1670
  return decision;
1562
1671
  } catch {
1563
1672
  return "deny";
1564
1673
  }
1565
1674
  }
1566
- async handleServerRequest(method, id, params, signal) {
1675
+ async handleServerRequest(method, id, params, signal2) {
1567
1676
  switch (method) {
1568
1677
  case "item/commandExecution/requestApproval": {
1569
1678
  const decision = await this.decide({
@@ -1574,7 +1683,7 @@ var CodexAgentSession = class {
1574
1683
  title: String(params["command"] ?? "command"),
1575
1684
  input: { command: params["command"], cwd: params["cwd"], reason: params["reason"] },
1576
1685
  raw: params
1577
- }, signal);
1686
+ }, signal2);
1578
1687
  this.respond(id, {
1579
1688
  decision: decision === "allow" ? "accept" : decision === "allow_always" ? "acceptForSession" : "decline"
1580
1689
  });
@@ -1589,7 +1698,7 @@ var CodexAgentSession = class {
1589
1698
  title: String(params["reason"] ?? "apply file changes"),
1590
1699
  input: { reason: params["reason"], grantRoot: params["grantRoot"] },
1591
1700
  raw: params
1592
- }, signal);
1701
+ }, signal2);
1593
1702
  this.respond(id, {
1594
1703
  decision: decision === "allow" ? "accept" : decision === "allow_always" ? "acceptForSession" : "decline"
1595
1704
  });
@@ -1605,7 +1714,7 @@ var CodexAgentSession = class {
1605
1714
  title: String(params["reason"] ?? "extra permissions"),
1606
1715
  input: requested,
1607
1716
  raw: params
1608
- }, signal);
1717
+ }, signal2);
1609
1718
  const granted = decision === "allow" || decision === "allow_always";
1610
1719
  this.respond(id, {
1611
1720
  permissions: granted ? { network: requested?.["network"] ?? void 0, fileSystem: requested?.["fileSystem"] ?? void 0 } : {},
@@ -1624,14 +1733,14 @@ var CodexAgentSession = class {
1624
1733
  title: String(params["command"] ?? params["reason"] ?? "approval"),
1625
1734
  input: params,
1626
1735
  raw: params
1627
- }, signal);
1736
+ }, signal2);
1628
1737
  this.respond(id, {
1629
1738
  decision: decision === "allow" ? "approved" : decision === "allow_always" ? "approved_for_session" : { denied: { rejection: "denied by the user" } }
1630
1739
  });
1631
1740
  break;
1632
1741
  }
1633
1742
  case "item/tool/requestUserInput": {
1634
- const response = await this.input(codexQuestionRequest(this.threadId, params), signal);
1743
+ const response = await this.input(codexQuestionRequest(this.threadId, params), signal2);
1635
1744
  const values = response.action === "accept" ? response.values ?? {} : {};
1636
1745
  this.respond(id, {
1637
1746
  answers: Object.fromEntries(
@@ -1644,7 +1753,7 @@ var CodexAgentSession = class {
1644
1753
  break;
1645
1754
  }
1646
1755
  case "mcpServer/elicitation/request": {
1647
- const response = await this.input(elicitationRequest("codex", this.threadId, params), signal);
1756
+ const response = await this.input(elicitationRequest("codex", this.threadId, params), signal2);
1648
1757
  this.respond(id, { ...elicitationResponse(response), _meta: null });
1649
1758
  break;
1650
1759
  }
@@ -1661,12 +1770,12 @@ var CodexAgentSession = class {
1661
1770
  }
1662
1771
  }
1663
1772
  }
1664
- async input(request, signal) {
1773
+ async input(request, signal2) {
1665
1774
  const handler = this.config.options.input;
1666
1775
  if (!handler) return declineInput();
1667
1776
  try {
1668
- if (signal?.aborted) return { action: "cancel" };
1669
- return await handler.respond(request, signal);
1777
+ if (signal2?.aborted) return { action: "cancel" };
1778
+ return await handler.respond(request, signal2);
1670
1779
  } catch {
1671
1780
  return { action: "cancel" };
1672
1781
  }
@@ -1733,7 +1842,7 @@ var CodexAgentSession = class {
1733
1842
  if (this.closed) return;
1734
1843
  this.closed = true;
1735
1844
  this.fail(new ProviderError("codex", "session closed"));
1736
- this.child?.kill("SIGTERM");
1845
+ killTree(this.child);
1737
1846
  this.child = void 0;
1738
1847
  }
1739
1848
  };
@@ -1824,12 +1933,17 @@ function spawnJsonl(options) {
1824
1933
  }
1825
1934
  });
1826
1935
  const onAbort = () => {
1827
- child.kill("SIGTERM");
1936
+ killTree(child);
1828
1937
  queue.end();
1829
1938
  };
1830
1939
  options.signal?.addEventListener("abort", onAbort, { once: true });
1940
+ queue.onReturn = () => {
1941
+ options.signal?.removeEventListener("abort", onAbort);
1942
+ killTree(child);
1943
+ };
1831
1944
  child.on("error", (err) => queue.fail(err));
1832
1945
  child.on("close", (code) => {
1946
+ queue.onReturn = void 0;
1833
1947
  options.signal?.removeEventListener("abort", onAbort);
1834
1948
  if (options.signal?.aborted) return queue.end();
1835
1949
  if (code !== 0) queue.fail(new ProcessExitError(code, stderr));
@@ -1955,7 +2069,7 @@ var CodexProvider = class {
1955
2069
  if (settled) return;
1956
2070
  settled = true;
1957
2071
  clearTimeout(timer);
1958
- child.kill("SIGTERM");
2072
+ killTree(child);
1959
2073
  fn();
1960
2074
  };
1961
2075
  const timer = setTimeout(() => finish(() => reject(new ProviderError(this.id, "timed out listing models via app-server"))), 15e3);
@@ -2028,7 +2142,7 @@ var CodexProvider = class {
2028
2142
  }
2029
2143
  async version() {
2030
2144
  try {
2031
- const out = spawnSync3(this.executable, ["--version"], { encoding: "utf8", timeout: 1e4 });
2145
+ const out = spawnSync4(this.executable, ["--version"], { encoding: "utf8", timeout: 1e4 });
2032
2146
  return out.stdout?.trim().split("\n")[0] || void 0;
2033
2147
  } catch {
2034
2148
  return void 0;
@@ -2817,7 +2931,7 @@ ${promptText}`;
2817
2931
  };
2818
2932
  }
2819
2933
  async *runStream(req, prepared, streamOptions) {
2820
- const { signal } = streamOptions;
2934
+ const { signal: signal2 } = streamOptions;
2821
2935
  let emitted = false;
2822
2936
  try {
2823
2937
  for await (const ev of this.attemptStream(prepared, streamOptions)) {
@@ -2826,7 +2940,7 @@ ${promptText}`;
2826
2940
  }
2827
2941
  return;
2828
2942
  } catch (err) {
2829
- if (signal?.aborted) return;
2943
+ if (signal2?.aborted) return;
2830
2944
  const fallback = emitted ? void 0 : this.prepareResumeFallback(req, prepared);
2831
2945
  if (!fallback) {
2832
2946
  yield { event: "error", data: toApiError(err).toBody() };
@@ -2835,13 +2949,13 @@ ${promptText}`;
2835
2949
  try {
2836
2950
  yield* this.attemptStream(fallback, streamOptions);
2837
2951
  } catch (err2) {
2838
- if (!signal?.aborted) yield { event: "error", data: toApiError(err2).toBody() };
2952
+ if (!signal2?.aborted) yield { event: "error", data: toApiError(err2).toBody() };
2839
2953
  }
2840
2954
  }
2841
2955
  }
2842
2956
  async *attemptStream(prepared, streamOptions) {
2843
2957
  const { provider, turn, norm, requestedModel } = prepared;
2844
- const { signal } = streamOptions;
2958
+ const { signal: signal2 } = streamOptions;
2845
2959
  const stripper = norm.prefill ? new PrefillStripper(norm.prefill) : void 0;
2846
2960
  const sse = new SseSynthesizer(`msg_${randomUUID().replace(/-/g, "")}`, requestedModel);
2847
2961
  let sessionId;
@@ -2853,7 +2967,7 @@ ${promptText}`;
2853
2967
  started = true;
2854
2968
  return sse.start();
2855
2969
  };
2856
- for await (const ev of provider.run({ ...turn, ...signal ? { signal } : {} })) {
2970
+ for await (const ev of provider.run({ ...turn, ...signal2 ? { signal: signal2 } : {} })) {
2857
2971
  if (ev.type === "session") {
2858
2972
  sessionId = ev.sessionId;
2859
2973
  } else if (ev.type === "text") {
@@ -2868,7 +2982,7 @@ ${promptText}`;
2868
2982
  done = ev;
2869
2983
  }
2870
2984
  }
2871
- if (signal?.aborted) return;
2985
+ if (signal2?.aborted) return;
2872
2986
  if (!done) throw new ProviderError(provider.id, "turn ended without a result");
2873
2987
  yield* start();
2874
2988
  if (stripper) {
@@ -3179,4 +3293,4 @@ export {
3179
3293
  ChatChunkTranslator,
3180
3294
  modelListBody
3181
3295
  };
3182
- //# sourceMappingURL=chunk-U3RFT7QV.js.map
3296
+ //# sourceMappingURL=chunk-EKN223KD.js.map