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.
- package/dashboard/dist/assets/{AgentTab-BNJ73P0m.js → AgentTab-DtjuI8NU.js} +1 -1
- package/dashboard/dist/assets/ConnectionModal-BIx6a5o1.js +1 -0
- package/dashboard/dist/assets/{DirBrowser-D-GT-G-w.js → DirBrowser-BINh_7NB.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-BrJMulJR.js → ExtensionsTab-CHpd147b.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-auPnx468.js → IMAccessTab-C4DbGxbG.js} +1 -1
- package/dashboard/dist/assets/{Modal-lYfO9m-I.js → Modal-D_PUi-5O.js} +1 -1
- package/dashboard/dist/assets/{Modals-PRdWOIY9.js → Modals-BahDEMqf.js} +1 -1
- package/dashboard/dist/assets/{Select-BhbU-rll.js → Select-Cp5VXb_2.js} +1 -1
- package/dashboard/dist/assets/SessionPanel-DZVNXptx.js +1 -0
- package/dashboard/dist/assets/SystemTab-CLznS31f.js +1 -0
- package/dashboard/dist/assets/index-CLGovkkw.js +3 -0
- package/dashboard/dist/assets/index-CXIN3nTr.css +1 -0
- package/dashboard/dist/assets/index-DcYOI6rL.js +23 -0
- package/dashboard/dist/assets/{shared-C5uXvhZJ.js → shared-C8DCTJ44.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/agent/acp-client.js +0 -59
- package/dist/agent/artifacts.js +1 -38
- package/dist/agent/auto-update.js +2 -78
- package/dist/agent/await-resume.js +0 -20
- package/dist/agent/cli/auth.js +3 -51
- package/dist/agent/cli/catalog.js +0 -3
- package/dist/agent/cli/detector.js +1 -19
- package/dist/agent/cli/index.js +0 -3
- package/dist/agent/cli/registry.js +0 -25
- package/dist/agent/driver.js +0 -14
- package/dist/agent/drivers/claude-tui.js +2 -769
- package/dist/agent/drivers/claude.js +12 -631
- package/dist/agent/drivers/codex.js +11 -162
- package/dist/agent/drivers/gemini.js +6 -114
- package/dist/agent/drivers/hermes.js +0 -128
- package/dist/agent/goal.js +0 -45
- package/dist/agent/handover.js +1 -46
- package/dist/agent/images.js +7 -133
- package/dist/agent/index.js +1 -29
- package/dist/agent/mcp/bridge.js +1 -197
- package/dist/agent/mcp/extensions.js +5 -84
- package/dist/agent/mcp/oauth.js +2 -69
- package/dist/agent/mcp/registry.js +2 -33
- package/dist/agent/mcp/session-server.js +2 -45
- package/dist/agent/mcp/tools/ask-user.js +0 -6
- package/dist/agent/mcp/tools/await-resume.js +0 -15
- package/dist/agent/mcp/tools/goal.js +0 -25
- package/dist/agent/mcp/tools/types.js +0 -5
- package/dist/agent/mcp/tools/workspace.js +0 -23
- package/dist/agent/npm.js +0 -14
- package/dist/agent/session.js +86 -196
- package/dist/agent/skill-installer.js +3 -70
- package/dist/agent/skills.js +0 -43
- package/dist/agent/stream.js +5 -148
- package/dist/agent/types.js +0 -12
- package/dist/agent/utils.js +2 -120
- package/dist/bot/bot.js +3 -468
- package/dist/bot/command-ui.js +0 -45
- package/dist/bot/commands.js +0 -51
- package/dist/bot/headless-bot.js +0 -21
- package/dist/bot/host.js +1 -5
- package/dist/bot/human-loop.js +0 -8
- package/dist/bot/menu.js +0 -3
- package/dist/bot/orchestration.js +0 -3
- package/dist/bot/render-shared.js +0 -63
- package/dist/bot/session-hub.js +14 -92
- package/dist/bot/session-status.js +1 -5
- package/dist/bot/streaming.js +0 -53
- package/dist/browser-profile.js +0 -51
- package/dist/browser-supervisor.js +0 -75
- package/dist/catalog/cli-tools.js +0 -53
- package/dist/catalog/index.js +0 -18
- package/dist/catalog/local-models.js +0 -18
- package/dist/catalog/mcp-servers.js +0 -35
- package/dist/catalog/skill-repos.js +0 -32
- package/dist/channels/base.js +0 -19
- package/dist/channels/dingtalk/bot.js +0 -3
- package/dist/channels/dingtalk/channel.js +0 -33
- package/dist/channels/discord/bot.js +0 -7
- package/dist/channels/discord/channel.js +0 -22
- package/dist/channels/feishu/bot.js +0 -71
- package/dist/channels/feishu/channel.js +0 -78
- package/dist/channels/feishu/markdown.js +0 -5
- package/dist/channels/feishu/render.js +0 -45
- package/dist/channels/health.js +0 -46
- package/dist/channels/slack/bot.js +0 -7
- package/dist/channels/slack/channel.js +1 -27
- package/dist/channels/states.js +0 -3
- package/dist/channels/telegram/bot.js +0 -60
- package/dist/channels/telegram/channel.js +2 -99
- package/dist/channels/telegram/directory.js +0 -3
- package/dist/channels/telegram/live-preview.js +0 -8
- package/dist/channels/telegram/render.js +0 -30
- package/dist/channels/wecom/bot.js +0 -3
- package/dist/channels/wecom/channel.js +0 -58
- package/dist/channels/weixin/api.js +0 -9
- package/dist/channels/weixin/bot.js +0 -88
- package/dist/channels/weixin/channel.js +0 -6
- package/dist/cli/autostart.js +1 -44
- package/dist/cli/channel-supervisor.js +1 -59
- package/dist/cli/channels.js +0 -6
- package/dist/cli/main.js +1 -145
- package/dist/cli/onboarding.js +0 -6
- package/dist/cli/run.js +0 -11
- package/dist/cli/setup-wizard.js +0 -3
- package/dist/core/config/runtime-config.js +0 -38
- package/dist/core/config/user-config.js +0 -63
- package/dist/core/config/validation.js +0 -9
- package/dist/core/constants.js +0 -268
- package/dist/core/git.js +0 -31
- package/dist/core/legacy-compat.js +1 -28
- package/dist/core/logging.js +0 -3
- package/dist/core/platform.js +0 -28
- package/dist/core/process-control.js +0 -8
- package/dist/core/secrets/index.js +0 -17
- package/dist/core/secrets/inline-seal.js +0 -7
- package/dist/core/secrets/ref.js +0 -19
- package/dist/core/secrets/resolver.js +0 -8
- package/dist/core/secrets/store.js +1 -12
- package/dist/core/utils.js +1 -37
- package/dist/core/version.js +0 -3
- package/dist/dashboard/platform.js +1 -35
- package/dist/dashboard/routes/agents.js +2 -71
- package/dist/dashboard/routes/cli.js +0 -27
- package/dist/dashboard/routes/config.js +0 -54
- package/dist/dashboard/routes/extensions.js +0 -132
- package/dist/dashboard/routes/local-models.js +0 -68
- package/dist/dashboard/routes/models.js +0 -46
- package/dist/dashboard/routes/sessions.js +0 -100
- package/dist/dashboard/runtime.js +1 -58
- package/dist/dashboard/server.js +0 -49
- package/dist/dashboard/session-control.js +2 -69
- package/dist/model/catalog.js +1 -17
- package/dist/model/index.js +0 -15
- package/dist/model/injector.js +1 -181
- package/dist/model/provider-models.js +1 -48
- package/dist/model/responses-bridge.js +3 -45
- package/dist/model/store.js +0 -17
- package/dist/model/types.js +0 -12
- package/dist/model/validation.js +1 -18
- package/dist/pikichannel/adapter-pikiloom.js +0 -31
- package/dist/pikichannel/code.js +0 -12
- package/dist/pikichannel/codec.js +0 -14
- package/dist/pikichannel/host.js +4 -33
- package/dist/pikichannel/protocol.js +0 -52
- package/dist/pikichannel/rendezvous-broker.js +5 -26
- package/dist/pikichannel/rendezvous-host.js +3 -16
- package/dist/pikichannel/server.js +6 -55
- package/dist/pikichannel/transport.js +0 -25
- package/dist/pikichannel/transports/webrtc-host.js +0 -18
- package/dist/pikichannel/transports/webrtc-shared.js +4 -34
- package/dist/pikichannel/transports/websocket-host.js +1 -14
- package/dist/pikichannel/turn.js +2 -69
- package/dist/pikichannel/web/sdk.js +14 -71
- package/package.json +1 -1
- package/dashboard/dist/assets/ConnectionModal-Dt05H2f9.js +0 -1
- package/dashboard/dist/assets/SessionPanel-_0dLWvEb.js +0 -1
- package/dashboard/dist/assets/SystemTab-CN_QrRKt.js +0 -1
- package/dashboard/dist/assets/index-CP3LYJR2.js +0 -23
- package/dashboard/dist/assets/index-C_neLnaO.js +0 -3
- package/dashboard/dist/assets/index-CyCspgtc.css +0 -1
package/dist/agent/acp-client.js
CHANGED
|
@@ -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 });
|
package/dist/agent/artifacts.js
CHANGED
|
@@ -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 {
|
|
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;
|
|
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);
|
|
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
|
}
|
package/dist/agent/cli/auth.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
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';
|