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,16 +1,3 @@
1
- /**
2
- * Dashboard API routes for extension management — MCP servers and skills.
3
- *
4
- * Catalog-first design: GET /catalog returns a unified list of recommended
5
- * registry entries merged with the user's installed servers, each tagged with
6
- * a single `state` field. The frontend uses that state to render the right CTA
7
- * (Install / Authorize / Enable / Disable / Remove).
8
- *
9
- * OAuth endpoints:
10
- * POST /oauth/start — kicks off auth-code flow, returns auth URL + state
11
- * GET /oauth/callback — provider redirects here; exchanges code for tokens
12
- * POST /oauth/revoke — clears stored tokens for a server
13
- */
14
1
  import { Hono } from 'hono';
15
2
  import { execFile } from 'node:child_process';
16
3
  import { addGlobalMcpExtension, removeGlobalMcpExtension, updateGlobalMcpExtension, addWorkspaceMcpExtension, removeWorkspaceMcpExtension, updateWorkspaceMcpExtension, getCatalogItems, buildInstalledConfigFromRecommended, checkMcpHealth, getCachedHealth, cacheHealth, getRecommendedMcpServer, listSkills, installSkill, removeSkill, recordSkillInstall, getSkillLedgerEntry, getRecommendedSkillRepos, searchSkillRepos, searchMcpServers, startAuthorization, completeAuthorization, deleteMcpToken, getMcpToken, } from '../../agent/index.js';
@@ -19,11 +6,6 @@ import { ensurePeekabooWarm } from '../../agent/mcp/bridge.js';
19
6
  import { runtime } from '../runtime.js';
20
7
  import path from 'node:path';
21
8
  import fs from 'node:fs';
22
- /**
23
- * Builtin catalog entries don't live in `extensions.mcp` — they're toggled by
24
- * a top-level config flag. Each catalogId maps to one flag; add a branch when
25
- * registering a new builtin.
26
- */
27
9
  function setBuiltinEnabled(catalogId, enabled) {
28
10
  if (catalogId === 'pikiloom-browser') {
29
11
  saveUserConfig({ ...loadUserConfig(), browserEnabled: enabled });
@@ -31,9 +13,6 @@ function setBuiltinEnabled(catalogId, enabled) {
31
13
  }
32
14
  if (catalogId === 'peekaboo') {
33
15
  saveUserConfig({ ...loadUserConfig(), peekabooEnabled: enabled });
34
- // Warm the npx package the moment Peekaboo is enabled so it's downloaded
35
- // before the first session — otherwise the agent hits a cold cache and
36
- // hangs at "Still connecting". Disabling is a no-op.
37
16
  if (enabled)
38
17
  ensurePeekabooWarm();
39
18
  return true;
@@ -54,15 +33,9 @@ function isValidWorkdir(dir) {
54
33
  }
55
34
  }
56
35
  function getCallbackRedirectUri(c) {
57
- // Build from the current request so the URL matches whatever port/origin
58
- // the dashboard is actually served on (dev, prod, custom port, etc.).
59
36
  const origin = new URL(c.req.url).origin;
60
37
  return `${origin}/api/extensions/mcp/oauth/callback`;
61
38
  }
62
- // ---------------------------------------------------------------------------
63
- // MCP — unified catalog
64
- // ---------------------------------------------------------------------------
65
- /** GET /api/extensions/mcp/catalog — Unified recommended + installed list with state. */
66
39
  app.get('/api/extensions/mcp/catalog', (c) => {
67
40
  const workdir = c.req.query('workdir') || runtime.getRequestWorkdir();
68
41
  const scopeParam = c.req.query('scope');
@@ -72,13 +45,6 @@ app.get('/api/extensions/mcp/catalog', (c) => {
72
45
  const items = getCatalogItems({ workdir, scope });
73
46
  return c.json({ ok: true, items });
74
47
  });
75
- /**
76
- * POST /api/extensions/mcp/install
77
- * Install a recommended registry entry. Body:
78
- * { catalogId, scope, workdir?, credentials?, enable? }
79
- * Missing credentials for mcp-oauth servers is fine — they'll surface as
80
- * `needs_auth` in the catalog, and the UI should call /oauth/start next.
81
- */
82
48
  app.post('/api/extensions/mcp/install', async (c) => {
83
49
  try {
84
50
  const body = await c.req.json();
@@ -92,7 +58,6 @@ app.post('/api/extensions/mcp/install', async (c) => {
92
58
  const ok = setBuiltinEnabled(rec.id, enable !== false);
93
59
  return c.json({ ok, enabled: ok && enable !== false });
94
60
  }
95
- // Don't enable yet for mcp-oauth if no token exists — user still needs to authorize.
96
61
  let shouldEnable = enable;
97
62
  if (rec.auth.type === 'mcp-oauth' && !getMcpToken(rec.id))
98
63
  shouldEnable = false;
@@ -120,10 +85,6 @@ app.post('/api/extensions/mcp/install', async (c) => {
120
85
  return c.json({ ok: false, error: e?.message || 'internal error' }, 500);
121
86
  }
122
87
  });
123
- /**
124
- * POST /api/extensions/mcp/toggle
125
- * Enable/disable an installed server by its installed key.
126
- */
127
88
  app.post('/api/extensions/mcp/toggle', async (c) => {
128
89
  try {
129
90
  const body = await c.req.json();
@@ -150,7 +111,6 @@ app.post('/api/extensions/mcp/toggle', async (c) => {
150
111
  return c.json({ ok: false, error: e?.message || 'internal error' }, 500);
151
112
  }
152
113
  });
153
- /** POST /api/extensions/mcp/update — patch config fields (credentials, url, etc.). */
154
114
  app.post('/api/extensions/mcp/update', async (c) => {
155
115
  try {
156
116
  const body = await c.req.json();
@@ -173,7 +133,6 @@ app.post('/api/extensions/mcp/update', async (c) => {
173
133
  return c.json({ ok: false, error: e?.message || 'internal error' }, 500);
174
134
  }
175
135
  });
176
- /** POST /api/extensions/mcp/remove — uninstall. Also clears any OAuth tokens. */
177
136
  app.post('/api/extensions/mcp/remove', async (c) => {
178
137
  try {
179
138
  const body = await c.req.json();
@@ -201,7 +160,6 @@ app.post('/api/extensions/mcp/remove', async (c) => {
201
160
  return c.json({ ok: false, error: e?.message || 'internal error' }, 500);
202
161
  }
203
162
  });
204
- /** POST /api/extensions/mcp/custom — add a user-defined server not in the registry. */
205
163
  app.post('/api/extensions/mcp/custom', async (c) => {
206
164
  try {
207
165
  const body = await c.req.json();
@@ -229,7 +187,6 @@ app.post('/api/extensions/mcp/custom', async (c) => {
229
187
  return c.json({ ok: false, error: e?.message || 'internal error' }, 500);
230
188
  }
231
189
  });
232
- /** POST /api/extensions/mcp/health — health check with 10-min cache per catalogId. */
233
190
  app.post('/api/extensions/mcp/health', async (c) => {
234
191
  try {
235
192
  const body = await c.req.json();
@@ -251,7 +208,6 @@ app.post('/api/extensions/mcp/health', async (c) => {
251
208
  return c.json({ ok: false, error: e?.message || 'internal error' }, 500);
252
209
  }
253
210
  });
254
- /** GET /api/extensions/mcp/search — search community MCP servers (fallback path). */
255
211
  app.get('/api/extensions/mcp/search', async (c) => {
256
212
  const query = c.req.query('q') || '';
257
213
  const parsed = parseInt(c.req.query('limit') || '20', 10);
@@ -264,10 +220,6 @@ app.get('/api/extensions/mcp/search', async (c) => {
264
220
  return c.json({ ok: false, error: e?.message, results: [] });
265
221
  }
266
222
  });
267
- // ---------------------------------------------------------------------------
268
- // MCP — OAuth
269
- // ---------------------------------------------------------------------------
270
- /** POST /api/extensions/mcp/oauth/start — returns authUrl the client should open. */
271
223
  app.post('/api/extensions/mcp/oauth/start', async (c) => {
272
224
  try {
273
225
  const body = await c.req.json();
@@ -297,7 +249,6 @@ app.post('/api/extensions/mcp/oauth/start', async (c) => {
297
249
  return c.json({ ok: false, error: e?.message || 'oauth start failed' }, 500);
298
250
  }
299
251
  });
300
- /** GET /api/extensions/mcp/oauth/callback — browser landing page for the provider redirect. */
301
252
  app.get('/api/extensions/mcp/oauth/callback', async (c) => {
302
253
  const code = c.req.query('code') || '';
303
254
  const state = c.req.query('state') || '';
@@ -364,7 +315,6 @@ app.get('/api/extensions/mcp/oauth/callback', async (c) => {
364
315
  });
365
316
  }
366
317
  });
367
- /** POST /api/extensions/mcp/oauth/revoke — clear stored tokens. */
368
318
  app.post('/api/extensions/mcp/oauth/revoke', async (c) => {
369
319
  try {
370
320
  const body = await c.req.json();
@@ -378,12 +328,6 @@ app.post('/api/extensions/mcp/oauth/revoke', async (c) => {
378
328
  return c.json({ ok: false, error: e?.message || 'internal error' }, 500);
379
329
  }
380
330
  });
381
- /**
382
- * Extract the GitHub owner slug from a skill `source`. Accepts both the
383
- * compact `owner/repo` form and full `https://github.com/owner/repo[...]`
384
- * URLs. Returns null when the source doesn't look like a GitHub reference,
385
- * in which case the dashboard falls back to a letter avatar.
386
- */
387
331
  function extractGithubOwner(source) {
388
332
  if (!source)
389
333
  return null;
@@ -391,7 +335,6 @@ function extractGithubOwner(source) {
391
335
  const owner = cleaned.split('/')[0]?.trim();
392
336
  if (!owner)
393
337
  return null;
394
- // GitHub usernames: alphanumerics + single hyphens, 1–39 chars.
395
338
  return /^[a-z0-9](?:[a-z0-9-]{0,38})$/i.test(owner) ? owner : null;
396
339
  }
397
340
  const githubMetaCache = new Map();
@@ -400,18 +343,6 @@ let githubMetaInflight = null;
400
343
  const remoteSkillsCache = new Map();
401
344
  const REMOTE_SKILLS_TTL_MS = 24 * 60 * 60 * 1000;
402
345
  const remoteSkillsInflight = new Map();
403
- /**
404
- * Resolve a GitHub API token without making the user export anything. Order:
405
- * 1. `GITHUB_TOKEN` env var (explicit override wins).
406
- * 2. `gh auth token` — pikiloom recommends `gh` in its CLI catalog, so any
407
- * user who's set up the CLI extensions has a token already. Cached for
408
- * 10 minutes since tokens rarely rotate; on rotation the next refresh
409
- * picks it up automatically.
410
- *
411
- * Returns null when neither path works (gh missing or not signed in). In that
412
- * case GitHub Contents calls fall back to the 60 req/h unauth limit, which is
413
- * still enough for most casual users.
414
- */
415
346
  let githubTokenCache = null;
416
347
  let githubTokenInflight = null;
417
348
  const GITHUB_TOKEN_TTL_MS = 10 * 60 * 1000;
@@ -421,11 +352,6 @@ async function resolveGithubToken() {
421
352
  if (githubTokenCache && Date.now() - githubTokenCache.resolvedAt < GITHUB_TOKEN_TTL_MS) {
422
353
  return githubTokenCache.value;
423
354
  }
424
- // Singleflight: if a probe is already in flight, await its result instead of
425
- // spawning N parallel `gh` subprocesses. The 14-source catalog warm-up used
426
- // to race here and one keyring lock-contention failure would poison the
427
- // cache for the next 10 minutes. We also only cache on success — a null
428
- // result just falls through to the next caller's probe.
429
355
  if (githubTokenInflight)
430
356
  return githubTokenInflight;
431
357
  githubTokenInflight = (async () => {
@@ -489,15 +415,6 @@ async function fetchGithubContents(owner, repo, path) {
489
415
  return null;
490
416
  }
491
417
  }
492
- /**
493
- * Discover skills inside a remote repo. Tries the two common layouts:
494
- * 1. `<repo>/skills/<name>/SKILL.md` (Anthropic-style)
495
- * 2. `<repo>/<name>/SKILL.md` (flat layout)
496
- *
497
- * Returns a deduplicated list of subdirectory names that look like skills.
498
- * Best-effort — a missing SKILL.md inside a subdir just gets included if the
499
- * subdir name matches sensible conventions, since some repos lazy-load.
500
- */
501
418
  async function listRemoteSkillsFromGithub(source) {
502
419
  const cached = remoteSkillsCache.get(source);
503
420
  if (cached && Date.now() - cached.cachedAt < REMOTE_SKILLS_TTL_MS)
@@ -510,7 +427,6 @@ async function listRemoteSkillsFromGithub(source) {
510
427
  if (!parsed)
511
428
  return null;
512
429
  const { owner, repo } = parsed;
513
- // Look for skills/ subdir first; fall back to root if absent.
514
430
  let listing = await fetchGithubContents(owner, repo, 'skills');
515
431
  let basePath = 'skills';
516
432
  if (!listing || listing.length === 0) {
@@ -520,15 +436,10 @@ async function listRemoteSkillsFromGithub(source) {
520
436
  if (!listing)
521
437
  return null;
522
438
  const directories = listing.filter(e => e.type === 'dir' && !e.name.startsWith('.'));
523
- // To keep one repo from costing 100+ subsequent fetches (verifying SKILL.md
524
- // inside each subdir), we skip the per-skill verification and instead
525
- // accept any directory under the resolved base as a candidate skill. This
526
- // matches how `npx skills add` itself enumerates targets.
527
439
  const skills = directories.map(d => ({
528
440
  name: d.name,
529
441
  path: d.path,
530
442
  }));
531
- // Some repos return >1000 entries; GitHub's contents endpoint caps there.
532
443
  const partial = directories.length >= 1000;
533
444
  const result = { skills, partial };
534
445
  remoteSkillsCache.set(source, { value: result, cachedAt: Date.now() });
@@ -537,7 +448,6 @@ async function listRemoteSkillsFromGithub(source) {
537
448
  remoteSkillsInflight.set(source, promise);
538
449
  return promise;
539
450
  }
540
- /** GET /api/extensions/skills/list?source=owner/repo — list a repo's available skills. */
541
451
  app.get('/api/extensions/skills/list', async (c) => {
542
452
  const source = c.req.query('source')?.trim();
543
453
  if (!source)
@@ -554,7 +464,6 @@ app.get('/api/extensions/skills/list', async (c) => {
554
464
  return c.json({ ok: true, skills: result.skills, partial: result.partial });
555
465
  });
556
466
  async function fetchOneRepoMeta(source) {
557
- // Accept either `owner/repo` or a full GitHub URL.
558
467
  const slug = source.replace(/^https?:\/\/github\.com\//, '').replace(/\.git$/, '');
559
468
  if (!/^[^/]+\/[^/]+$/.test(slug))
560
469
  return null;
@@ -582,13 +491,6 @@ async function fetchOneRepoMeta(source) {
582
491
  return null;
583
492
  }
584
493
  }
585
- /**
586
- * Default-branch HEAD commit SHA per source — the signal we diff installed
587
- * skills against. Cached with a much shorter TTL than the star/pushedAt meta
588
- * (10 min vs 24h) so "update available" stays close to real-time without
589
- * hammering the API; we only fetch it for collections that are actually
590
- * installed. Cache only on success so a transient null never sticks.
591
- */
592
494
  const repoHeadShaCache = new Map();
593
495
  const REPO_HEAD_SHA_TTL_MS = 10 * 60 * 1000;
594
496
  const repoHeadShaInflight = new Map();
@@ -608,8 +510,6 @@ async function fetchRepoHeadSha(source) {
608
510
  const controller = new AbortController();
609
511
  const timer = setTimeout(() => controller.abort(), 5_000);
610
512
  const token = await resolveGithubToken();
611
- // `commits?per_page=1` returns the latest commit on the default branch
612
- // without needing to know the branch name first.
613
513
  const res = await fetch(`https://api.github.com/repos/${slug}/commits?per_page=1`, {
614
514
  signal: controller.signal,
615
515
  headers: {
@@ -661,14 +561,12 @@ async function ensureRepoMeta(sources) {
661
561
  githubMetaInflight = null;
662
562
  }
663
563
  }
664
- /** GET /api/extensions/skills/catalog — unified recommended + installed skills view. */
665
564
  app.get('/api/extensions/skills/catalog', async (c) => {
666
565
  const workdir = c.req.query('workdir') || runtime.getRequestWorkdir();
667
566
  const scopeParam = c.req.query('scope');
668
567
  const scope = scopeParam === 'global' || scopeParam === 'workspace' || scopeParam === 'both'
669
568
  ? scopeParam
670
569
  : undefined;
671
- // Workspace view requires a workdir; global view can use the global skills dir without one.
672
570
  if (scope === 'workspace' && !workdir) {
673
571
  return c.json({ ok: false, error: 'workdir is required', items: [], installed: [] }, 400);
674
572
  }
@@ -680,27 +578,18 @@ app.get('/api/extensions/skills/catalog', async (c) => {
680
578
  return true;
681
579
  return repo.recommendedScope === scope || repo.recommendedScope === 'both';
682
580
  });
683
- // Best-effort GitHub metadata. We don't await on a cold cache here so the
684
- // first paint isn't blocked by GitHub latency — if results are still cold,
685
- // they'll appear on the next refresh (the dashboard already does SWR).
686
581
  const sources = filtered.map(r => r.source);
687
582
  const allCached = sources.every(s => githubMetaCache.has(s));
688
583
  if (allCached) {
689
- // Cheap path: nothing to fetch, just return.
690
584
  }
691
585
  else {
692
586
  await ensureRepoMeta(sources).catch(() => { });
693
587
  }
694
- // Fire-and-forget warm-up for the remote-skills listing on any source we
695
- // haven't seen yet. On the first ever catalog load this won't return totals,
696
- // but the SWR refresh on the dashboard picks them up moments later.
697
588
  for (const s of sources) {
698
589
  if (!remoteSkillsCache.has(s) && !remoteSkillsInflight.has(s)) {
699
590
  void listRemoteSkillsFromGithub(s).catch(() => { });
700
591
  }
701
592
  }
702
- // Build a lookup keyed by installed-skill name (lowercased) so we can quickly
703
- // mark a repo's remote skills as installed. Scope-filter once up front.
704
593
  const scopedInstalled = scope === 'global'
705
594
  ? installed.filter(s => s.scope === 'global')
706
595
  : scope === 'workspace'
@@ -709,9 +598,6 @@ app.get('/api/extensions/skills/catalog', async (c) => {
709
598
  const installedByName = new Map();
710
599
  for (const s of scopedInstalled)
711
600
  installedByName.set(s.name.toLowerCase(), s);
712
- // Resolve each repo's installed sub-skills first (sync). Prefer the remote
713
- // listing intersection over the static `repo.skills` hint — most catalog
714
- // entries don't fill the hint, and the GitHub listing is authoritative.
715
601
  const computeInstalledNames = (repo) => {
716
602
  const remote = remoteSkillsCache.get(repo.source)?.value;
717
603
  if (remote) {
@@ -725,12 +611,6 @@ app.get('/api/extensions/skills/catalog', async (c) => {
725
611
  .map(s => s.name);
726
612
  };
727
613
  const perRepo = filtered.map(repo => ({ repo, installedNames: computeInstalledNames(repo) }));
728
- // Update detection — only for installed collections (it's the only place the
729
- // signal is meaningful, and it keeps API calls proportional to what's
730
- // actually installed). Diff the live remote HEAD against the provenance
731
- // ledger. For installs predating the ledger (e.g. via skills.sh) we baseline
732
- // the current HEAD so future commits surface as updates, rather than
733
- // fabricating one we can't prove.
734
614
  const ledgerScope = scope === 'workspace' ? { workdir } : { global: true };
735
615
  const updateBySource = new Map();
736
616
  await Promise.all(perRepo.map(async ({ repo, installedNames }) => {
@@ -782,8 +662,6 @@ app.get('/api/extensions/skills/catalog', async (c) => {
782
662
  latestSha: upd?.latestSha,
783
663
  };
784
664
  });
785
- // Pinned first-party packs lead; then community popularity (stars desc, with
786
- // no-data entries sinking to the bottom so the most-loved repos surface first).
787
665
  items.sort((a, b) => {
788
666
  if (!!a.pinned !== !!b.pinned)
789
667
  return a.pinned ? -1 : 1;
@@ -791,7 +669,6 @@ app.get('/api/extensions/skills/catalog', async (c) => {
791
669
  });
792
670
  return c.json({ ok: true, items, installed });
793
671
  });
794
- /** POST /api/extensions/skills/install — install a skill via npx skills add. */
795
672
  app.post('/api/extensions/skills/install', async (c) => {
796
673
  try {
797
674
  const body = await c.req.json();
@@ -802,8 +679,6 @@ app.post('/api/extensions/skills/install', async (c) => {
802
679
  if (!isGlobal && !isValidWorkdir(workdir)) {
803
680
  return c.json({ ok: false, error: 'valid workdir is required for project-scoped install' }, 400);
804
681
  }
805
- // Resolve the remote HEAD so installSkill can record provenance for update
806
- // detection. Best-effort — a failed lookup just installs without a baseline.
807
682
  const sourceSha = await fetchRepoHeadSha(source.trim()).catch(() => null);
808
683
  const result = await installSkill(source.trim(), { global: isGlobal, skill, workdir, sourceSha });
809
684
  return c.json(result);
@@ -812,11 +687,6 @@ app.post('/api/extensions/skills/install', async (c) => {
812
687
  return c.json({ ok: false, error: e?.message || 'installation failed' }, 500);
813
688
  }
814
689
  });
815
- /**
816
- * POST /api/extensions/skills/update — pull the latest version of an installed
817
- * collection. Re-runs `skills add <source>` (no `-s`, so every skill from the
818
- * source is refreshed) and advances the provenance ledger to the new HEAD.
819
- */
820
690
  app.post('/api/extensions/skills/update', async (c) => {
821
691
  try {
822
692
  const body = await c.req.json();
@@ -835,7 +705,6 @@ app.post('/api/extensions/skills/update', async (c) => {
835
705
  return c.json({ ok: false, error: e?.message || 'update failed' }, 500);
836
706
  }
837
707
  });
838
- /** POST /api/extensions/skills/remove — remove an installed skill. */
839
708
  app.post('/api/extensions/skills/remove', async (c) => {
840
709
  try {
841
710
  const body = await c.req.json();
@@ -850,7 +719,6 @@ app.post('/api/extensions/skills/remove', async (c) => {
850
719
  return c.json({ ok: false, error: e?.message || 'removal failed' }, 500);
851
720
  }
852
721
  });
853
- /** GET /api/extensions/skills/search — search community skills. */
854
722
  app.get('/api/extensions/skills/search', async (c) => {
855
723
  const query = c.req.query('q') || '';
856
724
  try {
@@ -1,29 +1,3 @@
1
- /**
2
- * Dashboard API: Local Model backends (Ollama / mlx-lm).
3
- *
4
- * Surfaces a probe endpoint plus a connect action that links a detected
5
- * backend into the Provider/Profile model layer so the agent cards above
6
- * pick it up without any further configuration.
7
- *
8
- * GET /api/local-models/probe → which backends are running, what models
9
- * they expose, install/run hints, and
10
- * whether a Provider already points at each.
11
- * POST /api/local-models/connect → idempotently create the Provider for the
12
- * named backend.
13
- *
14
- * Endpoints we expect:
15
- * - Ollama baseURL → http://127.0.0.1:11434
16
- * version → GET /api/version
17
- * models → GET /api/tags
18
- * OpenAI → /v1/chat/completions, /v1/models
19
- *
20
- * - mlx-lm baseURL → http://127.0.0.1:8080 (mlx_lm.server default)
21
- * probe → GET /v1/models (200 OK iff server up; no version)
22
- *
23
- * Model downloads stay manual (user runs `ollama pull <tag>` or restarts
24
- * `mlx_lm.server --model <repo>` in their own terminal). The install spec
25
- * is shipped alongside detection so the UI can mirror the CLI tools page.
26
- */
27
1
  import { Hono } from 'hono';
28
2
  import { LOCAL_MODELS } from '../../catalog/local-models.js';
29
3
  import { listProviders, addProvider, listProfiles, addProfile, } from '../../model/index.js';
@@ -59,7 +33,6 @@ const BACKENDS = [
59
33
  label: 'mlx-lm',
60
34
  baseURL: 'http://127.0.0.1:8080',
61
35
  openAIBaseURL: 'http://127.0.0.1:8080/v1',
62
- // mlx_lm.server has no /api/version; /v1/models doubles as liveness probe.
63
36
  probePath: '/v1/models',
64
37
  homepage: 'https://github.com/ml-explore/mlx-lm',
65
38
  install: {
@@ -73,17 +46,12 @@ const BACKENDS = [
73
46
  label: 'Start the server (replace model)',
74
47
  cmd: 'mlx_lm.server --model mlx-community/Qwen2.5-Coder-7B-Instruct-4bit --port 8080',
75
48
  },
76
- // mlx-lm loads a single model per server instance — "pull" here means
77
- // re-launching the server with that model id.
78
49
  pullCommandTemplate: 'mlx_lm.server --model ${model} --port 8080',
79
50
  modelField: 'mlxModel',
80
51
  platforms: ['darwin'],
81
52
  },
82
53
  ];
83
54
  const PROBE_TIMEOUT_MS = 1500;
84
- // ---------------------------------------------------------------------------
85
- // HTTP helpers
86
- // ---------------------------------------------------------------------------
87
55
  async function fetchJson(url, timeoutMs = PROBE_TIMEOUT_MS) {
88
56
  const controller = new AbortController();
89
57
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -127,10 +95,6 @@ async function probeMlx(spec) {
127
95
  models: (res.data || []).map(m => ({ id: m.id })),
128
96
  };
129
97
  }
130
- /**
131
- * Normalize a provider baseURL for comparison: drop trailing slashes and
132
- * collapse the localhost ↔ 127.0.0.1 distinction.
133
- */
134
98
  function normalizeBaseURL(raw) {
135
99
  return raw
136
100
  .replace(/\/+$/, '')
@@ -164,9 +128,6 @@ async function probeBackend(spec, providers) {
164
128
  supportedOnThisOs: supported,
165
129
  };
166
130
  }
167
- // ---------------------------------------------------------------------------
168
- // Catalog join — recommended models × backend availability
169
- // ---------------------------------------------------------------------------
170
131
  function isEntryInstalled(entry, spec, installed) {
171
132
  const target = entry[spec.modelField];
172
133
  if (!target)
@@ -193,12 +154,6 @@ function joinCatalog(backends) {
193
154
  return { ...entry, installed: null };
194
155
  });
195
156
  }
196
- /**
197
- * For a connected local backend, mirror every detected model as a Profile under
198
- * its Provider so the unified picker shows them without an extra user gesture.
199
- * Idempotent. Never deletes Profiles — a model that disappears from probe
200
- * output might be a transient blip.
201
- */
202
157
  function syncLocalProfilesForBackend(providerId, detected) {
203
158
  if (!providerId || !detected.length)
204
159
  return { added: 0 };
@@ -213,17 +168,10 @@ function syncLocalProfilesForBackend(providerId, detected) {
213
168
  existing.add(m.id);
214
169
  }
215
170
  catch {
216
- // Provider may have been removed between calls — skip; next probe retries.
217
171
  }
218
172
  }
219
173
  return { added };
220
174
  }
221
- /**
222
- * Idempotently create a Provider pointing at this backend. Returns the
223
- * provider id. The placeholder API key is a sentinel ("local-no-auth") rather
224
- * than something that looks like a real key, so future code can recognize and
225
- * special-case local providers.
226
- */
227
175
  async function ensureProviderForBackend(spec) {
228
176
  const providers = listProviders();
229
177
  const existing = findProviderForBackend(providers, spec);
@@ -242,22 +190,6 @@ async function ensureProviderForBackend(spec) {
242
190
  return null;
243
191
  }
244
192
  }
245
- // ---------------------------------------------------------------------------
246
- // Routes
247
- // ---------------------------------------------------------------------------
248
- /**
249
- * Single probe-and-attach endpoint. The Local Models page no longer asks the
250
- * user to "connect" — every detected backend becomes a Provider automatically
251
- * so its models show up in the unified picker without an extra click.
252
- *
253
- * - Backend detected, no existing Provider → create one, then sync Profiles.
254
- * - Backend detected, Provider already exists → just sync Profiles.
255
- * - Backend not detected → leave existing Provider in place (a transient
256
- * blip during a restart shouldn't tear down config).
257
- *
258
- * Response includes `addedProviderIds` so the dashboard can refetch the upper
259
- * Model Providers / agent layer exactly when something new appears.
260
- */
261
193
  router.get('/api/local-models/probe', async (c) => {
262
194
  try {
263
195
  const initialProviders = listProviders();
@@ -1,30 +1,8 @@
1
- /**
2
- * Dashboard API: Model layer (Providers, Profiles, BYOK bindings).
3
- *
4
- * Endpoints:
5
- * GET /api/models/catalog → models.dev metadata
6
- * GET /api/models/providers → list configured providers
7
- * POST /api/models/providers → add new provider (paste key / use env)
8
- * PATCH /api/models/providers/:id → update provider
9
- * DELETE /api/models/providers/:id → remove provider + bound profiles
10
- * POST /api/models/providers/:id/validate → Feishu-style validation
11
- *
12
- * GET /api/models/profiles → list profiles
13
- * POST /api/models/profiles → add profile
14
- * PATCH /api/models/profiles/:id → update profile
15
- * DELETE /api/models/profiles/:id → remove profile
16
- *
17
- * GET /api/models/agents → list active bindings
18
- * POST /api/models/agents/:agent/active → bind/unbind a Profile
19
- */
20
1
  import { Hono } from 'hono';
21
2
  import { getModelsDevCatalog, searchCatalogProviders, listProviders, getProvider, addProvider, updateProvider, removeProvider, setProviderValidation, listProfiles, getProfile, addProfile, updateProfile, removeProfile, getActiveProfileId, setActiveProfile, prewarmLocalModel, validateProvider, getProviderModelList, invalidateProviderModels, } from '../../model/index.js';
22
3
  import { isCredentialRef, describeCredentialRef } from '../../core/secrets/index.js';
23
4
  import { allDriverIds } from '../../agent/index.js';
24
5
  const router = new Hono();
25
- // ---------------------------------------------------------------------------
26
- // Helpers
27
- // ---------------------------------------------------------------------------
28
6
  function publicProvider(p) {
29
7
  return {
30
8
  id: p.id,
@@ -39,9 +17,6 @@ function publicProvider(p) {
39
17
  };
40
18
  }
41
19
  const VALID_KINDS = ['anthropic', 'openai', 'openai-compatible', 'google'];
42
- // ---------------------------------------------------------------------------
43
- // Catalog (read-only models.dev mirror)
44
- // ---------------------------------------------------------------------------
45
20
  router.get('/api/models/catalog', async (c) => {
46
21
  const q = c.req.query('q') || '';
47
22
  const refresh = c.req.query('refresh') === '1';
@@ -98,9 +73,6 @@ router.get('/api/models/catalog/:providerId', async (c) => {
98
73
  return c.json({ ok: false, error: e?.message || String(e) }, 500);
99
74
  }
100
75
  });
101
- // ---------------------------------------------------------------------------
102
- // Providers
103
- // ---------------------------------------------------------------------------
104
76
  router.get('/api/models/providers', c => {
105
77
  return c.json({ ok: true, providers: listProviders().map(publicProvider) });
106
78
  });
@@ -168,16 +140,6 @@ router.delete('/api/models/providers/:id', async (c) => {
168
140
  invalidateProviderModels(id);
169
141
  return c.json({ ok: true });
170
142
  });
171
- /**
172
- * GET /api/models/providers/:id/models — model id list for the configured
173
- * provider, served from a 30-minute in-memory TTL cache. The first hit (or any
174
- * hit after a config edit) re-fetches from the provider's /models endpoint.
175
- *
176
- * Used by:
177
- * - Dashboard: per-agent unified config modal model picker
178
- * - IM /models command: when an agent is bound to this provider
179
- * - Agent status response: when an agent is bound to this provider
180
- */
181
143
  router.get('/api/models/providers/:id/models', async (c) => {
182
144
  const id = c.req.param('id');
183
145
  const refresh = c.req.query('refresh') === '1';
@@ -217,9 +179,6 @@ router.post('/api/models/providers/:id/validate', async (c) => {
217
179
  return c.json({ ok: true, validation: status, models: [] });
218
180
  }
219
181
  });
220
- // ---------------------------------------------------------------------------
221
- // Profiles
222
- // ---------------------------------------------------------------------------
223
182
  router.get('/api/models/profiles', c => {
224
183
  return c.json({ ok: true, profiles: listProfiles() });
225
184
  });
@@ -286,9 +245,6 @@ router.delete('/api/models/profiles/:id', c => {
286
245
  return c.json({ ok: false, error: 'Profile not found' }, 404);
287
246
  return c.json({ ok: true });
288
247
  });
289
- // ---------------------------------------------------------------------------
290
- // Agent bindings
291
- // ---------------------------------------------------------------------------
292
248
  router.get('/api/models/agents', c => {
293
249
  const agents = allDriverIds();
294
250
  return c.json({
@@ -315,8 +271,6 @@ router.post('/api/models/agents/:agent/active', async (c) => {
315
271
  return c.json({ ok: false, error: 'profileId (string|null) is required' }, 400);
316
272
  try {
317
273
  setActiveProfile(agent, profileId);
318
- // Warm a local backend the instant it's selected, so the user's first turn
319
- // skips the model cold-load. Fire-and-forget; never blocks the bind.
320
274
  if (profileId) {
321
275
  const profile = getProfile(profileId);
322
276
  const provider = profile ? getProvider(profile.providerId) : null;