@fancyboi999/open-tag-daemon 0.1.0 → 0.3.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.
Files changed (2) hide show
  1. package/dist/cli.mjs +1124 -29
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -3725,7 +3725,7 @@ try {
3725
3725
  // src/daemon/index.ts
3726
3726
  import os4 from "node:os";
3727
3727
  import fs3 from "node:fs";
3728
- import path7 from "node:path";
3728
+ import path12 from "node:path";
3729
3729
 
3730
3730
  // node_modules/ws/wrapper.mjs
3731
3731
  var import_stream = __toESM(require_stream(), 1);
@@ -3791,22 +3791,31 @@ function safeJson(o) {
3791
3791
  }
3792
3792
  }
3793
3793
 
3794
+ // src/daemonProtocol.ts
3795
+ var MACHINE_REJECTED_CODE = 4001;
3796
+
3794
3797
  // src/daemon/connection.ts
3798
+ var INITIAL_BACKOFF_MS = 1e3;
3799
+ var MAX_BACKOFF_MS = 3e4;
3795
3800
  var Connection = class {
3796
- constructor(url, key, onMsg, onOpen) {
3801
+ constructor(url, key, onMsg, onOpen, mkWs = (u) => new wrapper_default(u)) {
3797
3802
  this.url = url;
3798
3803
  this.key = key;
3799
3804
  this.onMsg = onMsg;
3800
3805
  this.onOpen = onOpen;
3806
+ this.mkWs = mkWs;
3801
3807
  }
3802
3808
  url;
3803
3809
  key;
3804
3810
  onMsg;
3805
3811
  onOpen;
3812
+ mkWs;
3806
3813
  ws = null;
3807
- delay = 1e3;
3814
+ delay = INITIAL_BACKOFF_MS;
3808
3815
  timer = null;
3809
3816
  should = true;
3817
+ accepted = false;
3818
+ // per-attempt: flips true once the server sends any frame (proof it accepted us, not rejected)
3810
3819
  log = createLogger("daemon:conn");
3811
3820
  connect() {
3812
3821
  this.should = true;
@@ -3824,13 +3833,17 @@ var Connection = class {
3824
3833
  if (!this.should) return;
3825
3834
  const wsUrl = this.url.replace(/^http/, "ws") + `/daemon/connect?key=${encodeURIComponent(this.key)}`;
3826
3835
  this.log.info("connecting", { url: this.url });
3827
- this.ws = new wrapper_default(wsUrl);
3836
+ this.accepted = false;
3837
+ this.ws = this.mkWs(wsUrl);
3828
3838
  this.ws.on("open", () => {
3829
- this.delay = 1e3;
3830
3839
  this.log.info("connected");
3831
3840
  this.onOpen();
3832
3841
  });
3833
3842
  this.ws.on("message", (d) => {
3843
+ if (!this.accepted) {
3844
+ this.accepted = true;
3845
+ this.delay = INITIAL_BACKOFF_MS;
3846
+ }
3834
3847
  let m;
3835
3848
  try {
3836
3849
  m = JSON.parse(d.toString());
@@ -3839,8 +3852,16 @@ var Connection = class {
3839
3852
  }
3840
3853
  this.onMsg(m);
3841
3854
  });
3842
- this.ws.on("close", () => {
3843
- this.log.warn("disconnected");
3855
+ this.ws.on("close", (code, reason) => {
3856
+ if (code === MACHINE_REJECTED_CODE) {
3857
+ this.delay = MAX_BACKOFF_MS;
3858
+ this.log.error(
3859
+ "server rejected this machine: its connection key is unknown or was removed. Re-issue the connect command (workspace \u2192 Computers \u2192 this machine \u2192 Reconnect), then restart the daemon with the new key.",
3860
+ { code, reason: reason?.toString?.() ?? String(reason ?? "") }
3861
+ );
3862
+ } else {
3863
+ this.log.warn("disconnected", { code });
3864
+ }
3844
3865
  this.scheduleReconnect();
3845
3866
  });
3846
3867
  this.ws.on("error", (e) => this.log.error("ws error", { detail: String(e?.message ?? e) }));
@@ -3852,13 +3873,13 @@ var Connection = class {
3852
3873
  this.timer = null;
3853
3874
  this.doConnect();
3854
3875
  }, this.delay);
3855
- this.delay = Math.min(this.delay * 2, 3e4);
3876
+ this.delay = Math.min(this.delay * 2, MAX_BACKOFF_MS);
3856
3877
  }
3857
3878
  };
3858
3879
 
3859
3880
  // src/daemon/agentManager.ts
3860
3881
  import { mkdir, writeFile, readFile, access, rm } from "node:fs/promises";
3861
- import path5 from "node:path";
3882
+ import path10 from "node:path";
3862
3883
  import os2 from "node:os";
3863
3884
 
3864
3885
  // src/daemon/prompt.ts
@@ -4421,6 +4442,893 @@ var codexRuntime = {
4421
4442
  }
4422
4443
  };
4423
4444
 
4445
+ // src/daemon/copilotRuntime.ts
4446
+ import { spawn as spawn3 } from "node:child_process";
4447
+ import { randomUUID } from "node:crypto";
4448
+ import { writeFileSync as writeFileSync2 } from "node:fs";
4449
+ import path5 from "node:path";
4450
+ var MAX3 = 2e3;
4451
+ var clip3 = (s) => String(s ?? "").slice(0, MAX3);
4452
+ var EFFORTS2 = /* @__PURE__ */ new Set(["none", "low", "medium", "high", "xhigh", "max"]);
4453
+ function reasoningEffort2(rc) {
4454
+ const e = rc?.reasoningEffort;
4455
+ return typeof e === "string" && EFFORTS2.has(e) ? e : null;
4456
+ }
4457
+ function summarizeToolArgs(tr) {
4458
+ let args2 = tr?.arguments;
4459
+ if (typeof args2 === "string") {
4460
+ try {
4461
+ args2 = JSON.parse(args2);
4462
+ } catch {
4463
+ return clip3(args2).slice(0, 160);
4464
+ }
4465
+ }
4466
+ if (!args2 || typeof args2 !== "object") return "";
4467
+ const v = args2.command ?? args2.path ?? args2.file_path ?? args2.filePath ?? args2.query ?? args2.pattern ?? args2.url ?? "";
4468
+ return clip3(typeof v === "string" ? v : JSON.stringify(v)).slice(0, 160);
4469
+ }
4470
+ function handleCopilotEvent(evt) {
4471
+ const out = { trajectory: [] };
4472
+ const data = evt?.data ?? {};
4473
+ switch (evt?.type) {
4474
+ case "assistant.turn_start":
4475
+ out.activity = { activity: "working", detail: "turn" };
4476
+ break;
4477
+ case "assistant.message": {
4478
+ if (data.content) out.trajectory.push({ kind: "text", text: clip3(data.content) });
4479
+ if (Array.isArray(data.toolRequests))
4480
+ for (const tr of data.toolRequests)
4481
+ out.trajectory.push({ kind: "tool", toolName: String(tr?.name ?? "tool"), toolInput: summarizeToolArgs(tr) });
4482
+ break;
4483
+ }
4484
+ case "assistant.reasoning":
4485
+ if (data.content) out.trajectory.push({ kind: "thinking", text: clip3(data.content) });
4486
+ break;
4487
+ case "result":
4488
+ if (typeof evt.sessionId === "string" && evt.sessionId) out.sessionId = evt.sessionId;
4489
+ if (typeof evt.exitCode === "number") out.exitCode = evt.exitCode;
4490
+ break;
4491
+ case "session.error":
4492
+ out.error = clip3(data.message ?? data.error ?? "copilot session error");
4493
+ break;
4494
+ }
4495
+ return out;
4496
+ }
4497
+ function buildArgs(prompt, sessionId, model, effort) {
4498
+ const args2 = ["-p", prompt, "--output-format", "json", "--allow-all", "--no-ask-user", "--session-id", sessionId];
4499
+ const m = model && model !== "default" ? model : "";
4500
+ if (m) args2.push("--model", m);
4501
+ if (effort) args2.push("--effort", effort);
4502
+ return args2;
4503
+ }
4504
+ var CopilotRun = class {
4505
+ constructor(opts, cb) {
4506
+ this.opts = opts;
4507
+ this.cb = cb;
4508
+ this.sessionId = opts.sessionId || randomUUID();
4509
+ this.env = { ...opts.env };
4510
+ delete this.env.NODE_OPTIONS;
4511
+ try {
4512
+ writeFileSync2(path5.join(opts.cwd, "AGENTS.md"), opts.systemPrompt);
4513
+ } catch (e) {
4514
+ cb.log.warn("copilot: AGENTS.md write failed", { detail: String(e) });
4515
+ }
4516
+ cb.onSession(this.sessionId);
4517
+ this.enqueue(opts.initialPrompt);
4518
+ }
4519
+ opts;
4520
+ cb;
4521
+ queue = [];
4522
+ turnBusy = false;
4523
+ stopped = false;
4524
+ proc = null;
4525
+ sessionId;
4526
+ everSucceeded = false;
4527
+ env;
4528
+ enqueue(text) {
4529
+ if (this.stopped) return;
4530
+ this.queue.push(text);
4531
+ this.pump();
4532
+ }
4533
+ pump() {
4534
+ if (this.stopped || this.turnBusy || this.queue.length === 0) return;
4535
+ this.runTurn(this.queue.shift());
4536
+ }
4537
+ runTurn(prompt) {
4538
+ this.turnBusy = true;
4539
+ this.cb.onActivity("working", "turn");
4540
+ const args2 = buildArgs(prompt, this.sessionId, this.opts.model, reasoningEffort2(this.opts.runtimeConfig));
4541
+ const proc = spawn3("copilot", args2, { cwd: this.opts.cwd, stdio: ["ignore", "pipe", "pipe"], env: this.env });
4542
+ this.proc = proc;
4543
+ let buf = "";
4544
+ let resultSeen = false;
4545
+ const errTail = [];
4546
+ let errLen = 0;
4547
+ const processLine = (ln) => {
4548
+ const t = ln.trim();
4549
+ if (!t) return;
4550
+ let evt;
4551
+ try {
4552
+ evt = JSON.parse(t);
4553
+ } catch {
4554
+ return;
4555
+ }
4556
+ const emit = handleCopilotEvent(evt);
4557
+ if (emit.exitCode !== void 0) resultSeen = true;
4558
+ if (emit.error) {
4559
+ this.cb.onTrajectory([{ kind: "text", text: "[copilot error] " + emit.error.slice(0, 500) }]);
4560
+ this.cb.onActivity("error", emit.error.slice(0, 200));
4561
+ }
4562
+ if (emit.activity) this.cb.onActivity(emit.activity.activity, emit.activity.detail);
4563
+ if (emit.trajectory.length) this.cb.onTrajectory(emit.trajectory);
4564
+ };
4565
+ proc.stdout?.on("data", (c) => {
4566
+ if (this.stopped) return;
4567
+ buf += c.toString();
4568
+ const lines = buf.split("\n");
4569
+ buf = lines.pop() ?? "";
4570
+ for (const ln of lines) processLine(ln);
4571
+ });
4572
+ proc.stderr?.on("data", (c) => {
4573
+ const t = c.toString();
4574
+ errTail.push(t);
4575
+ errLen += t.length;
4576
+ while (errLen > 4096 && errTail.length > 1) errLen -= errTail.shift().length;
4577
+ });
4578
+ proc.on("error", (e) => {
4579
+ this.proc = null;
4580
+ this.turnBusy = false;
4581
+ if (this.stopped) return;
4582
+ this.cb.log.error("copilot spawn failed", { detail: String(e?.message ?? e) });
4583
+ this.cb.onActivity("offline", "copilot not found");
4584
+ if (!this.everSucceeded) this.cb.onExit(1);
4585
+ else this.pump();
4586
+ });
4587
+ proc.on("exit", (code) => {
4588
+ if (buf.trim()) processLine(buf);
4589
+ buf = "";
4590
+ this.proc = null;
4591
+ this.turnBusy = false;
4592
+ if (this.stopped) return;
4593
+ if (code === 0 || resultSeen) {
4594
+ this.everSucceeded = true;
4595
+ this.cb.onActivity("online", "");
4596
+ this.pump();
4597
+ return;
4598
+ }
4599
+ const tail = errTail.join("").trim();
4600
+ const last = tail.split("\n").filter(Boolean).pop() || `copilot exited ${code ?? "signal"}`;
4601
+ this.cb.onTrajectory([{ kind: "text", text: "[copilot error] " + clip3(tail).slice(0, 500) }]);
4602
+ this.cb.onActivity("error", last.slice(0, 200));
4603
+ if (!this.everSucceeded) {
4604
+ this.cb.onExit(code ?? 1);
4605
+ return;
4606
+ }
4607
+ this.pump();
4608
+ });
4609
+ }
4610
+ stop() {
4611
+ this.stopped = true;
4612
+ const p = this.proc;
4613
+ this.proc = null;
4614
+ if (p) {
4615
+ try {
4616
+ p.kill("SIGTERM");
4617
+ } catch {
4618
+ }
4619
+ }
4620
+ }
4621
+ };
4622
+ var copilotRuntime = {
4623
+ name: "copilot",
4624
+ experimental: true,
4625
+ start(opts, cb) {
4626
+ const run = new CopilotRun(opts, cb);
4627
+ return { deliver: (text) => run.enqueue(text), stop: () => run.stop() };
4628
+ }
4629
+ };
4630
+
4631
+ // src/daemon/opencodeRuntime.ts
4632
+ import { spawn as spawn4 } from "node:child_process";
4633
+ import { writeFileSync as writeFileSync3 } from "node:fs";
4634
+ import path6 from "node:path";
4635
+ var MAX4 = 2e3;
4636
+ var clip4 = (s) => String(s ?? "").slice(0, MAX4);
4637
+ function variant(rc) {
4638
+ const e = rc?.reasoningEffort;
4639
+ return typeof e === "string" && e ? e : null;
4640
+ }
4641
+ function summarizeToolInput(input) {
4642
+ if (!input || typeof input !== "object") return clip4(input).slice(0, 160);
4643
+ const v = input.command ?? input.filePath ?? input.file_path ?? input.path ?? input.pattern ?? input.query ?? input.url ?? "";
4644
+ return clip4(typeof v === "string" ? v : JSON.stringify(v)).slice(0, 160);
4645
+ }
4646
+ function handleOpencodeEvent(evt) {
4647
+ const out = { trajectory: [] };
4648
+ if (typeof evt?.sessionID === "string" && evt.sessionID) out.sessionId = evt.sessionID;
4649
+ const part = evt?.part ?? {};
4650
+ switch (evt?.type) {
4651
+ case "step_start":
4652
+ out.activity = { activity: "working", detail: "turn" };
4653
+ break;
4654
+ case "text":
4655
+ if (part.text) out.trajectory.push({ kind: "text", text: clip4(part.text) });
4656
+ break;
4657
+ case "reasoning":
4658
+ if (part.text) out.trajectory.push({ kind: "thinking", text: clip4(part.text) });
4659
+ break;
4660
+ case "tool_use": {
4661
+ const name = String(part.tool ?? "tool");
4662
+ const input = part.state?.input ?? part.input;
4663
+ out.trajectory.push({ kind: "tool", toolName: name, toolInput: summarizeToolInput(input) });
4664
+ break;
4665
+ }
4666
+ case "error":
4667
+ out.error = String(evt.error?.data?.message ?? evt.error?.name ?? "opencode error");
4668
+ break;
4669
+ }
4670
+ return out;
4671
+ }
4672
+ function buildArgs2(message, opts, sessionId) {
4673
+ const args2 = ["run", "--format", "json", "--dangerously-skip-permissions", "--dir", opts.cwd];
4674
+ const model = opts.model && opts.model !== "default" ? opts.model : "";
4675
+ if (model) args2.push("--model", model);
4676
+ const v = variant(opts.runtimeConfig);
4677
+ if (v) args2.push("--variant", v);
4678
+ if (sessionId) args2.push("--session", sessionId);
4679
+ args2.push(message);
4680
+ return args2;
4681
+ }
4682
+ var OpencodeRun = class {
4683
+ constructor(opts, cb) {
4684
+ this.opts = opts;
4685
+ this.cb = cb;
4686
+ this.sessionId = opts.sessionId ?? null;
4687
+ this.env = { ...opts.env, PWD: opts.cwd };
4688
+ delete this.env.NODE_OPTIONS;
4689
+ try {
4690
+ writeFileSync3(path6.join(opts.cwd, "AGENTS.md"), opts.systemPrompt);
4691
+ } catch (e) {
4692
+ cb.log.warn("opencode: AGENTS.md write failed", { detail: String(e) });
4693
+ }
4694
+ if (this.sessionId) cb.onSession(this.sessionId);
4695
+ this.enqueue(opts.initialPrompt);
4696
+ }
4697
+ opts;
4698
+ cb;
4699
+ queue = [];
4700
+ turnBusy = false;
4701
+ stopped = false;
4702
+ proc = null;
4703
+ sessionId;
4704
+ everSucceeded = false;
4705
+ env;
4706
+ enqueue(text) {
4707
+ if (this.stopped) return;
4708
+ this.queue.push(text);
4709
+ this.pump();
4710
+ }
4711
+ pump() {
4712
+ if (this.stopped || this.turnBusy || this.queue.length === 0) return;
4713
+ this.runTurn(this.queue.shift());
4714
+ }
4715
+ runTurn(message) {
4716
+ this.turnBusy = true;
4717
+ this.cb.onActivity("working", "turn");
4718
+ const args2 = buildArgs2(message, this.opts, this.sessionId);
4719
+ const proc = spawn4("opencode", args2, { cwd: this.opts.cwd, stdio: ["ignore", "pipe", "pipe"], env: this.env });
4720
+ this.proc = proc;
4721
+ let buf = "";
4722
+ const errTail = [];
4723
+ let errLen = 0;
4724
+ const processLine = (ln) => {
4725
+ const t = ln.trim();
4726
+ if (!t) return;
4727
+ let evt;
4728
+ try {
4729
+ evt = JSON.parse(t);
4730
+ } catch {
4731
+ return;
4732
+ }
4733
+ const emit = handleOpencodeEvent(evt);
4734
+ if (emit.sessionId && emit.sessionId !== this.sessionId) {
4735
+ this.sessionId = emit.sessionId;
4736
+ this.cb.onSession(emit.sessionId);
4737
+ }
4738
+ if (emit.error) {
4739
+ this.cb.onTrajectory([{ kind: "text", text: "[opencode error] " + emit.error.slice(0, 500) }]);
4740
+ this.cb.onActivity("error", emit.error.slice(0, 200));
4741
+ }
4742
+ if (emit.activity) this.cb.onActivity(emit.activity.activity, emit.activity.detail);
4743
+ if (emit.trajectory.length) this.cb.onTrajectory(emit.trajectory);
4744
+ };
4745
+ proc.stdout?.on("data", (c) => {
4746
+ if (this.stopped) return;
4747
+ buf += c.toString();
4748
+ const lines = buf.split("\n");
4749
+ buf = lines.pop() ?? "";
4750
+ for (const ln of lines) processLine(ln);
4751
+ });
4752
+ proc.stderr?.on("data", (c) => {
4753
+ const t = c.toString();
4754
+ errTail.push(t);
4755
+ errLen += t.length;
4756
+ while (errLen > 4096 && errTail.length > 1) errLen -= errTail.shift().length;
4757
+ });
4758
+ proc.on("error", (e) => {
4759
+ this.proc = null;
4760
+ this.turnBusy = false;
4761
+ if (this.stopped) return;
4762
+ this.cb.log.error("opencode spawn failed", { detail: String(e?.message ?? e) });
4763
+ this.cb.onActivity("offline", "opencode not found");
4764
+ if (!this.everSucceeded) this.cb.onExit(1);
4765
+ else this.pump();
4766
+ });
4767
+ proc.on("exit", (code) => {
4768
+ if (buf.trim()) processLine(buf);
4769
+ buf = "";
4770
+ this.proc = null;
4771
+ this.turnBusy = false;
4772
+ if (this.stopped) return;
4773
+ if (code === 0) {
4774
+ this.everSucceeded = true;
4775
+ this.cb.onActivity("online", "");
4776
+ this.pump();
4777
+ return;
4778
+ }
4779
+ const tail = errTail.join("").trim();
4780
+ const last = tail.split("\n").filter(Boolean).pop() || `opencode exited ${code ?? "signal"}`;
4781
+ this.cb.onTrajectory([{ kind: "text", text: "[opencode error] " + clip4(tail).slice(0, 500) }]);
4782
+ this.cb.onActivity("error", last.slice(0, 200));
4783
+ if (!this.everSucceeded) {
4784
+ this.cb.onExit(code ?? 1);
4785
+ return;
4786
+ }
4787
+ this.pump();
4788
+ });
4789
+ }
4790
+ stop() {
4791
+ this.stopped = true;
4792
+ const p = this.proc;
4793
+ this.proc = null;
4794
+ if (p) {
4795
+ try {
4796
+ p.kill("SIGTERM");
4797
+ } catch {
4798
+ }
4799
+ }
4800
+ }
4801
+ };
4802
+ var opencodeRuntime = {
4803
+ name: "opencode",
4804
+ experimental: true,
4805
+ start(opts, cb) {
4806
+ const run = new OpencodeRun(opts, cb);
4807
+ return { deliver: (text) => run.enqueue(text), stop: () => run.stop() };
4808
+ }
4809
+ };
4810
+
4811
+ // src/daemon/kimiRuntime.ts
4812
+ import { spawn as spawn5 } from "node:child_process";
4813
+ import { writeFileSync as writeFileSync4 } from "node:fs";
4814
+ import path7 from "node:path";
4815
+ var MAX5 = 2e3;
4816
+ var clip5 = (s) => String(s ?? "").slice(0, MAX5);
4817
+ function summarizeToolArgs2(argsJson) {
4818
+ let a = argsJson;
4819
+ if (typeof a === "string") {
4820
+ try {
4821
+ a = JSON.parse(a);
4822
+ } catch {
4823
+ return clip5(a).slice(0, 160);
4824
+ }
4825
+ }
4826
+ if (!a || typeof a !== "object") return "";
4827
+ const v = a.command ?? a.filePath ?? a.file_path ?? a.path ?? a.pattern ?? a.query ?? a.url ?? "";
4828
+ return clip5(typeof v === "string" ? v : JSON.stringify(v)).slice(0, 160);
4829
+ }
4830
+ function handleKimiEvent(evt) {
4831
+ const out = { trajectory: [] };
4832
+ if (evt?.role === "meta" && evt.type === "session.resume_hint" && typeof evt.session_id === "string") {
4833
+ out.sessionId = evt.session_id;
4834
+ return out;
4835
+ }
4836
+ if (evt?.role === "assistant") {
4837
+ if (typeof evt.content === "string" && evt.content) out.trajectory.push({ kind: "text", text: clip5(evt.content) });
4838
+ if (Array.isArray(evt.tool_calls))
4839
+ for (const tc of evt.tool_calls) {
4840
+ const fn = tc?.function ?? {};
4841
+ out.trajectory.push({ kind: "tool", toolName: String(fn.name ?? "tool"), toolInput: summarizeToolArgs2(fn.arguments) });
4842
+ }
4843
+ }
4844
+ return out;
4845
+ }
4846
+ function buildArgs3(prompt, model, sessionId) {
4847
+ const args2 = ["-p", prompt, "--output-format", "stream-json"];
4848
+ const m = model && model !== "default" ? model : "";
4849
+ if (m) args2.push("-m", m);
4850
+ if (sessionId) args2.push("-r", sessionId);
4851
+ return args2;
4852
+ }
4853
+ var KimiRun = class {
4854
+ constructor(opts, cb) {
4855
+ this.opts = opts;
4856
+ this.cb = cb;
4857
+ this.sessionId = opts.sessionId ?? null;
4858
+ this.env = { ...opts.env, PWD: opts.cwd };
4859
+ delete this.env.NODE_OPTIONS;
4860
+ try {
4861
+ writeFileSync4(path7.join(opts.cwd, "AGENTS.md"), opts.systemPrompt);
4862
+ } catch (e) {
4863
+ cb.log.warn("kimi: AGENTS.md write failed", { detail: String(e) });
4864
+ }
4865
+ if (this.sessionId) cb.onSession(this.sessionId);
4866
+ this.enqueue(opts.initialPrompt);
4867
+ }
4868
+ opts;
4869
+ cb;
4870
+ queue = [];
4871
+ turnBusy = false;
4872
+ stopped = false;
4873
+ proc = null;
4874
+ sessionId;
4875
+ everSucceeded = false;
4876
+ env;
4877
+ enqueue(text) {
4878
+ if (this.stopped) return;
4879
+ this.queue.push(text);
4880
+ this.pump();
4881
+ }
4882
+ pump() {
4883
+ if (this.stopped || this.turnBusy || this.queue.length === 0) return;
4884
+ this.runTurn(this.queue.shift());
4885
+ }
4886
+ runTurn(prompt) {
4887
+ this.turnBusy = true;
4888
+ this.cb.onActivity("working", "turn");
4889
+ const args2 = buildArgs3(prompt, this.opts.model, this.sessionId);
4890
+ const proc = spawn5("kimi", args2, { cwd: this.opts.cwd, stdio: ["ignore", "pipe", "pipe"], env: this.env });
4891
+ this.proc = proc;
4892
+ let buf = "";
4893
+ const errTail = [];
4894
+ let errLen = 0;
4895
+ const processLine = (ln) => {
4896
+ const t = ln.trim();
4897
+ if (!t) return;
4898
+ let evt;
4899
+ try {
4900
+ evt = JSON.parse(t);
4901
+ } catch {
4902
+ return;
4903
+ }
4904
+ const emit = handleKimiEvent(evt);
4905
+ if (emit.sessionId && emit.sessionId !== this.sessionId) {
4906
+ this.sessionId = emit.sessionId;
4907
+ this.cb.onSession(emit.sessionId);
4908
+ }
4909
+ if (emit.activity) this.cb.onActivity(emit.activity.activity, emit.activity.detail);
4910
+ if (emit.trajectory.length) this.cb.onTrajectory(emit.trajectory);
4911
+ };
4912
+ proc.stdout?.on("data", (c) => {
4913
+ if (this.stopped) return;
4914
+ buf += c.toString();
4915
+ const lines = buf.split("\n");
4916
+ buf = lines.pop() ?? "";
4917
+ for (const ln of lines) processLine(ln);
4918
+ });
4919
+ proc.stderr?.on("data", (c) => {
4920
+ const t = c.toString();
4921
+ errTail.push(t);
4922
+ errLen += t.length;
4923
+ while (errLen > 4096 && errTail.length > 1) errLen -= errTail.shift().length;
4924
+ });
4925
+ proc.on("error", (e) => {
4926
+ this.proc = null;
4927
+ this.turnBusy = false;
4928
+ if (this.stopped) return;
4929
+ this.cb.log.error("kimi spawn failed", { detail: String(e?.message ?? e) });
4930
+ this.cb.onActivity("offline", "kimi not found");
4931
+ if (!this.everSucceeded) this.cb.onExit(1);
4932
+ else this.pump();
4933
+ });
4934
+ proc.on("exit", (code) => {
4935
+ if (buf.trim()) processLine(buf);
4936
+ buf = "";
4937
+ this.proc = null;
4938
+ this.turnBusy = false;
4939
+ if (this.stopped) return;
4940
+ if (code === 0) {
4941
+ this.everSucceeded = true;
4942
+ this.cb.onActivity("online", "");
4943
+ this.pump();
4944
+ return;
4945
+ }
4946
+ const tail = errTail.join("").trim();
4947
+ if (/not found|no session|session .* not/i.test(tail)) this.sessionId = null;
4948
+ const last = tail.split("\n").filter(Boolean).pop() || `kimi exited ${code ?? "signal"}`;
4949
+ this.cb.onTrajectory([{ kind: "text", text: "[kimi error] " + clip5(tail).slice(0, 500) }]);
4950
+ this.cb.onActivity("error", last.slice(0, 200));
4951
+ if (!this.everSucceeded) {
4952
+ this.cb.onExit(code ?? 1);
4953
+ return;
4954
+ }
4955
+ this.pump();
4956
+ });
4957
+ }
4958
+ stop() {
4959
+ this.stopped = true;
4960
+ const p = this.proc;
4961
+ this.proc = null;
4962
+ if (p) {
4963
+ try {
4964
+ p.kill("SIGTERM");
4965
+ } catch {
4966
+ }
4967
+ }
4968
+ }
4969
+ };
4970
+ var kimiRuntime = {
4971
+ name: "kimi",
4972
+ experimental: true,
4973
+ start(opts, cb) {
4974
+ const run = new KimiRun(opts, cb);
4975
+ return { deliver: (text) => run.enqueue(text), stop: () => run.stop() };
4976
+ }
4977
+ };
4978
+
4979
+ // src/daemon/piRuntime.ts
4980
+ import { spawn as spawn6 } from "node:child_process";
4981
+ import { writeFileSync as writeFileSync5 } from "node:fs";
4982
+ import path8 from "node:path";
4983
+ var MAX6 = 2e3;
4984
+ var clip6 = (s) => String(s ?? "").slice(0, MAX6);
4985
+ function summarizeToolArgs3(args2) {
4986
+ if (!args2 || typeof args2 !== "object") return clip6(args2).slice(0, 160);
4987
+ const v = args2.command ?? args2.filePath ?? args2.file_path ?? args2.path ?? args2.pattern ?? args2.query ?? args2.url ?? "";
4988
+ return clip6(typeof v === "string" ? v : JSON.stringify(v)).slice(0, 160);
4989
+ }
4990
+ function handlePiEvent(evt) {
4991
+ const out = { trajectory: [] };
4992
+ if (evt?.type === "session" && typeof evt.id === "string") {
4993
+ out.sessionId = evt.id;
4994
+ return out;
4995
+ }
4996
+ if (evt?.type === "message_end" && evt.message?.role === "assistant") {
4997
+ const m = evt.message;
4998
+ if (Array.isArray(m.content))
4999
+ for (const b of m.content) {
5000
+ if (b?.type === "text" && b.text) out.trajectory.push({ kind: "text", text: clip6(b.text) });
5001
+ else if (b?.type === "toolCall") out.trajectory.push({ kind: "tool", toolName: String(b.name ?? "tool"), toolInput: summarizeToolArgs3(b.arguments) });
5002
+ }
5003
+ if (m.stopReason === "error") out.error = String(m.errorMessage ?? "pi model error (no message)");
5004
+ }
5005
+ return out;
5006
+ }
5007
+ function buildArgs4(prompt, model, sessionId, cwd, promptFile) {
5008
+ const args2 = ["-p", prompt, "--mode", "json", "--append-system-prompt", promptFile, "--session-dir", cwd];
5009
+ const m = model && model !== "default" ? model : "";
5010
+ if (m) args2.push("--model", m);
5011
+ if (sessionId) args2.push("--session", sessionId);
5012
+ return args2;
5013
+ }
5014
+ var PiRun = class {
5015
+ constructor(opts, cb) {
5016
+ this.opts = opts;
5017
+ this.cb = cb;
5018
+ this.sessionId = opts.sessionId ?? null;
5019
+ this.promptFile = path8.join(opts.cwd, ".pi-system-prompt.md");
5020
+ this.env = { ...opts.env };
5021
+ delete this.env.NODE_OPTIONS;
5022
+ try {
5023
+ writeFileSync5(this.promptFile, opts.systemPrompt);
5024
+ } catch (e) {
5025
+ this.promptWriteFailed = true;
5026
+ cb.log.warn("pi: system-prompt write failed", { detail: String(e) });
5027
+ }
5028
+ if (this.sessionId) cb.onSession(this.sessionId);
5029
+ this.enqueue(opts.initialPrompt);
5030
+ }
5031
+ opts;
5032
+ cb;
5033
+ queue = [];
5034
+ turnBusy = false;
5035
+ stopped = false;
5036
+ proc = null;
5037
+ sessionId;
5038
+ everSucceeded = false;
5039
+ promptFile;
5040
+ env;
5041
+ promptWriteFailed = false;
5042
+ enqueue(text) {
5043
+ if (this.stopped) return;
5044
+ this.queue.push(text);
5045
+ this.pump();
5046
+ }
5047
+ pump() {
5048
+ if (this.stopped || this.turnBusy || this.queue.length === 0) return;
5049
+ this.runTurn(this.queue.shift());
5050
+ }
5051
+ runTurn(prompt) {
5052
+ this.turnBusy = true;
5053
+ if (this.promptWriteFailed) {
5054
+ this.cb.onTrajectory([{ kind: "text", text: "[pi error] system-prompt write failed \u2014 check cwd permissions" }]);
5055
+ this.cb.onActivity("error", "pi: system-prompt write failed");
5056
+ this.turnBusy = false;
5057
+ if (!this.everSucceeded) {
5058
+ this.cb.onExit(1);
5059
+ return;
5060
+ }
5061
+ return;
5062
+ }
5063
+ this.cb.onActivity("working", "turn");
5064
+ const args2 = buildArgs4(prompt, this.opts.model, this.sessionId, this.opts.cwd, this.promptFile);
5065
+ const proc = spawn6("pi", args2, { cwd: this.opts.cwd, stdio: ["ignore", "pipe", "pipe"], env: this.env });
5066
+ this.proc = proc;
5067
+ let buf = "";
5068
+ const errTail = [];
5069
+ let errLen = 0;
5070
+ const processLine = (ln) => {
5071
+ const t = ln.trim();
5072
+ if (!t) return;
5073
+ let evt;
5074
+ try {
5075
+ evt = JSON.parse(t);
5076
+ } catch {
5077
+ return;
5078
+ }
5079
+ const emit = handlePiEvent(evt);
5080
+ if (emit.sessionId && emit.sessionId !== this.sessionId) {
5081
+ this.sessionId = emit.sessionId;
5082
+ this.cb.onSession(emit.sessionId);
5083
+ }
5084
+ if (emit.trajectory.length) this.cb.onTrajectory(emit.trajectory);
5085
+ if (emit.error) {
5086
+ this.cb.onTrajectory([{ kind: "text", text: "[pi error] " + clip6(emit.error).slice(0, 500) }]);
5087
+ this.cb.onActivity("error", emit.error.slice(0, 200));
5088
+ }
5089
+ };
5090
+ proc.stdout?.on("data", (c) => {
5091
+ if (this.stopped) return;
5092
+ buf += c.toString();
5093
+ const lines = buf.split("\n");
5094
+ buf = lines.pop() ?? "";
5095
+ for (const ln of lines) processLine(ln);
5096
+ });
5097
+ proc.stderr?.on("data", (c) => {
5098
+ const t = c.toString();
5099
+ errTail.push(t);
5100
+ errLen += t.length;
5101
+ while (errLen > 4096 && errTail.length > 1) errLen -= errTail.shift().length;
5102
+ });
5103
+ proc.on("error", (e) => {
5104
+ this.proc = null;
5105
+ this.turnBusy = false;
5106
+ if (this.stopped) return;
5107
+ this.cb.log.error("pi spawn failed", { detail: String(e?.message ?? e) });
5108
+ this.cb.onActivity("offline", "pi not found");
5109
+ if (!this.everSucceeded) this.cb.onExit(1);
5110
+ else this.pump();
5111
+ });
5112
+ proc.on("exit", (code) => {
5113
+ if (buf.trim()) processLine(buf);
5114
+ buf = "";
5115
+ this.proc = null;
5116
+ this.turnBusy = false;
5117
+ if (this.stopped) return;
5118
+ if (code === 0) {
5119
+ this.everSucceeded = true;
5120
+ this.cb.onActivity("online", "");
5121
+ this.pump();
5122
+ return;
5123
+ }
5124
+ const tail = errTail.join("").trim();
5125
+ const last = tail.split("\n").filter(Boolean).pop() || `pi exited ${code ?? "signal"}`;
5126
+ this.cb.onTrajectory([{ kind: "text", text: "[pi error] " + clip6(tail).slice(0, 500) }]);
5127
+ this.cb.onActivity("error", last.slice(0, 200));
5128
+ if (!this.everSucceeded) {
5129
+ this.cb.onExit(code ?? 1);
5130
+ return;
5131
+ }
5132
+ this.pump();
5133
+ });
5134
+ }
5135
+ stop() {
5136
+ this.stopped = true;
5137
+ const p = this.proc;
5138
+ this.proc = null;
5139
+ if (p) {
5140
+ try {
5141
+ p.kill("SIGTERM");
5142
+ } catch {
5143
+ }
5144
+ }
5145
+ }
5146
+ };
5147
+ var piRuntime = {
5148
+ name: "pi",
5149
+ experimental: true,
5150
+ start(opts, cb) {
5151
+ const run = new PiRun(opts, cb);
5152
+ return { deliver: (text) => run.enqueue(text), stop: () => run.stop() };
5153
+ }
5154
+ };
5155
+
5156
+ // src/daemon/cursorRuntime.ts
5157
+ import { spawn as spawn7 } from "node:child_process";
5158
+ import { writeFileSync as writeFileSync6 } from "node:fs";
5159
+ import path9 from "node:path";
5160
+ var MAX7 = 2e3;
5161
+ var clip7 = (s) => String(s ?? "").slice(0, MAX7);
5162
+ function summarizeToolArgs4(args2) {
5163
+ if (!args2 || typeof args2 !== "object") return clip7(args2).slice(0, 160);
5164
+ const v = args2.command ?? args2.filePath ?? args2.file_path ?? args2.path ?? args2.pattern ?? args2.query ?? args2.url ?? "";
5165
+ return clip7(typeof v === "string" ? v : JSON.stringify(v)).slice(0, 160);
5166
+ }
5167
+ function handleCursorEvent(evt) {
5168
+ const out = { trajectory: [] };
5169
+ if (typeof evt?.session_id === "string" && evt.session_id) out.sessionId = evt.session_id;
5170
+ if (evt?.type === "tool_call" && evt.subtype === "started") {
5171
+ const tc = evt.tool_call;
5172
+ if (tc && typeof tc === "object") {
5173
+ const key = Object.keys(tc).find((k) => k.endsWith("ToolCall")) ?? Object.keys(tc)[0] ?? "tool";
5174
+ const name = key.replace(/ToolCall$/, "") || "tool";
5175
+ out.trajectory.push({ kind: "tool", toolName: name, toolInput: summarizeToolArgs4(tc[key]?.args) });
5176
+ }
5177
+ } else if (evt?.type === "result") {
5178
+ out.resultSeen = true;
5179
+ if (evt.is_error) out.error = clip7(evt.result) || "cursor reported an error";
5180
+ else if (evt.result != null) out.trajectory.push({ kind: "text", text: clip7(evt.result) });
5181
+ } else if (evt?.type === "system" && evt?.subtype === "error") {
5182
+ out.error = clip7(evt.error ?? evt.message ?? "cursor system error");
5183
+ }
5184
+ return out;
5185
+ }
5186
+ function buildArgs5(prompt, model, sessionId) {
5187
+ const args2 = ["-p", prompt, "--output-format", "stream-json", "-f"];
5188
+ const m = model && model !== "default" ? model : "";
5189
+ if (m) args2.push("--model", m);
5190
+ if (sessionId) args2.push("--resume", sessionId);
5191
+ return args2;
5192
+ }
5193
+ var CursorRun = class {
5194
+ constructor(opts, cb) {
5195
+ this.opts = opts;
5196
+ this.cb = cb;
5197
+ this.sessionId = opts.sessionId ?? null;
5198
+ this.env = { ...opts.env };
5199
+ delete this.env.NODE_OPTIONS;
5200
+ try {
5201
+ writeFileSync6(path9.join(opts.cwd, "AGENTS.md"), opts.systemPrompt);
5202
+ } catch (e) {
5203
+ cb.log.warn("cursor: AGENTS.md write failed", { detail: String(e) });
5204
+ }
5205
+ if (this.sessionId) cb.onSession(this.sessionId);
5206
+ this.enqueue(opts.initialPrompt);
5207
+ }
5208
+ opts;
5209
+ cb;
5210
+ queue = [];
5211
+ turnBusy = false;
5212
+ stopped = false;
5213
+ proc = null;
5214
+ sessionId;
5215
+ everSucceeded = false;
5216
+ env;
5217
+ enqueue(text) {
5218
+ if (this.stopped) return;
5219
+ this.queue.push(text);
5220
+ this.pump();
5221
+ }
5222
+ pump() {
5223
+ if (this.stopped || this.turnBusy || this.queue.length === 0) return;
5224
+ this.runTurn(this.queue.shift());
5225
+ }
5226
+ runTurn(prompt) {
5227
+ this.turnBusy = true;
5228
+ this.cb.onActivity("working", "turn");
5229
+ const args2 = buildArgs5(prompt, this.opts.model, this.sessionId);
5230
+ const proc = spawn7("cursor-agent", args2, { cwd: this.opts.cwd, stdio: ["ignore", "pipe", "pipe"], env: this.env });
5231
+ this.proc = proc;
5232
+ let buf = "";
5233
+ let resultSeen = false;
5234
+ let resultError = false;
5235
+ const errTail = [];
5236
+ let errLen = 0;
5237
+ const processLine = (ln) => {
5238
+ const t = ln.trim();
5239
+ if (!t) return;
5240
+ let evt;
5241
+ try {
5242
+ evt = JSON.parse(t);
5243
+ } catch {
5244
+ return;
5245
+ }
5246
+ const emit = handleCursorEvent(evt);
5247
+ if (emit.sessionId && emit.sessionId !== this.sessionId) {
5248
+ this.sessionId = emit.sessionId;
5249
+ this.cb.onSession(emit.sessionId);
5250
+ }
5251
+ if (emit.resultSeen) resultSeen = true;
5252
+ if (emit.error) {
5253
+ resultError = true;
5254
+ this.cb.onTrajectory([{ kind: "text", text: "[cursor error] " + clip7(emit.error).slice(0, 500) }]);
5255
+ this.cb.onActivity("error", emit.error.slice(0, 200));
5256
+ }
5257
+ if (emit.trajectory.length) this.cb.onTrajectory(emit.trajectory);
5258
+ };
5259
+ proc.stdout?.on("data", (c) => {
5260
+ if (this.stopped) return;
5261
+ buf += c.toString();
5262
+ const lines = buf.split("\n");
5263
+ buf = lines.pop() ?? "";
5264
+ for (const ln of lines) processLine(ln);
5265
+ });
5266
+ proc.stderr?.on("data", (c) => {
5267
+ const t = c.toString();
5268
+ errTail.push(t);
5269
+ errLen += t.length;
5270
+ while (errLen > 4096 && errTail.length > 1) errLen -= errTail.shift().length;
5271
+ });
5272
+ proc.on("error", (e) => {
5273
+ this.proc = null;
5274
+ this.turnBusy = false;
5275
+ if (this.stopped) return;
5276
+ this.cb.log.error("cursor spawn failed", { detail: String(e?.message ?? e) });
5277
+ this.cb.onActivity("offline", "cursor-agent not found");
5278
+ if (!this.everSucceeded) this.cb.onExit(1);
5279
+ else this.pump();
5280
+ });
5281
+ proc.on("exit", (code) => {
5282
+ if (buf.trim()) processLine(buf);
5283
+ buf = "";
5284
+ this.proc = null;
5285
+ this.turnBusy = false;
5286
+ if (this.stopped) return;
5287
+ if (code === 0) {
5288
+ if (!resultError) {
5289
+ this.everSucceeded = true;
5290
+ this.cb.onActivity("online", "");
5291
+ } else if (!this.everSucceeded) {
5292
+ this.cb.onExit(1);
5293
+ return;
5294
+ }
5295
+ this.pump();
5296
+ return;
5297
+ }
5298
+ if (!resultError) {
5299
+ const tail = errTail.join("").trim();
5300
+ const last = tail.split("\n").filter(Boolean).pop() || `cursor-agent exited ${code ?? "signal"}`;
5301
+ this.cb.onTrajectory([{ kind: "text", text: "[cursor error] " + clip7(tail).slice(0, 500) }]);
5302
+ this.cb.onActivity("error", last.slice(0, 200));
5303
+ }
5304
+ if (!this.everSucceeded) {
5305
+ this.cb.onExit(code ?? 1);
5306
+ return;
5307
+ }
5308
+ this.pump();
5309
+ });
5310
+ }
5311
+ stop() {
5312
+ this.stopped = true;
5313
+ const p = this.proc;
5314
+ this.proc = null;
5315
+ if (p) {
5316
+ try {
5317
+ p.kill("SIGTERM");
5318
+ } catch {
5319
+ }
5320
+ }
5321
+ }
5322
+ };
5323
+ var cursorRuntime = {
5324
+ name: "cursor",
5325
+ experimental: true,
5326
+ start(opts, cb) {
5327
+ const run = new CursorRun(opts, cb);
5328
+ return { deliver: (text) => run.enqueue(text), stop: () => run.stop() };
5329
+ }
5330
+ };
5331
+
4424
5332
  // src/daemon/runtimes.ts
4425
5333
  function has(tool) {
4426
5334
  try {
@@ -4431,10 +5339,10 @@ function has(tool) {
4431
5339
  }
4432
5340
  }
4433
5341
  function detectRuntimes() {
4434
- const found = ["claude", "codex", "kimi", "gemini", "opencode"].filter(has);
5342
+ const found = ["claude", "codex", "copilot", "kimi", "opencode", "pi", "cursor-agent"].filter(has).map((t) => t === "cursor-agent" ? "cursor" : t);
4435
5343
  return found;
4436
5344
  }
4437
- var REG = { claude: claudeRuntime, codex: codexRuntime };
5345
+ var REG = { claude: claudeRuntime, codex: codexRuntime, copilot: copilotRuntime, opencode: opencodeRuntime, kimi: kimiRuntime, pi: piRuntime, cursor: cursorRuntime };
4438
5346
  function getRuntime(name) {
4439
5347
  return REG[name] ?? null;
4440
5348
  }
@@ -4485,7 +5393,7 @@ var AgentManager = class {
4485
5393
  async reset(agentId, wipeWorkspace = false, clearMemory = false) {
4486
5394
  this.teardown(agentId);
4487
5395
  this.send({ type: "agent:session", agentId, sessionId: null });
4488
- const dir = path5.join(DATA_DIR, agentId);
5396
+ const dir = path10.join(DATA_DIR, agentId);
4489
5397
  if (wipeWorkspace) {
4490
5398
  try {
4491
5399
  await rm(dir, { recursive: true, force: true });
@@ -4495,7 +5403,7 @@ var AgentManager = class {
4495
5403
  }
4496
5404
  } else if (clearMemory) {
4497
5405
  try {
4498
- await writeFile(path5.join(dir, "MEMORY.md"), "# Memory\n\n(reset)\n");
5406
+ await writeFile(path10.join(dir, "MEMORY.md"), "# Memory\n\n(reset)\n");
4499
5407
  this.log.info("memory cleared", { agentId });
4500
5408
  } catch (e) {
4501
5409
  this.log.warn("clearMemory failed", { agentId, detail: String(e) });
@@ -4509,7 +5417,7 @@ var AgentManager = class {
4509
5417
  * title + `## Role`, preserving the agent's own sections. No-op if the workspace/file doesn't exist
4510
5418
  * yet (a not-yet-started agent gets fresh values from the DB when start() seeds it). */
4511
5419
  async syncProfile(agentId, displayName, description) {
4512
- const mem = path5.join(DATA_DIR, agentId, "MEMORY.md");
5420
+ const mem = path10.join(DATA_DIR, agentId, "MEMORY.md");
4513
5421
  let content;
4514
5422
  try {
4515
5423
  content = await readFile(mem, "utf8");
@@ -4551,9 +5459,9 @@ var AgentManager = class {
4551
5459
  return;
4552
5460
  }
4553
5461
  if (runtime.experimental) this.log.warn("experimental runtime", { runtime: runtime.name });
4554
- const dir = path5.join(DATA_DIR, agentId);
4555
- await mkdir(path5.join(dir, "notes"), { recursive: true });
4556
- const mem = path5.join(dir, "MEMORY.md");
5462
+ const dir = path10.join(DATA_DIR, agentId);
5463
+ await mkdir(path10.join(dir, "notes"), { recursive: true });
5464
+ const mem = path10.join(dir, "MEMORY.md");
4557
5465
  try {
4558
5466
  await access(mem);
4559
5467
  } catch {
@@ -4654,22 +5562,22 @@ var AgentManager = class {
4654
5562
 
4655
5563
  // src/daemon/workspace.ts
4656
5564
  import { readdir, readFile as readFile2, stat } from "node:fs/promises";
4657
- import path6 from "node:path";
5565
+ import path11 from "node:path";
4658
5566
  import os3 from "node:os";
4659
5567
  var DATA_DIR2 = agentsDir();
4660
5568
  var MAX_FILE = 256 * 1024;
4661
5569
  var SKIP = /* @__PURE__ */ new Set(["node_modules", ".git"]);
4662
5570
  function safe(agentId, rel) {
4663
- const root = path6.join(DATA_DIR2, agentId);
4664
- const target = path6.resolve(root, rel || ".");
4665
- if (target !== root && !target.startsWith(root + path6.sep)) return null;
5571
+ const root = path11.join(DATA_DIR2, agentId);
5572
+ const target = path11.resolve(root, rel || ".");
5573
+ if (target !== root && !target.startsWith(root + path11.sep)) return null;
4666
5574
  return target;
4667
5575
  }
4668
5576
  async function walk(root, rel, acc, depth) {
4669
5577
  if (depth > 6 || acc.length > 2e3) return;
4670
5578
  let ds;
4671
5579
  try {
4672
- ds = await readdir(path6.join(root, rel), { withFileTypes: true });
5580
+ ds = await readdir(path11.join(root, rel), { withFileTypes: true });
4673
5581
  } catch {
4674
5582
  return;
4675
5583
  }
@@ -4679,7 +5587,7 @@ async function walk(root, rel, acc, depth) {
4679
5587
  let size = 0;
4680
5588
  let modifiedAt = null;
4681
5589
  try {
4682
- const s = await stat(path6.join(root, childRel));
5590
+ const s = await stat(path11.join(root, childRel));
4683
5591
  size = d.isFile() ? s.size : 0;
4684
5592
  modifiedAt = s.mtime.toISOString();
4685
5593
  } catch {
@@ -4689,7 +5597,7 @@ async function walk(root, rel, acc, depth) {
4689
5597
  }
4690
5598
  }
4691
5599
  async function listWorkspace(agentId, _subPath = "") {
4692
- const root = path6.join(DATA_DIR2, agentId);
5600
+ const root = path11.join(DATA_DIR2, agentId);
4693
5601
  try {
4694
5602
  const files = [];
4695
5603
  await walk(root, "", files, 0);
@@ -4727,7 +5635,7 @@ async function readSkillsDir(dir, sourcePath) {
4727
5635
  if (!e.isDirectory()) continue;
4728
5636
  let name = e.name, description = "", userInvocable = false;
4729
5637
  try {
4730
- const txt = await readFile2(path6.join(dir, e.name, "SKILL.md"), "utf8");
5638
+ const txt = await readFile2(path11.join(dir, e.name, "SKILL.md"), "utf8");
4731
5639
  const fm = /^---\n([\s\S]*?)\n---/.exec(txt);
4732
5640
  if (fm) {
4733
5641
  name = fmField(fm[1], "name") || e.name;
@@ -4742,8 +5650,8 @@ async function readSkillsDir(dir, sourcePath) {
4742
5650
  return out;
4743
5651
  }
4744
5652
  async function listSkills(agentId) {
4745
- const global = await readSkillsDir(path6.join(os3.homedir(), ".claude", "skills"), "~/.claude/skills");
4746
- const workspace = await readSkillsDir(path6.join(DATA_DIR2, agentId, ".claude", "skills"), "<workspace>/.claude/skills");
5653
+ const global = await readSkillsDir(path11.join(os3.homedir(), ".claude", "skills"), "~/.claude/skills");
5654
+ const workspace = await readSkillsDir(path11.join(DATA_DIR2, agentId, ".claude", "skills"), "<workspace>/.claude/skills");
4747
5655
  return { global, workspace };
4748
5656
  }
4749
5657
  async function readWorkspaceFile(agentId, rel) {
@@ -4761,6 +5669,190 @@ async function readWorkspaceFile(agentId, rel) {
4761
5669
  }
4762
5670
  }
4763
5671
 
5672
+ // src/daemon/listModels.ts
5673
+ import { spawn as spawn8 } from "node:child_process";
5674
+ var titleCase = (s) => s ? s[0].toUpperCase() + s.slice(1) : s;
5675
+ function isModelId(s) {
5676
+ return /^[A-Za-z][A-Za-z0-9\-_./]*$/.test(s);
5677
+ }
5678
+ var CLAUDE_MODELS = [
5679
+ { id: "sonnet", label: "Sonnet" },
5680
+ { id: "opus", label: "Opus" },
5681
+ { id: "haiku", label: "Haiku" }
5682
+ ];
5683
+ var CLAUDE_EFFORT_LABEL = {
5684
+ low: "Low",
5685
+ medium: "Medium",
5686
+ high: "High",
5687
+ xhigh: "Extra high",
5688
+ max: "Max"
5689
+ };
5690
+ var CLAUDE_MODEL_EFFORT_ALLOW = {
5691
+ opus: /* @__PURE__ */ new Set(["low", "medium", "high", "xhigh", "max"]),
5692
+ sonnet: /* @__PURE__ */ new Set(["low", "medium", "high", "max"]),
5693
+ haiku: /* @__PURE__ */ new Set(["low", "medium", "high"])
5694
+ };
5695
+ function parseClaudeEffortLevels(helpText) {
5696
+ const m = /--effort\s*(?:<[^>]+>)?\s*(?:Effort level[^(]*)?\(([^)]+)\)/.exec(helpText);
5697
+ if (!m) return [];
5698
+ return m[1].split(",").map((s) => s.trim()).filter((s) => /^[a-z]+$/i.test(s));
5699
+ }
5700
+ function claudeThinkingForModel(modelId, superset) {
5701
+ const allow = CLAUDE_MODEL_EFFORT_ALLOW[modelId];
5702
+ const levels = superset.filter((v) => !allow || allow.has(v)).map((v) => ({ value: v, label: CLAUDE_EFFORT_LABEL[v] ?? titleCase(v) }));
5703
+ if (!levels.length) return void 0;
5704
+ return { levels, default: levels.some((l) => l.value === "medium") ? "medium" : levels[0].value };
5705
+ }
5706
+ function parseCodexModels(jsonStr) {
5707
+ let parsed;
5708
+ try {
5709
+ parsed = JSON.parse(jsonStr);
5710
+ } catch {
5711
+ return [];
5712
+ }
5713
+ const models = Array.isArray(parsed?.models) ? parsed.models : [];
5714
+ const out = [];
5715
+ for (const m of models) {
5716
+ if (m?.visibility !== "list") continue;
5717
+ const slug = typeof m?.slug === "string" ? m.slug : "";
5718
+ if (!slug) continue;
5719
+ const raw = Array.isArray(m?.supported_reasoning_levels) ? m.supported_reasoning_levels : [];
5720
+ const levels = raw.map((l) => ({ value: String(l?.effort ?? ""), label: titleCase(String(l?.effort ?? "")), description: typeof l?.description === "string" ? l.description : void 0 })).filter((l) => l.value);
5721
+ const thinking = levels.length ? { levels, default: typeof m?.default_reasoning_level === "string" ? m.default_reasoning_level : void 0 } : void 0;
5722
+ out.push({ id: slug, label: typeof m?.display_name === "string" && m.display_name ? m.display_name : slug, provider: "openai", ...thinking ? { thinking } : {} });
5723
+ }
5724
+ return out;
5725
+ }
5726
+ function parseOpencodeModels(stdout) {
5727
+ const out = [];
5728
+ for (const raw of stdout.split("\n")) {
5729
+ const line = raw.trim();
5730
+ if (!line) continue;
5731
+ if (line.startsWith("{") || line.startsWith('"') || line.startsWith("}")) continue;
5732
+ if (line === line.toUpperCase() && /[A-Z]/.test(line)) continue;
5733
+ const id = line.split(/\s+/)[0];
5734
+ const slash = id.indexOf("/");
5735
+ if (slash <= 0 || slash >= id.length - 1) continue;
5736
+ out.push({ id, label: id, provider: id.slice(0, slash) });
5737
+ }
5738
+ return out;
5739
+ }
5740
+ function parseCursorModels(stdout) {
5741
+ const out = [];
5742
+ for (const raw of stdout.split("\n")) {
5743
+ const line = raw.trim();
5744
+ if (!line) continue;
5745
+ const sep = line.indexOf(" - ");
5746
+ if (sep < 0) continue;
5747
+ const id = line.slice(0, sep).trim();
5748
+ if (!isModelId(id)) continue;
5749
+ let label = line.slice(sep + 3).trim();
5750
+ const isDefault = /default/i.test(label);
5751
+ const paren = label.indexOf("(");
5752
+ if (paren >= 0) label = label.slice(0, paren).trim();
5753
+ out.push({ id, label: label || id, provider: "cursor", ...isDefault ? { default: true } : {} });
5754
+ }
5755
+ return out;
5756
+ }
5757
+ function parsePiModels(out) {
5758
+ const res = [];
5759
+ for (const raw of out.split("\n")) {
5760
+ const line = raw.trim();
5761
+ if (!line) continue;
5762
+ if (isPiNoise(line)) continue;
5763
+ const fields = line.split(/\s+/);
5764
+ const first = fields[0];
5765
+ if (first.toLowerCase() === "provider") continue;
5766
+ let id;
5767
+ if (first.includes(":") || first.includes("/")) id = first.replace(":", "/");
5768
+ else if (fields.length >= 2) id = `${first}/${fields[1]}`;
5769
+ else continue;
5770
+ const slash = id.indexOf("/");
5771
+ if (slash <= 0 || slash >= id.length - 1) continue;
5772
+ res.push({ id, label: id, provider: id.slice(0, slash) });
5773
+ }
5774
+ return res;
5775
+ }
5776
+ function isPiNoise(line) {
5777
+ const l = line.toLowerCase();
5778
+ return l.includes("no models match pattern") || l.startsWith("warning:") || l.startsWith("error:") || l.startsWith("info:");
5779
+ }
5780
+ var LIST_TIMEOUT_MS = 7e3;
5781
+ var OUT_CAP = 256 * 1024;
5782
+ function runList(bin, args2, timeoutMs = LIST_TIMEOUT_MS) {
5783
+ return new Promise((resolve) => {
5784
+ const env = { ...process.env };
5785
+ delete env.NODE_OPTIONS;
5786
+ let proc;
5787
+ try {
5788
+ proc = spawn8(bin, args2, { stdio: ["ignore", "pipe", "pipe"], env });
5789
+ } catch (e) {
5790
+ return resolve({ stdout: "", stderr: String(e?.message ?? e), code: 1 });
5791
+ }
5792
+ let stdout = "";
5793
+ let stderr = "";
5794
+ proc.stdout?.on("data", (c) => {
5795
+ if (stdout.length < OUT_CAP) stdout += c.toString();
5796
+ });
5797
+ proc.stderr?.on("data", (c) => {
5798
+ if (stderr.length < OUT_CAP) stderr += c.toString();
5799
+ });
5800
+ const timer = setTimeout(() => {
5801
+ try {
5802
+ proc.kill("SIGKILL");
5803
+ } catch {
5804
+ }
5805
+ }, timeoutMs);
5806
+ proc.on("error", (e) => {
5807
+ clearTimeout(timer);
5808
+ resolve({ stdout, stderr: stderr || String(e?.message ?? e), code: 1 });
5809
+ });
5810
+ proc.on("exit", (code) => {
5811
+ clearTimeout(timer);
5812
+ resolve({ stdout, stderr, code });
5813
+ });
5814
+ });
5815
+ }
5816
+ async function listModels(runtime) {
5817
+ switch (runtime) {
5818
+ case "opencode": {
5819
+ let r = await runList("opencode", ["models", "--verbose"], 5e3);
5820
+ let models = parseOpencodeModels(r.stdout);
5821
+ if (!models.length) {
5822
+ r = await runList("opencode", ["models"], 2e3);
5823
+ models = parseOpencodeModels(r.stdout);
5824
+ }
5825
+ return models.length ? models : null;
5826
+ }
5827
+ case "cursor": {
5828
+ const r = await runList("cursor-agent", ["--list-models"]);
5829
+ const models = parseCursorModels(r.stdout);
5830
+ return models.length ? models : null;
5831
+ }
5832
+ case "pi": {
5833
+ const r = await runList("pi", ["--list-models"]);
5834
+ const models = parsePiModels(r.stdout || r.stderr);
5835
+ return models.length ? models : null;
5836
+ }
5837
+ case "claude": {
5838
+ const r = await runList("claude", ["--help"]);
5839
+ const superset = parseClaudeEffortLevels(r.stdout || r.stderr);
5840
+ if (!superset.length) return null;
5841
+ return CLAUDE_MODELS.map((m) => {
5842
+ const thinking = claudeThinkingForModel(m.id, superset);
5843
+ return { ...m, provider: "anthropic", ...thinking ? { thinking } : {} };
5844
+ });
5845
+ }
5846
+ case "codex": {
5847
+ const r = await runList("codex", ["debug", "models"]);
5848
+ const models = parseCodexModels(r.stdout);
5849
+ return models.length ? models : null;
5850
+ }
5851
+ default:
5852
+ return null;
5853
+ }
5854
+ }
5855
+
4764
5856
  // src/daemon/index.ts
4765
5857
  var log = createLogger("daemon");
4766
5858
  var args = process.argv.slice(2);
@@ -4785,7 +5877,7 @@ var readMachineId = () => {
4785
5877
  };
4786
5878
  var saveMachineId = (id) => {
4787
5879
  try {
4788
- fs3.mkdirSync(path7.dirname(MID_FILE), { recursive: true });
5880
+ fs3.mkdirSync(path12.dirname(MID_FILE), { recursive: true });
4789
5881
  fs3.writeFileSync(MID_FILE, id);
4790
5882
  } catch {
4791
5883
  }
@@ -4826,6 +5918,9 @@ conn = new Connection(serverUrl, apiKey, (msg) => {
4826
5918
  case "agent:skills:list":
4827
5919
  void listSkills(msg.agentId).then((r) => conn.send({ type: "skills:list", requestId: msg.requestId, agentId: msg.agentId, ...r }));
4828
5920
  break;
5921
+ case "probe-models":
5922
+ void listModels(msg.runtime ?? "").then((models) => conn.send({ type: "models", requestId: msg.requestId, runtime: msg.runtime, models })).catch((e) => conn.send({ type: "models", requestId: msg.requestId, runtime: msg.runtime, models: null, error: String(e?.message ?? e) }));
5923
+ break;
4829
5924
  case "ping":
4830
5925
  conn.send({ type: "pong" });
4831
5926
  break;
@@ -4840,7 +5935,7 @@ conn = new Connection(serverUrl, apiKey, (msg) => {
4840
5935
  runningAgents: mgr.running(),
4841
5936
  hostname: os4.hostname(),
4842
5937
  os: `${os4.platform()} ${os4.arch()}`,
4843
- daemonVersion: "0.1.0",
5938
+ daemonVersion: "0.3.0",
4844
5939
  machineId: readMachineId()
4845
5940
  // Stable identity: empty on first connection; server sends it back via ready:ack for persistence.
4846
5941
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fancyboi999/open-tag-daemon",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "open-tag compute-plane daemon — connect any machine to an open-tag server so its agents run there. No repo clone needed.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",