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
|
@@ -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;
|