pikiloop 0.4.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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +353 -0
  3. package/README.v2.md +287 -0
  4. package/README.zh-CN.md +352 -0
  5. package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
  6. package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
  7. package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
  8. package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
  9. package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
  10. package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
  11. package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
  12. package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
  13. package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
  14. package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
  15. package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
  16. package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
  17. package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
  18. package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
  19. package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
  20. package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
  21. package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
  22. package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
  23. package/dashboard/dist/assets/index-reSbuley.css +1 -0
  24. package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
  25. package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
  26. package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
  27. package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
  28. package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
  29. package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
  30. package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
  31. package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
  32. package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
  33. package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
  34. package/dashboard/dist/favicon.svg +28 -0
  35. package/dashboard/dist/index.html +17 -0
  36. package/dist/agent/acp-client.js +261 -0
  37. package/dist/agent/auto-update.js +432 -0
  38. package/dist/agent/await-resume.js +50 -0
  39. package/dist/agent/cli/auth.js +325 -0
  40. package/dist/agent/cli/catalog.js +40 -0
  41. package/dist/agent/cli/detector.js +136 -0
  42. package/dist/agent/cli/index.js +7 -0
  43. package/dist/agent/cli/registry.js +33 -0
  44. package/dist/agent/driver.js +39 -0
  45. package/dist/agent/drivers/claude-tui.js +2297 -0
  46. package/dist/agent/drivers/claude.js +2689 -0
  47. package/dist/agent/drivers/codex.js +2210 -0
  48. package/dist/agent/drivers/gemini.js +1059 -0
  49. package/dist/agent/drivers/hermes.js +795 -0
  50. package/dist/agent/goal.js +274 -0
  51. package/dist/agent/handover.js +130 -0
  52. package/dist/agent/images.js +355 -0
  53. package/dist/agent/index.js +50 -0
  54. package/dist/agent/mcp/bridge.js +791 -0
  55. package/dist/agent/mcp/extensions.js +637 -0
  56. package/dist/agent/mcp/oauth.js +353 -0
  57. package/dist/agent/mcp/registry.js +119 -0
  58. package/dist/agent/mcp/session-server.js +229 -0
  59. package/dist/agent/mcp/tools/ask-user.js +113 -0
  60. package/dist/agent/mcp/tools/await-resume.js +77 -0
  61. package/dist/agent/mcp/tools/goal.js +144 -0
  62. package/dist/agent/mcp/tools/types.js +12 -0
  63. package/dist/agent/mcp/tools/workspace.js +212 -0
  64. package/dist/agent/npm.js +31 -0
  65. package/dist/agent/session.js +1206 -0
  66. package/dist/agent/skill-installer.js +160 -0
  67. package/dist/agent/skills.js +257 -0
  68. package/dist/agent/stream.js +743 -0
  69. package/dist/agent/types.js +13 -0
  70. package/dist/agent/utils.js +687 -0
  71. package/dist/bot/bot.js +2499 -0
  72. package/dist/bot/command-ui.js +633 -0
  73. package/dist/bot/commands.js +513 -0
  74. package/dist/bot/headless-bot.js +36 -0
  75. package/dist/bot/host.js +192 -0
  76. package/dist/bot/human-loop.js +168 -0
  77. package/dist/bot/menu.js +48 -0
  78. package/dist/bot/orchestration.js +79 -0
  79. package/dist/bot/render-shared.js +309 -0
  80. package/dist/bot/session-hub.js +361 -0
  81. package/dist/bot/session-status.js +55 -0
  82. package/dist/bot/streaming.js +309 -0
  83. package/dist/browser-profile.js +579 -0
  84. package/dist/browser-supervisor.js +249 -0
  85. package/dist/catalog/cli-tools.js +421 -0
  86. package/dist/catalog/index.js +21 -0
  87. package/dist/catalog/local-models.js +94 -0
  88. package/dist/catalog/mcp-servers.js +315 -0
  89. package/dist/catalog/skill-repos.js +173 -0
  90. package/dist/channels/base.js +55 -0
  91. package/dist/channels/dingtalk/bot.js +549 -0
  92. package/dist/channels/dingtalk/channel.js +268 -0
  93. package/dist/channels/discord/bot.js +552 -0
  94. package/dist/channels/discord/channel.js +245 -0
  95. package/dist/channels/feishu/bot.js +1275 -0
  96. package/dist/channels/feishu/channel.js +911 -0
  97. package/dist/channels/feishu/markdown.js +91 -0
  98. package/dist/channels/feishu/render.js +619 -0
  99. package/dist/channels/health.js +109 -0
  100. package/dist/channels/slack/bot.js +554 -0
  101. package/dist/channels/slack/channel.js +283 -0
  102. package/dist/channels/states.js +6 -0
  103. package/dist/channels/telegram/bot.js +1310 -0
  104. package/dist/channels/telegram/channel.js +820 -0
  105. package/dist/channels/telegram/directory.js +111 -0
  106. package/dist/channels/telegram/live-preview.js +220 -0
  107. package/dist/channels/telegram/render.js +384 -0
  108. package/dist/channels/wecom/bot.js +558 -0
  109. package/dist/channels/wecom/channel.js +479 -0
  110. package/dist/channels/weixin/api.js +520 -0
  111. package/dist/channels/weixin/bot.js +1000 -0
  112. package/dist/channels/weixin/channel.js +222 -0
  113. package/dist/cli/autostart.js +262 -0
  114. package/dist/cli/channel-supervisor.js +313 -0
  115. package/dist/cli/channels.js +54 -0
  116. package/dist/cli/main.js +726 -0
  117. package/dist/cli/onboarding.js +227 -0
  118. package/dist/cli/run.js +308 -0
  119. package/dist/cli/setup-wizard.js +235 -0
  120. package/dist/core/config/runtime-config.js +201 -0
  121. package/dist/core/config/user-config.js +510 -0
  122. package/dist/core/config/validation.js +521 -0
  123. package/dist/core/constants.js +400 -0
  124. package/dist/core/git.js +145 -0
  125. package/dist/core/legacy-compat.js +60 -0
  126. package/dist/core/logging.js +101 -0
  127. package/dist/core/platform.js +59 -0
  128. package/dist/core/process-control.js +315 -0
  129. package/dist/core/secrets/index.js +42 -0
  130. package/dist/core/secrets/inline-seal.js +60 -0
  131. package/dist/core/secrets/ref.js +33 -0
  132. package/dist/core/secrets/resolver.js +65 -0
  133. package/dist/core/secrets/store.js +63 -0
  134. package/dist/core/utils.js +233 -0
  135. package/dist/core/version.js +15 -0
  136. package/dist/dashboard/platform.js +219 -0
  137. package/dist/dashboard/routes/agents.js +450 -0
  138. package/dist/dashboard/routes/cli.js +174 -0
  139. package/dist/dashboard/routes/config.js +523 -0
  140. package/dist/dashboard/routes/extensions.js +745 -0
  141. package/dist/dashboard/routes/local-models.js +290 -0
  142. package/dist/dashboard/routes/models.js +324 -0
  143. package/dist/dashboard/routes/sessions.js +838 -0
  144. package/dist/dashboard/runtime.js +410 -0
  145. package/dist/dashboard/server.js +237 -0
  146. package/dist/dashboard/session-control.js +347 -0
  147. package/dist/model/catalog.js +104 -0
  148. package/dist/model/index.js +20 -0
  149. package/dist/model/injector.js +272 -0
  150. package/dist/model/provider-models.js +112 -0
  151. package/dist/model/store.js +212 -0
  152. package/dist/model/types.js +13 -0
  153. package/dist/model/validation.js +203 -0
  154. package/package.json +82 -0
@@ -0,0 +1,325 @@
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
+ import { spawn } from 'node:child_process';
18
+ import { randomUUID } from 'node:crypto';
19
+ import { EventEmitter } from 'node:events';
20
+ import { detectCli, invalidateCliStatus, currentPlatform } from './detector.js';
21
+ import { getRecommendedCli } from './registry.js';
22
+ const SESSIONS = new Map();
23
+ const MAX_SESSION_AGE_MS = 15 * 60 * 1000;
24
+ const POLL_INTERVAL_MS = 2_000;
25
+ const POLL_TIMEOUT_MS = 10 * 60 * 1000;
26
+ const BACKLOG_LINES = 200;
27
+ // ---------------------------------------------------------------------------
28
+ // Session lifecycle
29
+ // ---------------------------------------------------------------------------
30
+ function reapExpiredSessions() {
31
+ const now = Date.now();
32
+ for (const [id, s] of SESSIONS) {
33
+ if (s.done && now - s.startedAt > MAX_SESSION_AGE_MS)
34
+ SESSIONS.delete(id);
35
+ }
36
+ }
37
+ export function getAuthSession(sessionId) {
38
+ reapExpiredSessions();
39
+ return SESSIONS.get(sessionId);
40
+ }
41
+ export function cancelAuthSession(sessionId) {
42
+ const s = SESSIONS.get(sessionId);
43
+ if (!s || s.done)
44
+ return false;
45
+ s._child?.kill?.('SIGTERM');
46
+ return true;
47
+ }
48
+ function startStreamingSession(opts) {
49
+ const { cli, argv, env, shell, pollUntilReady, computeOk } = opts;
50
+ const sessionId = randomUUID();
51
+ const events = new EventEmitter();
52
+ // Unlimited listeners — SSE clients may resubscribe multiple times.
53
+ events.setMaxListeners(0);
54
+ const session = {
55
+ sessionId,
56
+ cliId: cli.id,
57
+ startedAt: Date.now(),
58
+ events,
59
+ done: false,
60
+ ok: false,
61
+ exitCode: null,
62
+ backlog: [],
63
+ };
64
+ SESSIONS.set(sessionId, session);
65
+ const [cmd, ...args] = argv;
66
+ const child = spawn(cmd, args, {
67
+ stdio: ['ignore', 'pipe', 'pipe'],
68
+ env,
69
+ shell,
70
+ windowsHide: true,
71
+ });
72
+ session._child = child;
73
+ const pushOutput = (chunk) => {
74
+ session.backlog.push(chunk);
75
+ if (session.backlog.length > BACKLOG_LINES) {
76
+ session.backlog.splice(0, session.backlog.length - BACKLOG_LINES);
77
+ }
78
+ events.emit('event', { type: 'output', chunk });
79
+ };
80
+ child.stdout?.on('data', (buf) => pushOutput(buf.toString('utf8')));
81
+ child.stderr?.on('data', (buf) => pushOutput(buf.toString('utf8')));
82
+ child.on('error', (err) => {
83
+ events.emit('event', { type: 'error', message: err.message });
84
+ });
85
+ let settled = false;
86
+ let poller;
87
+ const settle = async (exitCode) => {
88
+ if (settled)
89
+ return;
90
+ settled = true;
91
+ if (poller)
92
+ clearInterval(poller);
93
+ invalidateCliStatus(cli.id);
94
+ let finalStatus;
95
+ try {
96
+ finalStatus = await detectCli(cli);
97
+ }
98
+ catch { /* best-effort */ }
99
+ if (finalStatus) {
100
+ events.emit('event', { type: 'status', status: finalStatus });
101
+ }
102
+ session.ok = computeOk(exitCode, finalStatus);
103
+ session.exitCode = exitCode;
104
+ session.done = true;
105
+ events.emit('event', { type: 'done', ok: session.ok, exitCode });
106
+ };
107
+ if (pollUntilReady) {
108
+ const pollDeadline = Date.now() + POLL_TIMEOUT_MS;
109
+ poller = setInterval(async () => {
110
+ if (settled)
111
+ return;
112
+ if (Date.now() > pollDeadline) {
113
+ child.kill('SIGTERM');
114
+ void settle(null);
115
+ return;
116
+ }
117
+ try {
118
+ invalidateCliStatus(cli.id);
119
+ const status = await detectCli(cli);
120
+ events.emit('event', { type: 'status', status });
121
+ if (status.state === 'ready') {
122
+ if (!child.killed)
123
+ child.kill('SIGTERM');
124
+ void settle(child.exitCode);
125
+ }
126
+ }
127
+ catch { /* ignore polling errors */ }
128
+ }, POLL_INTERVAL_MS);
129
+ }
130
+ child.on('close', (code) => { void settle(code); });
131
+ return { ok: true, sessionId };
132
+ }
133
+ // ---------------------------------------------------------------------------
134
+ // oauth-web login
135
+ // ---------------------------------------------------------------------------
136
+ export async function startCliAuthSession(cliId) {
137
+ const cli = getRecommendedCli(cliId);
138
+ if (!cli)
139
+ return { ok: false, error: `unknown cli: ${cliId}` };
140
+ if (cli.auth.type !== 'oauth-web' || !cli.auth.loginArgv) {
141
+ return { ok: false, error: `cli ${cliId} does not support oauth-web sign-in` };
142
+ }
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
+ if (cli.auth.manualLoginCommands && cli.auth.manualLoginCommands.length > 0) {
146
+ return { ok: false, error: `cli ${cliId} uses manual sign-in (run the commands in your own terminal)` };
147
+ }
148
+ return startStreamingSession({
149
+ cli,
150
+ argv: cli.auth.loginArgv,
151
+ env: { ...process.env, NO_COLOR: '1', TERM: 'dumb', CI: '' },
152
+ pollUntilReady: true,
153
+ computeOk: (_exit, status) => status?.state === 'ready',
154
+ });
155
+ }
156
+ const NPM_GLOBAL_INSTALL_RE = /^npm\s+(?:install|i)\s+(?:-g|--global)\s+(\S.*)$/;
157
+ 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
+ export function resolveAutoInstallSpec(cli, platform) {
165
+ const commands = cli.install[platform] || [];
166
+ for (const c of commands) {
167
+ const cmd = c.cmd.trim();
168
+ if (SHELL_METACHAR_RE.test(cmd))
169
+ continue;
170
+ if (/^sudo\b/i.test(cmd))
171
+ continue;
172
+ const m = cmd.match(NPM_GLOBAL_INSTALL_RE);
173
+ if (!m)
174
+ continue;
175
+ const pkgs = m[1].split(/\s+/).filter(Boolean);
176
+ if (pkgs.length === 0)
177
+ continue;
178
+ return { argv: ['npm', 'install', '-g', ...pkgs], label: c.label || 'npm' };
179
+ }
180
+ return null;
181
+ }
182
+ export async function startCliInstallSession(cliId) {
183
+ const cli = getRecommendedCli(cliId);
184
+ if (!cli)
185
+ return { ok: false, error: `unknown cli: ${cliId}` };
186
+ const spec = resolveAutoInstallSpec(cli, currentPlatform());
187
+ if (!spec)
188
+ return { ok: false, error: `no auto-install command available for ${cliId}` };
189
+ return startStreamingSession({
190
+ cli,
191
+ argv: spec.argv,
192
+ 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
+ shell: process.platform === 'win32',
196
+ pollUntilReady: false,
197
+ computeOk: (exit, status) => exit === 0 && (status ? status.state !== 'not_installed' : true),
198
+ });
199
+ }
200
+ // ---------------------------------------------------------------------------
201
+ // Token auth — apply-credentials flow
202
+ // ---------------------------------------------------------------------------
203
+ import fs from 'node:fs';
204
+ import path from 'node:path';
205
+ 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
+ export async function applyCliToken(cliId, values) {
212
+ const cli = getRecommendedCli(cliId);
213
+ if (!cli)
214
+ return { ok: false, error: `unknown cli: ${cliId}` };
215
+ if (cli.auth.type !== 'token')
216
+ return { ok: false, error: `cli ${cliId} does not use token auth` };
217
+ try {
218
+ if (cli.id === 'aws') {
219
+ const id = (values.AWS_ACCESS_KEY_ID || '').trim();
220
+ const secret = (values.AWS_SECRET_ACCESS_KEY || '').trim();
221
+ const region = (values.AWS_DEFAULT_REGION || '').trim();
222
+ if (!id || !secret)
223
+ return { ok: false, error: 'AWS access key ID and secret are required' };
224
+ const awsDir = path.join(os.homedir(), '.aws');
225
+ fs.mkdirSync(awsDir, { recursive: true, mode: 0o700 });
226
+ const credPath = path.join(awsDir, 'credentials');
227
+ const credBody = `[default]\naws_access_key_id = ${id}\naws_secret_access_key = ${secret}\n`;
228
+ mergeIniSection(credPath, 'default', credBody);
229
+ if (region) {
230
+ const configPath = path.join(awsDir, 'config');
231
+ mergeIniSection(configPath, 'default', `[default]\nregion = ${region}\n`);
232
+ }
233
+ }
234
+ 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
+ const key = (values.MOWEN_API_KEY || '').trim();
238
+ if (!key)
239
+ return { ok: false, error: 'Mowen API Key is required' };
240
+ const applied = await new Promise((resolve) => {
241
+ const child = spawn('mocli', ['auth', 'init', '--apik', key], {
242
+ stdio: ['ignore', 'pipe', 'pipe'],
243
+ env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
244
+ });
245
+ let stderr = '';
246
+ child.stderr?.on('data', b => { stderr += b.toString('utf8'); });
247
+ child.on('error', err => resolve({ ok: false, stderr: err.message }));
248
+ child.on('close', code => resolve({ ok: code === 0, stderr }));
249
+ });
250
+ if (!applied.ok) {
251
+ return { ok: false, error: applied.stderr.trim().slice(0, 200) || 'mocli auth init failed' };
252
+ }
253
+ }
254
+ else {
255
+ return { ok: false, error: `token auth is not implemented for ${cliId}` };
256
+ }
257
+ invalidateCliStatus(cliId);
258
+ const status = await detectCli(cli);
259
+ return { ok: status.state === 'ready', status, error: status.state === 'ready' ? undefined : status.authDetail };
260
+ }
261
+ catch (e) {
262
+ return { ok: false, error: e?.message || 'failed to apply token' };
263
+ }
264
+ }
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
+ function mergeIniSection(filePath, section, block) {
270
+ let current = '';
271
+ try {
272
+ current = fs.readFileSync(filePath, 'utf-8');
273
+ }
274
+ catch { /* new file */ }
275
+ const header = `[${section}]`;
276
+ const lines = current.split(/\r?\n/);
277
+ const out = [];
278
+ let inTarget = false;
279
+ for (const line of lines) {
280
+ const trimmed = line.trim();
281
+ if (trimmed === header) {
282
+ inTarget = true;
283
+ continue;
284
+ }
285
+ if (inTarget) {
286
+ if (/^\[.+\]$/.test(trimmed)) {
287
+ inTarget = false;
288
+ out.push(line);
289
+ }
290
+ // else: drop old line
291
+ }
292
+ else {
293
+ out.push(line);
294
+ }
295
+ }
296
+ let body = out.join('\n').replace(/\n+$/, '');
297
+ if (body)
298
+ body += '\n\n';
299
+ body += block.trim() + '\n';
300
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
301
+ fs.writeFileSync(filePath, body, { mode: 0o600 });
302
+ }
303
+ export async function logoutCli(cliId) {
304
+ const cli = getRecommendedCli(cliId);
305
+ if (!cli)
306
+ return { ok: false, error: `unknown cli: ${cliId}` };
307
+ if (!cli.auth.logoutArgv || cli.auth.logoutArgv.length === 0) {
308
+ return { ok: false, error: `cli ${cliId} has no logout command` };
309
+ }
310
+ return new Promise((resolve) => {
311
+ const [cmd, ...args] = cli.auth.logoutArgv;
312
+ const child = spawn(cmd, args, {
313
+ stdio: ['ignore', 'pipe', 'pipe'],
314
+ env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' },
315
+ });
316
+ let stderr = '';
317
+ child.stderr?.on('data', b => { stderr += b.toString('utf8'); });
318
+ child.on('error', err => resolve({ ok: false, error: err.message }));
319
+ child.on('close', async () => {
320
+ invalidateCliStatus(cliId);
321
+ const status = await detectCli(cli).catch(() => undefined);
322
+ resolve({ ok: true, status, error: stderr.trim() ? stderr.trim().slice(0, 200) : undefined });
323
+ });
324
+ });
325
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * CLI catalog — merges the recommended registry with live detection status.
3
+ */
4
+ import { getRecommendedClis } from './registry.js';
5
+ import { detectCli, getCachedCliStatus, currentPlatform } from './detector.js';
6
+ import { resolveAutoInstallSpec } from './auth.js';
7
+ export async function getCliCatalog() {
8
+ const recs = getRecommendedClis();
9
+ const platform = currentPlatform();
10
+ const results = await Promise.all(recs.map(async (cli) => {
11
+ const cached = getCachedCliStatus(cli.id);
12
+ const status = cached ?? await detectCli(cli);
13
+ const auto = resolveAutoInstallSpec(cli, platform);
14
+ return {
15
+ id: cli.id,
16
+ binary: cli.binary,
17
+ name: cli.name,
18
+ description: cli.description,
19
+ descriptionZh: cli.descriptionZh,
20
+ category: cli.category,
21
+ iconSlug: cli.iconSlug,
22
+ iconUrl: cli.iconUrl,
23
+ homepage: cli.homepage,
24
+ install: cli.install,
25
+ auth: cli.auth,
26
+ state: status.state,
27
+ version: status.version,
28
+ authDetail: status.authDetail,
29
+ platform,
30
+ autoInstall: auto ? { label: auto.label } : undefined,
31
+ };
32
+ }));
33
+ return results;
34
+ }
35
+ export async function refreshCliStatus(id) {
36
+ const cli = getRecommendedClis().find(c => c.id === id);
37
+ if (!cli)
38
+ return undefined;
39
+ return await detectCli(cli);
40
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * CLI detector — checks whether a CLI binary is on PATH, what version it is,
3
+ * and whether the user is already signed in.
4
+ *
5
+ * Results are cached with a short TTL so catalog rendering stays snappy.
6
+ */
7
+ import { execFile } from 'node:child_process';
8
+ import path from 'node:path';
9
+ import os from 'node:os';
10
+ import fs from 'node:fs';
11
+ const DETECT_TTL_MS = 30_000;
12
+ const cache = new Map();
13
+ // ---------------------------------------------------------------------------
14
+ // Helpers
15
+ // ---------------------------------------------------------------------------
16
+ function runArgv(argv, timeoutMs) {
17
+ return new Promise((resolve) => {
18
+ const [cmd, ...rest] = argv;
19
+ execFile(cmd, rest, {
20
+ timeout: timeoutMs,
21
+ env: { ...process.env, NO_COLOR: '1', CLICOLOR: '0', TERM: 'dumb' },
22
+ shell: false,
23
+ windowsHide: true,
24
+ }, (err, stdout, stderr) => {
25
+ const code = err?.code;
26
+ const exitCode = typeof code === 'number' ? code : (err ? 1 : 0);
27
+ resolve({
28
+ ok: !err,
29
+ stdout: stdout?.toString() || '',
30
+ stderr: stderr?.toString() || '',
31
+ code: typeof exitCode === 'number' ? exitCode : null,
32
+ });
33
+ });
34
+ });
35
+ }
36
+ function which(binary) {
37
+ const isWin = process.platform === 'win32';
38
+ const exts = isWin ? (process.env.PATHEXT || '.EXE;.CMD;.BAT').split(';') : [''];
39
+ const sep = isWin ? ';' : ':';
40
+ const pathDirs = (process.env.PATH || '').split(sep);
41
+ for (const dir of pathDirs) {
42
+ if (!dir)
43
+ continue;
44
+ for (const ext of exts) {
45
+ const candidate = path.join(dir, binary + ext);
46
+ try {
47
+ const stat = fs.statSync(candidate);
48
+ if (stat.isFile())
49
+ return candidate;
50
+ }
51
+ catch { /* next */ }
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+ function extractVersion(stdout, stderr) {
57
+ const text = (stdout || stderr).trim();
58
+ if (!text)
59
+ return undefined;
60
+ const firstLine = text.split(/\r?\n/, 1)[0].trim();
61
+ // Common patterns: "gh version 2.56.0 (...)" / "1.2.3" / "aws-cli/2.x ..."
62
+ const m = firstLine.match(/\b(\d+\.\d+(?:\.\d+)?(?:[-.+][0-9A-Za-z.]+)?)\b/);
63
+ return m ? m[1] : firstLine.slice(0, 80);
64
+ }
65
+ function trimDetail(s) {
66
+ const text = s.trim();
67
+ if (!text)
68
+ return undefined;
69
+ const first = text.split(/\r?\n/, 1)[0].trim();
70
+ return first.slice(0, 200);
71
+ }
72
+ // ---------------------------------------------------------------------------
73
+ // Public
74
+ // ---------------------------------------------------------------------------
75
+ export function getCachedCliStatus(id) {
76
+ const cached = cache.get(id);
77
+ if (!cached)
78
+ return undefined;
79
+ if (Date.now() - cached.checkedAt > DETECT_TTL_MS)
80
+ return undefined;
81
+ return cached;
82
+ }
83
+ export function invalidateCliStatus(id) {
84
+ if (id)
85
+ cache.delete(id);
86
+ else
87
+ cache.clear();
88
+ }
89
+ export async function detectCli(cli) {
90
+ const binaryPath = which(cli.binary);
91
+ if (!binaryPath) {
92
+ const status = {
93
+ id: cli.id, binary: cli.binary, state: 'not_installed', checkedAt: Date.now(),
94
+ };
95
+ cache.set(cli.id, status);
96
+ return status;
97
+ }
98
+ // Installed — read version, best-effort.
99
+ let version;
100
+ if (cli.versionArgv && cli.versionArgv.length) {
101
+ const v = await runArgv(cli.versionArgv, 5_000);
102
+ version = extractVersion(v.stdout, v.stderr);
103
+ }
104
+ if (cli.auth.type === 'none' || !cli.auth.statusArgv || cli.auth.statusArgv.length === 0) {
105
+ const status = {
106
+ id: cli.id, binary: cli.binary, state: 'ready', version, checkedAt: Date.now(),
107
+ };
108
+ cache.set(cli.id, status);
109
+ return status;
110
+ }
111
+ const result = await runArgv(cli.auth.statusArgv, 6_000);
112
+ // Some CLIs (gcloud) report success via exit 0 but emit empty output when no
113
+ // account is configured — fall back to a content check when declared.
114
+ const patternOk = !cli.auth.statusReadyPattern
115
+ || new RegExp(cli.auth.statusReadyPattern).test(result.stdout);
116
+ const state = (result.ok && patternOk) ? 'ready' : 'installed_not_auth';
117
+ const status = {
118
+ id: cli.id, binary: cli.binary, state, version,
119
+ authDetail: state === 'ready' ? trimDetail(result.stdout) : trimDetail(result.stderr || result.stdout),
120
+ checkedAt: Date.now(),
121
+ };
122
+ cache.set(cli.id, status);
123
+ return status;
124
+ }
125
+ /** Best-effort platform key for picking install commands. */
126
+ export function currentPlatform() {
127
+ if (process.platform === 'darwin')
128
+ return 'darwin';
129
+ if (process.platform === 'win32')
130
+ return 'win';
131
+ return 'linux';
132
+ }
133
+ /** Where AWS credentials go for the `token` auth flow. */
134
+ export function awsCredentialsPath() {
135
+ return path.join(os.homedir(), '.aws', 'credentials');
136
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Barrel entry for the CLI extension module.
3
+ */
4
+ export { getRecommendedClis, getRecommendedCli, } from './registry.js';
5
+ export { detectCli, getCachedCliStatus, invalidateCliStatus, currentPlatform, } from './detector.js';
6
+ export { getCliCatalog, refreshCliStatus, } from './catalog.js';
7
+ export { startCliAuthSession, getAuthSession, cancelAuthSession, applyCliToken, logoutCli, startCliInstallSession, resolveAutoInstallSpec, } from './auth.js';
@@ -0,0 +1,33 @@
1
+ /**
2
+ * CLI tool registry — curated command-line tools agents commonly need.
3
+ *
4
+ * Each entry declares how to install the binary on each OS, how to detect the
5
+ * install / auth state, and (optionally) how to drive the sign-in flow.
6
+ *
7
+ * Two auth types are supported today:
8
+ * - oauth-web: the CLI has a first-party `<cli> auth login --web` flavor that
9
+ * prints a device code and opens the browser. We spawn it, stream output,
10
+ * and poll the status command to know when the user finished in the browser.
11
+ * - token: the user pastes an API key; we set it via the CLI's config command
12
+ * (or via env var for CLIs that only read from env).
13
+ *
14
+ * Keep this list opinionated and small — better to nail the common case than to
15
+ * ship a wall of half-working entries.
16
+ */
17
+ // ---------------------------------------------------------------------------
18
+ // Recommended CLI tools — data lives in src/catalog/
19
+ //
20
+ // This module owns the *types* and helper functions. Edit
21
+ // `src/catalog/cli-tools.ts` to add or hide a CLI tool entry.
22
+ // ---------------------------------------------------------------------------
23
+ import { CLI_TOOLS } from '../../catalog/index.js';
24
+ const RECOMMENDED_CLIS = CLI_TOOLS;
25
+ // ---------------------------------------------------------------------------
26
+ // Public API
27
+ // ---------------------------------------------------------------------------
28
+ export function getRecommendedClis() {
29
+ return RECOMMENDED_CLIS;
30
+ }
31
+ export function getRecommendedCli(id) {
32
+ return RECOMMENDED_CLIS.find(c => c.id === id);
33
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * agent-driver.ts — Agent driver interface and registry.
3
+ *
4
+ * Each CLI agent (claude, codex, gemini, ...) implements AgentDriver.
5
+ * Register with `registerDriver()`, look up with `getDriver()`.
6
+ */
7
+ // ---------------------------------------------------------------------------
8
+ // Registry
9
+ // ---------------------------------------------------------------------------
10
+ const drivers = new Map();
11
+ export function registerDriver(d) { drivers.set(d.id, d); }
12
+ export function getDriver(id) {
13
+ const d = drivers.get(id);
14
+ if (!d)
15
+ throw new Error(`Unknown agent: ${id}. Available: ${[...drivers.keys()].join(', ')}`);
16
+ return d;
17
+ }
18
+ export function hasDriver(id) { return drivers.has(id); }
19
+ export function allDrivers() { return [...drivers.values()]; }
20
+ export function allDriverIds() { return [...drivers.keys()]; }
21
+ export function shutdownAllDrivers() {
22
+ for (const d of drivers.values())
23
+ d.shutdown();
24
+ }
25
+ const DEFAULT_CAPABILITIES = { fork: false, modelSwitch: true, workflow: false };
26
+ export function getDriverCapabilities(id) {
27
+ const d = drivers.get(id);
28
+ if (!d?.capabilities)
29
+ return DEFAULT_CAPABILITIES;
30
+ return { ...DEFAULT_CAPABILITIES, ...d.capabilities };
31
+ }
32
+ /**
33
+ * Provider kinds this driver can route through. Empty array means the driver
34
+ * declared no compatibility (so no Profiles will be listed for it). Callers
35
+ * should treat this as the filter for cross-provider model offerings.
36
+ */
37
+ export function getAcceptedProviderKinds(id) {
38
+ return drivers.get(id)?.acceptedProviderKinds ?? [];
39
+ }