pikiloom 0.4.26 → 0.4.28

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 (156) hide show
  1. package/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DtjuI8NU.js} +1 -1
  2. package/dashboard/dist/assets/ConnectionModal-BIx6a5o1.js +1 -0
  3. package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-BINh_7NB.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-CHpd147b.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C4DbGxbG.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-D_PUi-5O.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-BahDEMqf.js} +1 -1
  8. package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-Cp5VXb_2.js} +1 -1
  9. package/dashboard/dist/assets/SessionPanel-DZVNXptx.js +1 -0
  10. package/dashboard/dist/assets/SystemTab-CLznS31f.js +1 -0
  11. package/dashboard/dist/assets/index-CLGovkkw.js +3 -0
  12. package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
  13. package/dashboard/dist/assets/index-DcYOI6rL.js +23 -0
  14. package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C8DCTJ44.js} +1 -1
  15. package/dashboard/dist/index.html +2 -2
  16. package/dist/agent/acp-client.js +0 -59
  17. package/dist/agent/artifacts.js +1 -38
  18. package/dist/agent/auto-update.js +2 -78
  19. package/dist/agent/await-resume.js +0 -20
  20. package/dist/agent/cli/auth.js +3 -51
  21. package/dist/agent/cli/catalog.js +0 -3
  22. package/dist/agent/cli/detector.js +1 -19
  23. package/dist/agent/cli/index.js +0 -3
  24. package/dist/agent/cli/registry.js +0 -25
  25. package/dist/agent/driver.js +0 -14
  26. package/dist/agent/drivers/claude-tui.js +2 -769
  27. package/dist/agent/drivers/claude.js +12 -631
  28. package/dist/agent/drivers/codex.js +11 -162
  29. package/dist/agent/drivers/gemini.js +6 -114
  30. package/dist/agent/drivers/hermes.js +0 -128
  31. package/dist/agent/goal.js +0 -45
  32. package/dist/agent/handover.js +1 -46
  33. package/dist/agent/images.js +7 -133
  34. package/dist/agent/index.js +1 -29
  35. package/dist/agent/mcp/bridge.js +1 -197
  36. package/dist/agent/mcp/extensions.js +5 -84
  37. package/dist/agent/mcp/oauth.js +2 -69
  38. package/dist/agent/mcp/registry.js +2 -33
  39. package/dist/agent/mcp/session-server.js +2 -45
  40. package/dist/agent/mcp/tools/ask-user.js +0 -6
  41. package/dist/agent/mcp/tools/await-resume.js +0 -15
  42. package/dist/agent/mcp/tools/goal.js +0 -25
  43. package/dist/agent/mcp/tools/types.js +0 -5
  44. package/dist/agent/mcp/tools/workspace.js +0 -23
  45. package/dist/agent/npm.js +0 -14
  46. package/dist/agent/session.js +86 -196
  47. package/dist/agent/skill-installer.js +3 -70
  48. package/dist/agent/skills.js +0 -43
  49. package/dist/agent/stream.js +5 -148
  50. package/dist/agent/types.js +0 -12
  51. package/dist/agent/utils.js +2 -120
  52. package/dist/bot/bot.js +3 -468
  53. package/dist/bot/command-ui.js +0 -45
  54. package/dist/bot/commands.js +0 -51
  55. package/dist/bot/headless-bot.js +0 -21
  56. package/dist/bot/host.js +1 -5
  57. package/dist/bot/human-loop.js +0 -8
  58. package/dist/bot/menu.js +0 -3
  59. package/dist/bot/orchestration.js +0 -3
  60. package/dist/bot/render-shared.js +0 -63
  61. package/dist/bot/session-hub.js +14 -92
  62. package/dist/bot/session-status.js +1 -5
  63. package/dist/bot/streaming.js +0 -53
  64. package/dist/browser-profile.js +0 -51
  65. package/dist/browser-supervisor.js +0 -75
  66. package/dist/catalog/cli-tools.js +0 -53
  67. package/dist/catalog/index.js +0 -18
  68. package/dist/catalog/local-models.js +0 -18
  69. package/dist/catalog/mcp-servers.js +0 -35
  70. package/dist/catalog/skill-repos.js +0 -32
  71. package/dist/channels/base.js +0 -19
  72. package/dist/channels/dingtalk/bot.js +0 -3
  73. package/dist/channels/dingtalk/channel.js +0 -33
  74. package/dist/channels/discord/bot.js +0 -7
  75. package/dist/channels/discord/channel.js +0 -22
  76. package/dist/channels/feishu/bot.js +0 -71
  77. package/dist/channels/feishu/channel.js +0 -78
  78. package/dist/channels/feishu/markdown.js +0 -5
  79. package/dist/channels/feishu/render.js +0 -45
  80. package/dist/channels/health.js +0 -46
  81. package/dist/channels/slack/bot.js +0 -7
  82. package/dist/channels/slack/channel.js +1 -27
  83. package/dist/channels/states.js +0 -3
  84. package/dist/channels/telegram/bot.js +0 -60
  85. package/dist/channels/telegram/channel.js +2 -99
  86. package/dist/channels/telegram/directory.js +0 -3
  87. package/dist/channels/telegram/live-preview.js +0 -8
  88. package/dist/channels/telegram/render.js +0 -30
  89. package/dist/channels/wecom/bot.js +0 -3
  90. package/dist/channels/wecom/channel.js +0 -58
  91. package/dist/channels/weixin/api.js +0 -9
  92. package/dist/channels/weixin/bot.js +0 -88
  93. package/dist/channels/weixin/channel.js +0 -6
  94. package/dist/cli/autostart.js +1 -44
  95. package/dist/cli/channel-supervisor.js +1 -59
  96. package/dist/cli/channels.js +0 -6
  97. package/dist/cli/main.js +1 -145
  98. package/dist/cli/onboarding.js +0 -6
  99. package/dist/cli/run.js +0 -11
  100. package/dist/cli/setup-wizard.js +0 -3
  101. package/dist/core/config/runtime-config.js +0 -38
  102. package/dist/core/config/user-config.js +0 -63
  103. package/dist/core/config/validation.js +0 -9
  104. package/dist/core/constants.js +0 -268
  105. package/dist/core/git.js +0 -31
  106. package/dist/core/legacy-compat.js +1 -28
  107. package/dist/core/logging.js +0 -3
  108. package/dist/core/platform.js +0 -28
  109. package/dist/core/process-control.js +0 -8
  110. package/dist/core/secrets/index.js +0 -17
  111. package/dist/core/secrets/inline-seal.js +0 -7
  112. package/dist/core/secrets/ref.js +0 -19
  113. package/dist/core/secrets/resolver.js +0 -8
  114. package/dist/core/secrets/store.js +1 -12
  115. package/dist/core/utils.js +1 -37
  116. package/dist/core/version.js +0 -3
  117. package/dist/dashboard/platform.js +1 -35
  118. package/dist/dashboard/routes/agents.js +2 -71
  119. package/dist/dashboard/routes/cli.js +0 -27
  120. package/dist/dashboard/routes/config.js +0 -54
  121. package/dist/dashboard/routes/extensions.js +0 -132
  122. package/dist/dashboard/routes/local-models.js +0 -68
  123. package/dist/dashboard/routes/models.js +0 -46
  124. package/dist/dashboard/routes/sessions.js +0 -100
  125. package/dist/dashboard/runtime.js +1 -58
  126. package/dist/dashboard/server.js +0 -49
  127. package/dist/dashboard/session-control.js +2 -69
  128. package/dist/model/catalog.js +1 -17
  129. package/dist/model/index.js +0 -15
  130. package/dist/model/injector.js +1 -181
  131. package/dist/model/provider-models.js +1 -48
  132. package/dist/model/responses-bridge.js +3 -45
  133. package/dist/model/store.js +0 -17
  134. package/dist/model/types.js +0 -12
  135. package/dist/model/validation.js +1 -18
  136. package/dist/pikichannel/adapter-pikiloom.js +0 -31
  137. package/dist/pikichannel/code.js +0 -12
  138. package/dist/pikichannel/codec.js +0 -14
  139. package/dist/pikichannel/host.js +4 -33
  140. package/dist/pikichannel/protocol.js +0 -52
  141. package/dist/pikichannel/rendezvous-broker.js +5 -26
  142. package/dist/pikichannel/rendezvous-host.js +3 -16
  143. package/dist/pikichannel/server.js +6 -55
  144. package/dist/pikichannel/transport.js +0 -25
  145. package/dist/pikichannel/transports/webrtc-host.js +0 -18
  146. package/dist/pikichannel/transports/webrtc-shared.js +4 -34
  147. package/dist/pikichannel/transports/websocket-host.js +1 -14
  148. package/dist/pikichannel/turn.js +2 -69
  149. package/dist/pikichannel/web/sdk.js +14 -71
  150. package/package.json +1 -1
  151. package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
  152. package/dashboard/dist/assets/SessionPanel-_0dLWvEb.js +0 -1
  153. package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
  154. package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
  155. package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
  156. package/dashboard/dist/assets/index-CyCspgtc.css +0 -1
@@ -1,37 +1,7 @@
1
- /**
2
- * Minimal Agent Client Protocol (ACP) client.
3
- *
4
- * ACP is a JSON-RPC 2.0 protocol over stdio that standardises how clients
5
- * (editors, orchestrators) talk to AI coding agents. Reference impls:
6
- * Hermes (`hermes acp`), Gemini CLI (`gemini --acp`), OpenCode, Claude Code
7
- * via `@zed-industries/claude-code-acp` adapter.
8
- *
9
- * This is a deliberately small implementation — it parses line-delimited
10
- * JSON-RPC messages, dispatches results/notifications, and lets callers
11
- * `request()` and consume an async stream of `sessionUpdate` notifications.
12
- *
13
- * Spec: https://agentclientprotocol.com — methods we use:
14
- * client → agent: initialize, session/new, session/load, session/prompt,
15
- * session/set_model, session/set_session_mode, session/cancel
16
- * agent → client: session/update (streaming notifications)
17
- * session/request_permission (we deny by default)
18
- * fs/read_text_file, fs/write_text_file (we say "no")
19
- */
20
1
  import { spawn } from 'node:child_process';
21
2
  import { createInterface } from 'node:readline';
22
3
  import { EventEmitter } from 'node:events';
23
4
  import { agentLog, agentWarn } from './utils.js';
24
- /**
25
- * Translate a Claude-style MCP server map (keyed by name with
26
- * `{type?, command|url, args?, env?, headers?}`) into ACP wire format:
27
- * stdio: { name, command, args, env: [{name, value}, ...] }
28
- * http: { type: "http", name, url, headers: [{name, value}, ...] }
29
- * sse: { type: "sse", name, url, headers: [{name, value}, ...] }
30
- *
31
- * The ACP schema requires `name` plus a list-of-records `env` for stdio
32
- * servers — sending an object map for env, or omitting name, results in
33
- * a JSON-RPC -32602 (Invalid params) response.
34
- */
35
5
  export function toAcpMcpServers(servers) {
36
6
  if (!servers)
37
7
  return [];
@@ -57,14 +27,6 @@ export function toAcpMcpServers(servers) {
57
27
  }
58
28
  return out;
59
29
  }
60
- /**
61
- * AcpClient — encapsulates a single ACP child process.
62
- *
63
- * Events:
64
- * - 'sessionUpdate' (params) → agent → client streaming events
65
- * - 'request' ({method, id, params}) → agent → client requests we must answer
66
- * - 'exit' (code) → process closed
67
- */
68
30
  export class AcpClient extends EventEmitter {
69
31
  opts;
70
32
  proc = null;
@@ -88,7 +50,6 @@ export class AcpClient extends EventEmitter {
88
50
  rl.on('line', line => this.handleLine(line));
89
51
  this.proc.stderr.on('data', (chunk) => {
90
52
  const text = chunk.toString();
91
- // ACP stderr is reserved for human-readable diagnostics.
92
53
  for (const ln of text.split('\n')) {
93
54
  const t = ln.trim();
94
55
  if (t)
@@ -109,7 +70,6 @@ export class AcpClient extends EventEmitter {
109
70
  isAlive() {
110
71
  return !!this.proc && !this.exited;
111
72
  }
112
- /** Send a JSON-RPC request and resolve when the agent responds. */
113
73
  async request(method, params, timeoutMs = 60_000) {
114
74
  if (!this.proc || this.exited)
115
75
  throw new Error('ACP process not running');
@@ -127,12 +87,6 @@ export class AcpClient extends EventEmitter {
127
87
  this.write({ jsonrpc: '2.0', id, method, params });
128
88
  });
129
89
  }
130
- /**
131
- * Best-effort request — resolves with `null` on any error or non-existent
132
- * method, instead of throwing. Useful for optional methods like
133
- * `session/set_session_mode` that may or may not be implemented by the
134
- * agent. The error is logged but does not propagate.
135
- */
136
90
  async tryRequest(method, params, timeoutMs = 15_000) {
137
91
  try {
138
92
  return await this.request(method, params, timeoutMs);
@@ -142,24 +96,17 @@ export class AcpClient extends EventEmitter {
142
96
  return null;
143
97
  }
144
98
  }
145
- /** Send a notification (no response expected). */
146
99
  notify(method, params) {
147
100
  if (!this.proc || this.exited)
148
101
  return;
149
102
  this.write({ jsonrpc: '2.0', method, params });
150
103
  }
151
- /** Send a JSON-RPC response back to the agent (when it asks us something). */
152
104
  respond(id, result) {
153
105
  this.write({ jsonrpc: '2.0', id, result });
154
106
  }
155
107
  respondError(id, code, message) {
156
108
  this.write({ jsonrpc: '2.0', id, error: { code, message } });
157
109
  }
158
- /**
159
- * Wait for the session/update event stream to go quiet for `quietMs`
160
- * milliseconds, or until `maxMs` elapses. Used to drain replay events
161
- * after `session/load`. Returns the number of events observed.
162
- */
163
110
  async waitForQuiet(quietMs = 150, maxMs = 3_000) {
164
111
  let events = 0;
165
112
  let lastEventAt = Date.now();
@@ -179,7 +126,6 @@ export class AcpClient extends EventEmitter {
179
126
  this.off('sessionUpdate', onUpdate);
180
127
  }
181
128
  }
182
- /** Gracefully shut down. */
183
129
  async close() {
184
130
  if (!this.proc || this.exited)
185
131
  return;
@@ -198,7 +144,6 @@ export class AcpClient extends EventEmitter {
198
144
  this.proc?.on('close', () => { clearTimeout(timer); resolve(); });
199
145
  });
200
146
  }
201
- // ------------------------------------------------------------------------
202
147
  write(msg) {
203
148
  if (!this.proc || this.exited)
204
149
  return;
@@ -218,11 +163,9 @@ export class AcpClient extends EventEmitter {
218
163
  msg = JSON.parse(trimmed);
219
164
  }
220
165
  catch {
221
- // ACP sometimes prefixes diagnostic logs to stdout; ignore non-JSON
222
166
  agentLog(`[acp:stdout-noise] ${trimmed.slice(0, 200)}`);
223
167
  return;
224
168
  }
225
- // Response (has id and result/error)
226
169
  if ('id' in msg && (('result' in msg) || ('error' in msg))) {
227
170
  const resp = msg;
228
171
  const pending = this.pending.get(resp.id);
@@ -235,7 +178,6 @@ export class AcpClient extends EventEmitter {
235
178
  pending.resolve(resp.result);
236
179
  return;
237
180
  }
238
- // Notification (no id) — agent → client streaming event
239
181
  if (!('id' in msg) && 'method' in msg) {
240
182
  const n = msg;
241
183
  if (n.method === 'session/update') {
@@ -246,7 +188,6 @@ export class AcpClient extends EventEmitter {
246
188
  }
247
189
  return;
248
190
  }
249
- // Request (has id and method) — agent → client RPC
250
191
  if ('id' in msg && 'method' in msg) {
251
192
  const r = msg;
252
193
  this.emit('request', { id: r.id, method: r.method, params: r.params });
@@ -1,19 +1,3 @@
1
- /**
2
- * artifacts.ts — delivered-artifact manifest (the single source of truth for
3
- * "files the agent handed to the user during a session").
4
- *
5
- * When the agent calls `im_send_file`, the bot materializes the file into the
6
- * session attachments dir and appends a record here. IM channels additionally
7
- * push the bytes to their chat; the dashboard serves the materialized copy over
8
- * HTTP and renders it. Recording is terminal-agnostic, so a session watched
9
- * from BOTH a chat and the dashboard shows the same deliveries on either side,
10
- * and they survive a page reload / workspace cleanup.
11
- *
12
- * The manifest lives next to the MCP-buffered image attachments
13
- * (`sessionAttachmentsDir`), which is already an allowlist root for the
14
- * dashboard attachment endpoint — so materialized artifacts are servable with
15
- * no new trust boundary.
16
- */
17
1
  import fs from 'node:fs';
18
2
  import path from 'node:path';
19
3
  import { sessionAttachmentsDir } from './images.js';
@@ -28,7 +12,6 @@ const MIME_BY_EXT = {
28
12
  '.tar': 'application/x-tar', '.mp4': 'video/mp4', '.mov': 'video/quicktime', '.webm': 'video/webm',
29
13
  '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.log': 'text/plain; charset=utf-8',
30
14
  };
31
- /** Best-effort MIME for a delivered artifact; `application/octet-stream` when unknown. */
32
15
  export function mimeForArtifact(filePath) {
33
16
  return MIME_BY_EXT[path.extname(filePath).toLowerCase()] || 'application/octet-stream';
34
17
  }
@@ -41,13 +24,6 @@ function deliveredDir(agent, sessionId) {
41
24
  function sanitizeName(name) {
42
25
  return (name || 'file').replace(/[/\\\0]+/g, '_').replace(/^\.+/, '').slice(0, 200) || 'file';
43
26
  }
44
- /**
45
- * Materialize `srcPath` into the session's delivered-artifacts dir so it is
46
- * (a) servable by the dashboard attachment endpoint (the dir is an allowlist
47
- * root) and (b) durable across workspace cleanup. Hardlinks when possible,
48
- * falling back to a copy across filesystems. The on-disk name is stamped to
49
- * avoid collisions; the pristine basename travels in the manifest.
50
- */
51
27
  export function stageDeliveredArtifact(agent, sessionId, srcPath) {
52
28
  const fileName = sanitizeName(path.basename(srcPath));
53
29
  const stat = fs.statSync(srcPath);
@@ -63,7 +39,6 @@ export function stageDeliveredArtifact(agent, sessionId, srcPath) {
63
39
  }
64
40
  return { path: dest, fileName, fileSize: stat.size, fileMime: mimeForArtifact(fileName) };
65
41
  }
66
- /** Append a record to the session's delivered-artifact manifest. */
67
42
  export function recordDeliveredArtifact(agent, sessionId, entry) {
68
43
  try {
69
44
  const file = manifestPath(agent, sessionId);
@@ -74,11 +49,6 @@ export function recordDeliveredArtifact(agent, sessionId, entry) {
74
49
  agentWarn(`[artifacts] record failed: ${e?.message || e}`);
75
50
  }
76
51
  }
77
- /**
78
- * Materialize + record a delivered artifact in one step. Best-effort: returns
79
- * the stored record (with the materialized path), or null on failure — delivery
80
- * must never crash the stream.
81
- */
82
52
  export function deliverArtifact(agent, sessionId, srcPath, opts) {
83
53
  try {
84
54
  const staged = stageDeliveredArtifact(agent, sessionId, srcPath);
@@ -100,7 +70,6 @@ export function deliverArtifact(agent, sessionId, srcPath, opts) {
100
70
  return null;
101
71
  }
102
72
  }
103
- /** Read the delivered-artifact manifest for a session (tolerant of partial lines). */
104
73
  export function readDeliveredArtifacts(agent, sessionId) {
105
74
  const file = manifestPath(agent, sessionId);
106
75
  let raw;
@@ -120,16 +89,10 @@ export function readDeliveredArtifacts(agent, sessionId) {
120
89
  if (rec && typeof rec.path === 'string' && rec.fileName)
121
90
  out.push(rec);
122
91
  }
123
- catch { /* skip malformed line */ }
92
+ catch { }
124
93
  }
125
94
  return out;
126
95
  }
127
- /**
128
- * Project the delivered-artifact manifest into pre-transport MessageBlocks
129
- * (content = `file://<abs>`). The dashboard read path rewrites these to
130
- * attachment HTTP URLs via `rewriteAttachmentBlocksForTransport`. Records whose
131
- * file no longer exists on disk are dropped.
132
- */
133
96
  export function deliveredArtifactBlocks(agent, sessionId) {
134
97
  const blocks = [];
135
98
  for (const a of readDeliveredArtifacts(agent, sessionId)) {
@@ -1,6 +1,3 @@
1
- /**
2
- * Background agent CLI version checking and update prompts.
3
- */
4
1
  import fs from 'node:fs';
5
2
  import os from 'node:os';
6
3
  import path from 'node:path';
@@ -17,39 +14,18 @@ function setUpdateState(agent, patch) {
17
14
  const current = updateStates.get(agent) || emptyState();
18
15
  updateStates.set(agent, { ...current, ...patch });
19
16
  }
20
- /** Returns the cached update state for a specific agent (or null). */
21
17
  export function getAgentUpdateState(agent) {
22
18
  return updateStates.get(agent) || null;
23
19
  }
24
- /** Returns all cached update states. */
25
20
  export function getAllAgentUpdateStates() {
26
21
  const result = {};
27
22
  for (const [key, value] of updateStates)
28
23
  result[key] = value;
29
24
  return result;
30
25
  }
31
- // ---------------------------------------------------------------------------
32
- // Spawn coordination
33
- //
34
- // A CLI reinstall (`npm install -g` / `brew upgrade`) briefly tears down and
35
- // rewrites the bin symlink, so an agent spawn that races it execs into
36
- // "/bin/sh: <cli>: command not found" (exit 127). The updating process drops a
37
- // per-agent marker file for the duration of the destructive step; the spawn
38
- // path waits while it's held. The marker is cross-process (this worker AND the
39
- // `npx pikiloom@latest` self-bootstrap both update + spawn), and carries the
40
- // updater's pid so a marker orphaned by a crashed updater is detected via
41
- // liveness and cleaned up — a spawn never hangs on a stale marker, and a
42
- // genuinely-missing CLI (no marker) fails fast instead of waiting.
43
- // ---------------------------------------------------------------------------
44
- /** Path of the in-flight-reinstall marker for `agent` (one file per agent). */
45
26
  export function agentUpdateMarkerPath(agent) {
46
27
  return path.join(os.homedir(), STATE_DIR_NAME, `agent-update-${agent}.installing`);
47
28
  }
48
- /**
49
- * True when a live updater is mid-reinstall of `agent`. Reads the marker's pid
50
- * and probes liveness with `kill(pid, 0)`; an orphaned marker (writer gone) is
51
- * removed and reported idle so spawns don't hang on a crashed updater.
52
- */
53
29
  function agentReinstallInFlight(agent) {
54
30
  const marker = agentUpdateMarkerPath(agent);
55
31
  let raw;
@@ -57,12 +33,12 @@ function agentReinstallInFlight(agent) {
57
33
  raw = fs.readFileSync(marker, 'utf8');
58
34
  }
59
35
  catch {
60
- return false; // no marker — nothing updating
36
+ return false;
61
37
  }
62
38
  const pid = Number.parseInt(raw.trim(), 10);
63
39
  if (Number.isInteger(pid) && pid > 0) {
64
40
  try {
65
- process.kill(pid, 0); // throws ESRCH if the updater is gone
41
+ process.kill(pid, 0);
66
42
  return true;
67
43
  }
68
44
  catch {
@@ -73,7 +49,6 @@ function agentReinstallInFlight(agent) {
73
49
  return false;
74
50
  }
75
51
  }
76
- // Unreadable pid — fall back to mtime so a corrupt marker still ages out.
77
52
  try {
78
53
  return Date.now() - fs.statSync(marker).mtimeMs <= AGENT_UPDATE_COMMAND_TIMEOUT_MS;
79
54
  }
@@ -81,11 +56,6 @@ function agentReinstallInFlight(agent) {
81
56
  return false;
82
57
  }
83
58
  }
84
- /**
85
- * Marks `agent` as mid-reinstall (cross-process) for the duration of
86
- * `install()` so concurrent spawns can wait it out. Always clears the marker,
87
- * even when the install throws.
88
- */
89
59
  export async function withAgentUpdateGate(agent, install) {
90
60
  const id = String(agent || '').trim();
91
61
  if (!id)
@@ -106,11 +76,6 @@ export async function withAgentUpdateGate(agent, install) {
106
76
  catch { }
107
77
  }
108
78
  }
109
- /**
110
- * Resolves once no live reinstall of `agent` is in flight, or after
111
- * `timeoutMs`. No-op (returns immediately) when nothing is updating. Called
112
- * from the spawn path so we never exec a half-swapped CLI binary.
113
- */
114
79
  export async function awaitAgentUpdateIdle(agent, timeoutMs) {
115
80
  const id = String(agent || '').trim();
116
81
  if (!id)
@@ -179,10 +144,6 @@ function isNpmPackageOwnedBinary(binPath, pkg, npmRoot) {
179
144
  return true;
180
145
  return isPathInside(realPackageDir, binPath);
181
146
  }
182
- /**
183
- * Check if a binary was installed via Homebrew by resolving its real path.
184
- * Homebrew cask binaries typically symlink through Caskroom or Cellar.
185
- */
186
147
  function isBrewInstalledBinary(binPath) {
187
148
  const realPath = realPathOrNull(binPath);
188
149
  const target = realPath || binPath;
@@ -196,14 +157,12 @@ export function resolveAgentUpdateStrategy(agent, npmPrefix, npmRoot = null) {
196
157
  const binPath = String(agent.path || '').trim();
197
158
  if (!binPath)
198
159
  return { kind: 'skip', reason: 'no binary path' };
199
- // Check for Homebrew install first (binary resolves to Caskroom/Cellar).
200
160
  if (isBrewInstalledBinary(binPath)) {
201
161
  const cask = getAgentBrewCask(id);
202
162
  if (cask)
203
163
  return { kind: 'brew', cask };
204
164
  return { kind: 'skip', reason: 'brew-installed but no known cask' };
205
165
  }
206
- // Check for npm global install.
207
166
  const npmBinDir = npmPrefix ? path.join(path.resolve(npmPrefix), 'bin') : null;
208
167
  const npmManaged = !!(npmBinDir && isPathInside(npmBinDir, binPath));
209
168
  if (!npmManaged)
@@ -223,14 +182,6 @@ async function runCommand(cmd, args, opts = {}) {
223
182
  let finished = false;
224
183
  const child = spawn(cmd, args, {
225
184
  stdio: ['ignore', 'pipe', 'pipe'],
226
- // HOMEBREW_NO_AUTO_UPDATE=1 skips brew's implicit `brew update` before
227
- // each command — we already resolve the latest version via the
228
- // formulae.brew.sh API, so the refresh is redundant. NOTE: this does NOT
229
- // prevent brew's vendor-install-ruby step, which contends with concurrent
230
- // brew processes (Homebrew's launchd autoupdate, a manual brew run, etc.)
231
- // on the `vendor-install-ruby` lockf and surfaces as "Failed to upgrade
232
- // Homebrew Portable Ruby". Those transient collisions are handled by
233
- // `isBrewBusyError` below, which downgrades the failure to a soft skip.
234
185
  env: { ...process.env, npm_config_yes: 'true', HOMEBREW_NO_AUTO_UPDATE: '1' },
235
186
  });
236
187
  const timeoutMs = Math.max(500, opts.timeoutMs ?? AGENT_UPDATE_COMMAND_TIMEOUT_MS);
@@ -274,9 +225,6 @@ async function getNpmGlobalRoot() {
274
225
  return result.ok ? result.stdout.trim().split('\n')[0] || null : null;
275
226
  }
276
227
  async function getLatestPackageVersion(pkg) {
277
- // `--prefer-online` bypasses the local npm metadata cache so we always see
278
- // the registry's current `latest` tag. Without it, `npm view` can serve a
279
- // stale version for several minutes after a release.
280
228
  const result = await runCommand('npm', ['view', pkg, 'version', '--json', '--prefer-online'], { timeoutMs: AGENT_UPDATE_TIMEOUTS.npmView });
281
229
  if (!result.ok)
282
230
  return null;
@@ -322,28 +270,11 @@ async function updateViaNpm(pkg) {
322
270
  const result = await runCommand('npm', ['install', '-g', `${pkg}@latest`]);
323
271
  return { ok: result.ok, detail: result.ok ? result.stdout.trim() || null : result.error };
324
272
  }
325
- /**
326
- * Detects the transient brew contention surfaced when another brew process is
327
- * already running its `vendor-install ruby` step (Homebrew's launchd
328
- * autoupdate, a manual `brew upgrade`, etc.). We treat these as soft skips
329
- * rather than failures so the dashboard doesn't shout an error at the user for
330
- * what is really "try again in a minute".
331
- */
332
273
  function isBrewBusyError(text) {
333
274
  if (!text)
334
275
  return false;
335
276
  return /vendor-install ruby|already locked|Failed to upgrade Homebrew Portable Ruby|is already running/i.test(text);
336
277
  }
337
- // ---------------------------------------------------------------------------
338
- // Homebrew helpers
339
- // ---------------------------------------------------------------------------
340
- /** Get latest available version for a Homebrew cask.
341
- *
342
- * Queries Homebrew's public formulae API rather than `brew info`, because the
343
- * local cask metadata is only refreshed by `brew update` — without it, a
344
- * just-published cask version can stay invisible for hours/days. The HTTPS API
345
- * always returns the current cask manifest. Falls back to local `brew info`
346
- * if the network call fails. */
347
278
  async function getLatestBrewCaskVersion(cask) {
348
279
  const apiVersion = await fetchBrewCaskVersionFromApi(cask);
349
280
  if (apiVersion)
@@ -413,7 +344,6 @@ export function startAgentAutoUpdate(opts) {
413
344
  setUpdateState(id, { status: 'skipped', detail: strategy.reason, checkedAt: Date.now() });
414
345
  continue;
415
346
  }
416
- // Use brew version check for brew installs, npm for npm installs.
417
347
  const latestVersion = strategy.kind === 'brew'
418
348
  ? await getLatestBrewCaskVersion(strategy.cask)
419
349
  : await getLatestPackageVersion(pkg);
@@ -456,10 +386,6 @@ export function startAgentAutoUpdate(opts) {
456
386
  }
457
387
  })();
458
388
  }
459
- // ---------------------------------------------------------------------------
460
- // On-demand version check (called from dashboard)
461
- // ---------------------------------------------------------------------------
462
- /** Check latest version for a single agent. Uses brew or npm depending on install method. */
463
389
  export async function checkAgentLatestVersion(agent) {
464
390
  const id = String(agent.agent || '').trim();
465
391
  const pkg = getAgentPackage(id);
@@ -467,7 +393,6 @@ export async function checkAgentLatestVersion(agent) {
467
393
  return { ...emptyState(), status: 'skipped', detail: 'unsupported agent', checkedAt: Date.now() };
468
394
  const currentVersion = extractAgentSemver(agent.version);
469
395
  setUpdateState(id, { currentVersion, status: 'checking' });
470
- // Detect brew install and use brew version check.
471
396
  const binPath = String(agent.path || '').trim();
472
397
  const brewCask = binPath && isBrewInstalledBinary(binPath) ? getAgentBrewCask(id) : null;
473
398
  const latestVersion = brewCask
@@ -490,7 +415,6 @@ export async function checkAgentLatestVersion(agent) {
490
415
  setUpdateState(id, state);
491
416
  return state;
492
417
  }
493
- /** Manually trigger an update for a specific agent (auto-detects brew vs npm). */
494
418
  export async function manualAgentUpdate(agent, log) {
495
419
  const id = String(agent.agent || '').trim();
496
420
  const pkg = getAgentPackage(id);
@@ -1,22 +1,3 @@
1
- /**
2
- * await-resume.ts — "waiting for background work" session marker.
3
- *
4
- * <sessionRoot>/awaiting.json — persisted marker
5
- *
6
- * A turn runs as a one-shot `claude -p` process that exits at its `result`
7
- * event. When the agent launches truly detached work (a daemon outliving the
8
- * harness, e.g. an install that must survive a restart) and ends the turn
9
- * intending to report back, the process exits and the session reads as plainly
10
- * "completed" — there is no live process for the background task to wake. This
11
- * marker lets the agent declare that intent (via the `await_background` MCP
12
- * tool, which writes the file directly) so the dashboard can show a distinct
13
- * "waiting" state instead of "completed".
14
- *
15
- * The MCP tool (child process) writes the file; the parent only reads and
16
- * clears it — the same split used by goal.ts / tools/goal.ts. The marker is
17
- * cleared the next time the session runs (clearAwaitResume in stream.ts), so a
18
- * resumed turn naturally drops back out of the "waiting" state.
19
- */
20
1
  import fs from 'node:fs';
21
2
  import path from 'node:path';
22
3
  const AWAIT_FILE = 'awaiting.json';
@@ -45,6 +26,5 @@ export function clearAwaitResume(workdir, agent, sessionId) {
45
26
  fs.rmSync(sessionAwaitPath(workdir, agent, sessionId), { force: true });
46
27
  }
47
28
  catch {
48
- /* best-effort */
49
29
  }
50
30
  }
@@ -1,19 +1,3 @@
1
- /**
2
- * CLI auth + install runner.
3
- *
4
- * Two flows share a single streaming-child-session core:
5
- *
6
- * - oauth-web: spawn `loginArgv`, stream stdout/stderr to the UI, poll
7
- * `statusArgv` in the background, and settle when the CLI reports ready.
8
- * Used by CLIs that print a device code / one-time-code non-interactively
9
- * (gh, wrangler, supabase, …). CLIs whose sign-in needs a real TTY use
10
- * `manualLoginCommands` instead and never reach this runner.
11
- * - install: spawn the `npm install -g <pkg>` argv, stream output, re-detect
12
- * once the child exits.
13
- *
14
- * Token CLIs (aws, mocli) skip this entirely — credentials come in via a plain
15
- * POST and are applied synchronously by `applyCliToken`.
16
- */
17
1
  import { spawn } from 'node:child_process';
18
2
  import { randomUUID } from 'node:crypto';
19
3
  import { EventEmitter } from 'node:events';
@@ -24,9 +8,6 @@ const MAX_SESSION_AGE_MS = 15 * 60 * 1000;
24
8
  const POLL_INTERVAL_MS = 2_000;
25
9
  const POLL_TIMEOUT_MS = 10 * 60 * 1000;
26
10
  const BACKLOG_LINES = 200;
27
- // ---------------------------------------------------------------------------
28
- // Session lifecycle
29
- // ---------------------------------------------------------------------------
30
11
  function reapExpiredSessions() {
31
12
  const now = Date.now();
32
13
  for (const [id, s] of SESSIONS) {
@@ -49,7 +30,6 @@ function startStreamingSession(opts) {
49
30
  const { cli, argv, env, shell, pollUntilReady, computeOk } = opts;
50
31
  const sessionId = randomUUID();
51
32
  const events = new EventEmitter();
52
- // Unlimited listeners — SSE clients may resubscribe multiple times.
53
33
  events.setMaxListeners(0);
54
34
  const session = {
55
35
  sessionId,
@@ -95,7 +75,7 @@ function startStreamingSession(opts) {
95
75
  try {
96
76
  finalStatus = await detectCli(cli);
97
77
  }
98
- catch { /* best-effort */ }
78
+ catch { }
99
79
  if (finalStatus) {
100
80
  events.emit('event', { type: 'status', status: finalStatus });
101
81
  }
@@ -124,15 +104,12 @@ function startStreamingSession(opts) {
124
104
  void settle(child.exitCode);
125
105
  }
126
106
  }
127
- catch { /* ignore polling errors */ }
107
+ catch { }
128
108
  }, POLL_INTERVAL_MS);
129
109
  }
130
110
  child.on('close', (code) => { void settle(code); });
131
111
  return { ok: true, sessionId };
132
112
  }
133
- // ---------------------------------------------------------------------------
134
- // oauth-web login
135
- // ---------------------------------------------------------------------------
136
113
  export async function startCliAuthSession(cliId) {
137
114
  const cli = getRecommendedCli(cliId);
138
115
  if (!cli)
@@ -140,8 +117,6 @@ export async function startCliAuthSession(cliId) {
140
117
  if (cli.auth.type !== 'oauth-web' || !cli.auth.loginArgv) {
141
118
  return { ok: false, error: `cli ${cliId} does not support oauth-web sign-in` };
142
119
  }
143
- // CLIs configured for manual login should never spawn here — guard so a
144
- // misbehaving client can't end-run the documented terminal flow.
145
120
  if (cli.auth.manualLoginCommands && cli.auth.manualLoginCommands.length > 0) {
146
121
  return { ok: false, error: `cli ${cliId} uses manual sign-in (run the commands in your own terminal)` };
147
122
  }
@@ -155,12 +130,6 @@ export async function startCliAuthSession(cliId) {
155
130
  }
156
131
  const NPM_GLOBAL_INSTALL_RE = /^npm\s+(?:install|i)\s+(?:-g|--global)\s+(\S.*)$/;
157
132
  const SHELL_METACHAR_RE = /[|;&`$()<>]/;
158
- /**
159
- * Inspect a CLI's install spec for the current platform and return a single
160
- * argv that's safe to spawn without user-side approvals. Returns null when no
161
- * such command exists (brew/apt/dnf/winget/scoop/curl-pipe-sh entries are all
162
- * rejected on purpose — those require sudo or interactive confirmation).
163
- */
164
133
  export function resolveAutoInstallSpec(cli, platform) {
165
134
  const commands = cli.install[platform] || [];
166
135
  for (const c of commands) {
@@ -190,24 +159,14 @@ export async function startCliInstallSession(cliId) {
190
159
  cli,
191
160
  argv: spec.argv,
192
161
  env: { ...process.env, NO_COLOR: '1', TERM: 'dumb', CI: '1' },
193
- // npm on Windows is npm.cmd — needs shell resolution. spawn() args are
194
- // still passed argv-style; we don't concat into a shell string.
195
162
  shell: process.platform === 'win32',
196
163
  pollUntilReady: false,
197
164
  computeOk: (exit, status) => exit === 0 && (status ? status.state !== 'not_installed' : true),
198
165
  });
199
166
  }
200
- // ---------------------------------------------------------------------------
201
- // Token auth — apply-credentials flow
202
- // ---------------------------------------------------------------------------
203
167
  import fs from 'node:fs';
204
168
  import path from 'node:path';
205
169
  import os from 'node:os';
206
- /**
207
- * Apply a set of credentials for a `token`-auth CLI and verify. Each CLI gets a
208
- * tailored write path because there's no universal convention — we only do this
209
- * for CLIs we explicitly support.
210
- */
211
170
  export async function applyCliToken(cliId, values) {
212
171
  const cli = getRecommendedCli(cliId);
213
172
  if (!cli)
@@ -232,8 +191,6 @@ export async function applyCliToken(cliId, values) {
232
191
  }
233
192
  }
234
193
  else if (cli.id === 'mocli') {
235
- // mocli stores its key via `mocli auth init --apik <KEY>` — let the CLI
236
- // own its storage path so future schema changes upstream don't break us.
237
194
  const key = (values.MOWEN_API_KEY || '').trim();
238
195
  if (!key)
239
196
  return { ok: false, error: 'Mowen API Key is required' };
@@ -262,16 +219,12 @@ export async function applyCliToken(cliId, values) {
262
219
  return { ok: false, error: e?.message || 'failed to apply token' };
263
220
  }
264
221
  }
265
- /**
266
- * Merge a `[section]` block into an INI-style file, replacing any existing
267
- * block with the same name. Idempotent; preserves other sections and comments.
268
- */
269
222
  function mergeIniSection(filePath, section, block) {
270
223
  let current = '';
271
224
  try {
272
225
  current = fs.readFileSync(filePath, 'utf-8');
273
226
  }
274
- catch { /* new file */ }
227
+ catch { }
275
228
  const header = `[${section}]`;
276
229
  const lines = current.split(/\r?\n/);
277
230
  const out = [];
@@ -287,7 +240,6 @@ function mergeIniSection(filePath, section, block) {
287
240
  inTarget = false;
288
241
  out.push(line);
289
242
  }
290
- // else: drop old line
291
243
  }
292
244
  else {
293
245
  out.push(line);
@@ -1,6 +1,3 @@
1
- /**
2
- * CLI catalog — merges the recommended registry with live detection status.
3
- */
4
1
  import { getRecommendedClis } from './registry.js';
5
2
  import { detectCli, getCachedCliStatus, currentPlatform } from './detector.js';
6
3
  import { resolveAutoInstallSpec } from './auth.js';