@polderlabs/bizar 4.4.13 → 4.5.1
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/bizar-dash/CHANGELOG.md +37 -276
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +2 -0
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
- package/bizar-dash/dist/assets/main-eWZ4NlCL.css +1 -0
- package/bizar-dash/dist/assets/main-usWhlPWa.js +362 -0
- package/bizar-dash/dist/assets/main-usWhlPWa.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile-DQLFCjwJ.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile-DQLFCjwJ.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.js +352 -0
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.js.map +1 -0
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
- package/bizar-dash/skills/bizar/SKILL.md +116 -0
- package/bizar-dash/skills/chat/SKILL.md +74 -0
- package/bizar-dash/skills/headroom/SKILL.md +94 -0
- package/bizar-dash/skills/lightrag/SKILL.md +86 -0
- package/bizar-dash/skills/minimax/SKILL.md +80 -0
- package/bizar-dash/skills/obsidian/SKILL.md +68 -0
- package/bizar-dash/skills/providers/SKILL.md +75 -0
- package/bizar-dash/skills/sdk/SKILL.md +138 -0
- package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
- package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
- package/bizar-dash/skills/usage/SKILL.md +62 -0
- package/bizar-dash/src/server/api.mjs +18 -0
- package/bizar-dash/src/server/headroom.mjs +645 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +272 -2
- package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
- package/bizar-dash/src/server/memory-store.mjs +189 -0
- package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
- package/bizar-dash/src/server/minimax.mjs +196 -5
- package/bizar-dash/src/server/providers-store.mjs +956 -0
- package/bizar-dash/src/server/routes/_shared.mjs +17 -0
- package/bizar-dash/src/server/routes/config.mjs +52 -1
- package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
- package/bizar-dash/src/server/routes/headroom.mjs +126 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
- package/bizar-dash/src/server/routes/memory.mjs +668 -1
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
- package/bizar-dash/src/server/routes/providers.mjs +266 -5
- package/bizar-dash/src/server/routes/skills.mjs +32 -43
- package/bizar-dash/src/server/routes/update.mjs +340 -0
- package/bizar-dash/src/server/routes/usage.mjs +136 -0
- package/bizar-dash/src/server/serve-info.mjs +135 -4
- package/bizar-dash/src/server/server.mjs +20 -0
- package/bizar-dash/src/server/skills-store.mjs +152 -262
- package/bizar-dash/src/web/App.tsx +120 -29
- package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
- package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
- package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
- package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
- package/bizar-dash/src/web/components/Topbar.tsx +2 -1
- package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
- package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
- package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
- package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
- package/bizar-dash/src/web/lib/api.ts +43 -0
- package/bizar-dash/src/web/lib/types.ts +16 -0
- package/bizar-dash/src/web/main.tsx +2 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
- package/bizar-dash/src/web/styles/chat.css +135 -1
- package/bizar-dash/src/web/styles/main.css +46 -0
- package/bizar-dash/src/web/styles/memory.css +955 -0
- package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
- package/bizar-dash/src/web/styles/settings.css +418 -0
- package/bizar-dash/src/web/styles/skills.css +302 -0
- package/bizar-dash/src/web/styles/tasks.css +288 -0
- package/bizar-dash/src/web/views/Chat.tsx +276 -48
- package/bizar-dash/src/web/views/Config.tsx +3 -2065
- package/bizar-dash/src/web/views/Memory.tsx +140 -0
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
- package/bizar-dash/src/web/views/Overview.tsx +3 -0
- package/bizar-dash/src/web/views/Settings.tsx +36 -0
- package/bizar-dash/src/web/views/Skills.tsx +208 -260
- package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
- package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
- package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +354 -0
- package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
- package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
- package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
- package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
- package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
- package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
- package/bizar-dash/tests/headroom-install.test.mjs +173 -0
- package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
- package/bizar-dash/tests/headroom-status.test.mjs +117 -0
- package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
- package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
- package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
- package/bizar-dash/tests/memory-tab.test.mjs +322 -0
- package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
- package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
- package/bizar-dash/tests/mod-upgrade.node.test.mjs +1 -1
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
- package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
- package/bizar-dash/tests/skills-list.test.mjs +232 -0
- package/bizar-dash/tests/skills-search.test.mjs +222 -0
- package/bizar-dash/tests/submit-feedback.test.mjs +6 -6
- package/bizar-dash/tests/tasks-create.test.mjs +187 -0
- package/bizar-dash/tests/update-check.test.mjs +127 -0
- package/bizar-dash/tests/update-run.test.mjs +266 -0
- package/cli/bin.mjs +247 -1
- package/cli/provision.mjs +118 -4
- package/config/agents/_shared/SKILLS.md +109 -0
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
- package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
- package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
|
@@ -60,12 +60,27 @@ import { createConnection, isIP } from 'node:net';
|
|
|
60
60
|
const HOME = homedir();
|
|
61
61
|
|
|
62
62
|
// Mirrors plugins/bizar/src/serve-info.ts → BG_DIRS pattern.
|
|
63
|
-
const
|
|
63
|
+
const DEFAULT_SERVE_INFO_FILES = [
|
|
64
64
|
join(HOME, '.cache', 'bizar', 'serve.json'),
|
|
65
65
|
join(HOME, '.config', 'opencode', 'serve.json'),
|
|
66
66
|
join(HOME, '.bizar', 'serve.json'),
|
|
67
67
|
];
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the serve.json candidate paths at call time so test
|
|
71
|
+
* suites can override with `BIZAR_SERVE_JSON_PATH=/tmp/...json`.
|
|
72
|
+
* Order: env override (if set and exists) → default search list.
|
|
73
|
+
*
|
|
74
|
+
* @returns {string[]}
|
|
75
|
+
*/
|
|
76
|
+
function serveInfoFiles() {
|
|
77
|
+
const override = process.env.BIZAR_SERVE_JSON_PATH;
|
|
78
|
+
if (override && typeof override === 'string' && override.length > 0) {
|
|
79
|
+
return [override];
|
|
80
|
+
}
|
|
81
|
+
return DEFAULT_SERVE_INFO_FILES;
|
|
82
|
+
}
|
|
83
|
+
|
|
69
84
|
/**
|
|
70
85
|
* @typedef {Object} ServeInfo
|
|
71
86
|
* @property {string} baseUrl e.g. "http://127.0.0.1:4097"
|
|
@@ -100,7 +115,7 @@ const SERVE_INFO_FILES = [
|
|
|
100
115
|
* @returns {ServeInfo|null}
|
|
101
116
|
*/
|
|
102
117
|
export function readServeInfo() {
|
|
103
|
-
for (const file of
|
|
118
|
+
for (const file of serveInfoFiles()) {
|
|
104
119
|
if (!existsSync(file)) continue;
|
|
105
120
|
try {
|
|
106
121
|
const raw = readFileSync(file, 'utf8');
|
|
@@ -304,7 +319,7 @@ export async function listOpencodeSessions(info, timeoutMs = 5000) {
|
|
|
304
319
|
}
|
|
305
320
|
}
|
|
306
321
|
|
|
307
|
-
export const SERVE_INFO_FILE_PATHS =
|
|
322
|
+
export const SERVE_INFO_FILE_PATHS = DEFAULT_SERVE_INFO_FILES;
|
|
308
323
|
|
|
309
324
|
// ── v3.5.4 (bug: dispatch stuck) — spawn helpers ─────────────────────────
|
|
310
325
|
//
|
|
@@ -623,7 +638,123 @@ export function normalizeOpencodeMessage(msg) {
|
|
|
623
638
|
}
|
|
624
639
|
|
|
625
640
|
/**
|
|
626
|
-
* `
|
|
641
|
+
* `DELETE /api/session/{id}?directory=...` — delete a session on the
|
|
642
|
+
* opencode serve child. v4.2.4 dashboard feature parity: the rail
|
|
643
|
+
* menu offers delete and the chat info panel can prune finished
|
|
644
|
+
* sessions from inside the dashboard.
|
|
645
|
+
*
|
|
646
|
+
* Idempotent: a 404 (already gone) is treated as a successful delete.
|
|
647
|
+
*
|
|
648
|
+
* @param {ServeInfo} info
|
|
649
|
+
* @param {string} sessionId
|
|
650
|
+
* @param {string} [directory]
|
|
651
|
+
* @param {number} [timeoutMs]
|
|
652
|
+
* @returns {Promise<{ok:true,status:number}|{ok:false,error:string,status?:number}>}
|
|
653
|
+
*/
|
|
654
|
+
export async function deleteOpencodeSession(info, sessionId, directory, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
655
|
+
if (!info) return { ok: false, error: 'serve-info not available — plugin is not running' };
|
|
656
|
+
if (!sessionId) return { ok: false, error: 'sessionId is required' };
|
|
657
|
+
const dir = directory || info.worktree || '';
|
|
658
|
+
const url = `${info.baseUrl}/api/session/${encodeURIComponent(sessionId)}?directory=${encodeURIComponent(dir)}`;
|
|
659
|
+
const ac = new AbortController();
|
|
660
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
661
|
+
try {
|
|
662
|
+
const res = await fetch(url, {
|
|
663
|
+
method: 'DELETE',
|
|
664
|
+
headers: {
|
|
665
|
+
Authorization: buildAuthHeader(info),
|
|
666
|
+
Accept: 'application/json',
|
|
667
|
+
},
|
|
668
|
+
signal: ac.signal,
|
|
669
|
+
});
|
|
670
|
+
if (res.ok || res.status === 404) {
|
|
671
|
+
return { ok: true, status: res.status };
|
|
672
|
+
}
|
|
673
|
+
let detail = '';
|
|
674
|
+
try { detail = (await res.text()).slice(0, 500); } catch { /* ignore */ }
|
|
675
|
+
return {
|
|
676
|
+
ok: false,
|
|
677
|
+
status: res.status,
|
|
678
|
+
error: `DELETE /api/session/${sessionId} failed: ${res.status} ${res.statusText}${detail ? ` — ${detail}` : ''}`,
|
|
679
|
+
};
|
|
680
|
+
} catch (err) {
|
|
681
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
682
|
+
const isAbort = err instanceof Error && err.name === 'AbortError';
|
|
683
|
+
return {
|
|
684
|
+
ok: false,
|
|
685
|
+
error: isAbort ? `deleteSession timed out after ${timeoutMs}ms` : `deleteSession network error: ${msg}`,
|
|
686
|
+
};
|
|
687
|
+
} finally {
|
|
688
|
+
clearTimeout(timer);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* `PATCH /api/session/{id}?directory=...` — update session fields
|
|
694
|
+
* (currently just `title`) on the opencode serve child. Used by the
|
|
695
|
+
* rail row-menu "Rename" affordance.
|
|
696
|
+
*
|
|
697
|
+
* @param {ServeInfo} info
|
|
698
|
+
* @param {string} sessionId
|
|
699
|
+
* @param {{ title?: string }} patch
|
|
700
|
+
* @param {string} [directory]
|
|
701
|
+
* @param {number} [timeoutMs]
|
|
702
|
+
* @returns {Promise<{ok:true,session:object}|{ok:false,error:string,status?:number}>}
|
|
703
|
+
*/
|
|
704
|
+
export async function updateOpencodeSession(info, sessionId, patch, directory, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
705
|
+
if (!info) return { ok: false, error: 'serve-info not available — plugin is not running' };
|
|
706
|
+
if (!sessionId) return { ok: false, error: 'sessionId is required' };
|
|
707
|
+
if (!patch || typeof patch !== 'object') return { ok: false, error: 'patch is required' };
|
|
708
|
+
const dir = directory || info.worktree || '';
|
|
709
|
+
const url = `${info.baseUrl}/api/session/${encodeURIComponent(sessionId)}?directory=${encodeURIComponent(dir)}`;
|
|
710
|
+
const ac = new AbortController();
|
|
711
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
712
|
+
try {
|
|
713
|
+
const body = {};
|
|
714
|
+
if (typeof patch.title === 'string') {
|
|
715
|
+
const t = patch.title.trim();
|
|
716
|
+
if (!t) return { ok: false, error: 'title cannot be empty' };
|
|
717
|
+
if (t.length > 200) return { ok: false, error: 'title too long (> 200 chars)' };
|
|
718
|
+
body.title = t;
|
|
719
|
+
} else {
|
|
720
|
+
return { ok: false, error: 'no supported fields in patch (only `title` is wired)' };
|
|
721
|
+
}
|
|
722
|
+
const res = await fetch(url, {
|
|
723
|
+
method: 'PATCH',
|
|
724
|
+
headers: {
|
|
725
|
+
Authorization: buildAuthHeader(info),
|
|
726
|
+
'Content-Type': 'application/json',
|
|
727
|
+
Accept: 'application/json',
|
|
728
|
+
},
|
|
729
|
+
body: JSON.stringify(body),
|
|
730
|
+
signal: ac.signal,
|
|
731
|
+
});
|
|
732
|
+
if (!res.ok) {
|
|
733
|
+
let detail = '';
|
|
734
|
+
try { detail = (await res.text()).slice(0, 500); } catch { /* ignore */ }
|
|
735
|
+
return {
|
|
736
|
+
ok: false,
|
|
737
|
+
status: res.status,
|
|
738
|
+
error: `PATCH /api/session/${sessionId} failed: ${res.status} ${res.statusText}${detail ? ` — ${detail}` : ''}`,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
let session = null;
|
|
742
|
+
try { session = await res.json(); } catch { /* non-JSON body is fine — we still got 2xx */ }
|
|
743
|
+
return { ok: true, session };
|
|
744
|
+
} catch (err) {
|
|
745
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
746
|
+
const isAbort = err instanceof Error && err.name === 'AbortError';
|
|
747
|
+
return {
|
|
748
|
+
ok: false,
|
|
749
|
+
error: isAbort ? `updateSession timed out after ${timeoutMs}ms` : `updateSession network error: ${msg}`,
|
|
750
|
+
};
|
|
751
|
+
} finally {
|
|
752
|
+
clearTimeout(timer);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* GET /health — used by the dispatcher startup check to verify the
|
|
627
758
|
* serve child is actually reachable before we try to enqueue work.
|
|
628
759
|
*
|
|
629
760
|
* v3.11.0 — Replaced the HTTP `GET /health` probe with a TCP-connect
|
|
@@ -302,6 +302,10 @@ export async function createServer({
|
|
|
302
302
|
}
|
|
303
303
|
currentBroadcast = localBroadcast;
|
|
304
304
|
|
|
305
|
+
// v4 — Load BIZAR_* env vars from ~/.config/bizar/env.json into process.env.
|
|
306
|
+
const { loadEnvJson } = await import('./routes/env-vars.mjs');
|
|
307
|
+
loadEnvJson();
|
|
308
|
+
|
|
305
309
|
const apiRouter = await createApiRouter({
|
|
306
310
|
state,
|
|
307
311
|
watcher,
|
|
@@ -311,6 +315,22 @@ export async function createServer({
|
|
|
311
315
|
broadcast: localBroadcast,
|
|
312
316
|
});
|
|
313
317
|
|
|
318
|
+
// v5.0.0 — Headroom startup hook. Runs after api.mjs is loaded so the
|
|
319
|
+
// headroom routes are registered. Errors are caught and logged — startup
|
|
320
|
+
// must not fail if Headroom has issues.
|
|
321
|
+
const { headroomStartupHook } = await import('./headroom.mjs');
|
|
322
|
+
const { readSettings } = await import('./routes/_shared.mjs');
|
|
323
|
+
try {
|
|
324
|
+
const settings = readSettings();
|
|
325
|
+
if (settings?.data?.headroom) {
|
|
326
|
+
headroomStartupHook(settings.data.headroom).catch((err) => {
|
|
327
|
+
console.warn('[bizar-dash] headroomStartupHook error:', err?.message || err);
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
} catch (err) {
|
|
331
|
+
console.warn('[bizar-dash] headroom startup hook skipped:', err?.message || err);
|
|
332
|
+
}
|
|
333
|
+
|
|
314
334
|
// All /api/* routes go through apiRouter (after mod routes are checked).
|
|
315
335
|
// IMPORTANT: mount v2 router FIRST so `/api/v2/*` matches before
|
|
316
336
|
// apiRouter's internal 404 catch-all (api.mjs line ~109) can swallow it.
|
|
@@ -1,300 +1,190 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
// src/server/skills-store.mjs
|
|
2
|
+
//
|
|
3
|
+
// v4.0.0 - Skills registry that scans SKILL.md files from all local
|
|
4
|
+
// sources rather than relying on the `skills` CLI npm registry.
|
|
5
|
+
//
|
|
6
|
+
// Sources (in priority order for conflicts):
|
|
7
|
+
// 1. ~/.opencode/skills/<name>/SKILL.md - user-overridable builtins
|
|
8
|
+
// 2. ~/.agents/skills/<name>/SKILL.md - user-added skills
|
|
9
|
+
// 3. bizar-dash/skills/<name>/SKILL.md - BizarHarness shipped
|
|
10
|
+
// 4. .agents/skills/<name>/SKILL.md - project-local
|
|
11
|
+
// 5. .opencode/skills/<name>/SKILL.md - project-local
|
|
12
|
+
//
|
|
13
|
+
// Each SKILL.md is parsed for YAML frontmatter (description:) and
|
|
14
|
+
// the first # H1 heading (display name fallback). No external deps.
|
|
14
15
|
import {
|
|
15
16
|
existsSync,
|
|
17
|
+
readdirSync,
|
|
16
18
|
readFileSync,
|
|
17
|
-
|
|
18
|
-
renameSync,
|
|
19
|
-
mkdirSync,
|
|
19
|
+
statSync,
|
|
20
20
|
} from 'node:fs';
|
|
21
|
-
import {
|
|
21
|
+
import { join, dirname } from 'node:path';
|
|
22
22
|
import { homedir } from 'node:os';
|
|
23
|
-
import { execFile } from 'node:child_process';
|
|
24
|
-
import { promisify } from 'node:util';
|
|
25
23
|
|
|
26
|
-
const execFileP = promisify(execFile);
|
|
27
24
|
const HOME = homedir();
|
|
28
|
-
const STATE_FILE = join(HOME, '.config', 'bizar', 'skills-state.json');
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
/** Project root is injected at construction time. */
|
|
27
|
+
let PROJECT_ROOT = process.cwd();
|
|
28
|
+
|
|
29
|
+
export function setProjectRoot(p) { PROJECT_ROOT = p; }
|
|
30
|
+
|
|
31
|
+
// -- Source directories --------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/** All SKILL.md scan roots in priority order (first wins for conflicts). */
|
|
34
|
+
function sourceRoots() {
|
|
35
|
+
return [
|
|
36
|
+
{ dir: join(HOME, '.opencode', 'skills'), source: 'user' },
|
|
37
|
+
{ dir: join(HOME, '.agents', 'skills'), source: 'user' },
|
|
38
|
+
{ dir: join(PROJECT_ROOT, 'bizar-dash', 'skills'), source: 'shipped' },
|
|
39
|
+
{ dir: join(PROJECT_ROOT, '.agents', 'skills'), source: 'project' },
|
|
40
|
+
{ dir: join(PROJECT_ROOT, '.opencode', 'skills'), source: 'project' },
|
|
41
|
+
];
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
// -- Frontmatter parser (no external deps) ------------------------------------
|
|
45
|
+
|
|
39
46
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* output is loosely categorized, so we accept any string.
|
|
47
|
+
* Parse YAML frontmatter from a SKILL.md string.
|
|
48
|
+
* Returns { description, name, ...rest } from frontmatter, plus the body.
|
|
43
49
|
*/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
{
|
|
47
|
-
{ id: 'frameworks', label: 'Frameworks', icon: 'layers' },
|
|
48
|
-
{ id: 'tools', label: 'Tools', icon: 'wrench' },
|
|
49
|
-
{ id: 'testing', label: 'Testing', icon: 'flask' },
|
|
50
|
-
{ id: 'design', label: 'Design', icon: 'palette' },
|
|
51
|
-
{ id: 'reasoning', label: 'Reasoning', icon: 'brain' },
|
|
52
|
-
{ id: 'planning', label: 'Planning', icon: 'map' },
|
|
53
|
-
{ id: 'gitops', label: 'GitOps', icon: 'git-branch' },
|
|
54
|
-
{ id: 'docs', label: 'Docs', icon: 'book' },
|
|
55
|
-
];
|
|
50
|
+
function parseFrontmatter(raw) {
|
|
51
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
52
|
+
if (!fmMatch) return { description: '', name: '', body: raw };
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
const fmStr = fmMatch[1];
|
|
55
|
+
const body = fmMatch[2];
|
|
56
|
+
const fm = {};
|
|
57
|
+
|
|
58
|
+
// Very simple YAML key: value parser (values only - no nested structures)
|
|
59
|
+
for (const line of fmStr.split('\n')) {
|
|
60
|
+
const colonIdx = line.indexOf(':');
|
|
61
|
+
if (colonIdx < 0) continue;
|
|
62
|
+
const key = line.slice(0, colonIdx).trim();
|
|
63
|
+
const val = line.slice(colonIdx + 1).trim().replace(/^['"]|['"]$/g, '');
|
|
64
|
+
fm[key] = val;
|
|
63
65
|
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
description: fm.description || '',
|
|
69
|
+
name: fm.name || '',
|
|
70
|
+
body,
|
|
71
|
+
...fm,
|
|
72
|
+
};
|
|
64
73
|
}
|
|
65
74
|
|
|
66
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Extract display name from body: first # Heading or ## Heading.
|
|
77
|
+
* Returns null if none found.
|
|
78
|
+
*/
|
|
79
|
+
function extractHeading(body) {
|
|
80
|
+
const m = body.match(/^#{1,2}\s+(.+)$/m);
|
|
81
|
+
return m ? m[1].trim() : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// -- Core scanner -------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Scan one directory for SKILL.md files.
|
|
88
|
+
* Returns an array of parsed skill objects.
|
|
89
|
+
*/
|
|
90
|
+
function scanDir(dir, source) {
|
|
91
|
+
if (!existsSync(dir)) return [];
|
|
92
|
+
const out = [];
|
|
67
93
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
94
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
if (!entry.isDirectory()) continue;
|
|
97
|
+
const mdPath = join(dir, entry.name, 'SKILL.md');
|
|
98
|
+
if (!existsSync(mdPath)) continue;
|
|
99
|
+
let raw;
|
|
100
|
+
try {
|
|
101
|
+
raw = readFileSync(mdPath, 'utf8');
|
|
102
|
+
} catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const { description, name: fmName, body } = parseFrontmatter(raw);
|
|
106
|
+
const displayName = fmName || extractHeading(body) || entry.name;
|
|
107
|
+
const relPath = mdPath; // absolute path to the file
|
|
108
|
+
|
|
109
|
+
out.push({
|
|
110
|
+
name: displayName,
|
|
111
|
+
description: description.slice(0, 200),
|
|
112
|
+
source,
|
|
113
|
+
path: relPath,
|
|
114
|
+
body, // full body for detail view
|
|
115
|
+
});
|
|
116
|
+
}
|
|
70
117
|
} catch {
|
|
71
|
-
|
|
118
|
+
// Directory unreadable - skip
|
|
72
119
|
}
|
|
120
|
+
return out;
|
|
73
121
|
}
|
|
74
122
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
123
|
+
/** In-memory cache with manual invalidation. */
|
|
124
|
+
let _cache = null;
|
|
125
|
+
let _cacheAge = 0;
|
|
126
|
+
const CACHE_TTL_MS = 30_000;
|
|
127
|
+
|
|
128
|
+
function getCache() {
|
|
129
|
+
if (_cache && Date.now() - _cacheAge < CACHE_TTL_MS) return _cache;
|
|
130
|
+
const skills = [];
|
|
131
|
+
for (const { dir, source } of sourceRoots()) {
|
|
132
|
+
skills.push(...scanDir(dir, source));
|
|
133
|
+
}
|
|
134
|
+
_cache = skills;
|
|
135
|
+
_cacheAge = Date.now();
|
|
136
|
+
return skills;
|
|
84
137
|
}
|
|
85
138
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
{
|
|
90
|
-
id: 'anthropics/skills',
|
|
91
|
-
name: 'Anthropic Skills',
|
|
92
|
-
description: 'Curated skill set from Anthropic — Claude prompt patterns, agents, evals.',
|
|
93
|
-
category: 'reasoning',
|
|
94
|
-
source: 'anthropics/skills',
|
|
95
|
-
tags: ['reasoning', 'agents'],
|
|
96
|
-
version: '1.0.0',
|
|
97
|
-
installCmd: 'skills add anthropics/skills',
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
id: 'vercel-labs/agent-skills',
|
|
101
|
-
name: 'Vercel Agent Skills',
|
|
102
|
-
description: 'Vercel-Labs agent skill set — React, Next.js, frontend performance.',
|
|
103
|
-
category: 'frameworks',
|
|
104
|
-
source: 'vercel-labs/agent-skills',
|
|
105
|
-
tags: ['react', 'nextjs', 'frontend'],
|
|
106
|
-
version: '1.0.0',
|
|
107
|
-
installCmd: 'skills add vercel-labs/agent-skills',
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
id: 'supabase/agent-skills',
|
|
111
|
-
name: 'Supabase Agent Skills',
|
|
112
|
-
description: 'Supabase agent skill set — Postgres, Auth, Edge Functions.',
|
|
113
|
-
category: 'tools',
|
|
114
|
-
source: 'supabase/agent-skills',
|
|
115
|
-
tags: ['postgres', 'auth'],
|
|
116
|
-
version: '1.0.0',
|
|
117
|
-
installCmd: 'skills add supabase/agent-skills',
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
id: 'mattpocock/skills',
|
|
121
|
-
name: 'Matt Pocock Skills',
|
|
122
|
-
description: 'TypeScript, TDD, testing patterns from Matt Pocock.',
|
|
123
|
-
category: 'testing',
|
|
124
|
-
source: 'mattpocock/skills',
|
|
125
|
-
tags: ['typescript', 'tdd'],
|
|
126
|
-
version: '1.0.0',
|
|
127
|
-
installCmd: 'skills add mattpocock/skills',
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
id: 'cloudflare/skills',
|
|
131
|
-
name: 'Cloudflare Skills',
|
|
132
|
-
description: 'Cloudflare Workers, Durable Objects, Agents SDK.',
|
|
133
|
-
category: 'tools',
|
|
134
|
-
source: 'cloudflare/skills',
|
|
135
|
-
tags: ['cloudflare', 'workers'],
|
|
136
|
-
version: '1.0.0',
|
|
137
|
-
installCmd: 'skills add cloudflare/skills',
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
id: 'anthropics/claude-code-skills',
|
|
141
|
-
name: 'Claude Code Skills',
|
|
142
|
-
description: 'Skills for Claude Code — prompt patterns and tool usage.',
|
|
143
|
-
category: 'reasoning',
|
|
144
|
-
source: 'anthropics/claude-code-skills',
|
|
145
|
-
tags: ['claude', 'code'],
|
|
146
|
-
version: '1.0.0',
|
|
147
|
-
installCmd: 'skills add anthropics/claude-code-skills',
|
|
148
|
-
},
|
|
149
|
-
];
|
|
139
|
+
export function invalidateCache() {
|
|
140
|
+
_cache = null;
|
|
141
|
+
_cacheAge = 0;
|
|
150
142
|
}
|
|
151
143
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (lower.some((t) => /reasoning|agent|claude|gpt|llm|chat/.test(t))) return 'reasoning';
|
|
159
|
-
if (lower.some((t) => /plan|roadmap|architecture/.test(t))) return 'planning';
|
|
160
|
-
if (lower.some((t) => /git|github|gh|ci|deploy/.test(t))) return 'gitops';
|
|
161
|
-
if (lower.some((t) => /doc|readme|wiki|changelog/.test(t))) return 'docs';
|
|
162
|
-
if (lower.some((t) => /python|js|ts|rust|go|kotlin|swift|c\+\+|ruby|java|php/.test(t))) return 'languages';
|
|
163
|
-
return 'tools';
|
|
144
|
+
// -- Fuzzy search (simple includes-based, no external deps) --------------------
|
|
145
|
+
|
|
146
|
+
function fuzzyMatch(skill, q) {
|
|
147
|
+
const hay = `${skill.name} ${skill.description}`.toLowerCase();
|
|
148
|
+
const terms = q.toLowerCase().split(/\s+/);
|
|
149
|
+
return terms.every((t) => hay.includes(t));
|
|
164
150
|
}
|
|
165
151
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
152
|
+
function stripAnsi(str) {
|
|
153
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// -- Public API ----------------------------------------------------------------
|
|
169
157
|
|
|
170
|
-
|
|
158
|
+
export const skillsStore = {
|
|
159
|
+
/** List all skills grouped by source. */
|
|
171
160
|
async list() {
|
|
172
|
-
|
|
173
|
-
const out = await safeExec(['list', '--json']);
|
|
174
|
-
let items = [];
|
|
175
|
-
if (out && out.trim().startsWith('[')) {
|
|
176
|
-
try {
|
|
177
|
-
items = JSON.parse(out);
|
|
178
|
-
} catch {
|
|
179
|
-
items = [];
|
|
180
|
-
}
|
|
181
|
-
} else if (out && out.trim().startsWith('{')) {
|
|
182
|
-
// Some versions return { skills: [...] }
|
|
183
|
-
try {
|
|
184
|
-
const parsed = JSON.parse(out);
|
|
185
|
-
items = parsed.skills || parsed.items || [];
|
|
186
|
-
} catch {
|
|
187
|
-
items = [];
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
// If the CLI is not installed or returned nothing, fall back to a
|
|
191
|
-
// minimal local mock so the UI is still useful in dev.
|
|
192
|
-
if (items.length === 0) {
|
|
193
|
-
items = mockCatalog().map((m) => ({ ...m, installed: true, mock: true }));
|
|
194
|
-
}
|
|
195
|
-
return items.map((it) => {
|
|
196
|
-
const id = it.id || it.name || it.source || it.path || 'unknown';
|
|
197
|
-
const name = it.name || id.split('/').pop() || id;
|
|
198
|
-
const tags = Array.isArray(it.tags) ? it.tags : [];
|
|
199
|
-
const category = it.category || inferCategoryFromTags(tags);
|
|
200
|
-
const enabled = state[id]?.enabled !== false; // default enabled
|
|
201
|
-
return {
|
|
202
|
-
id,
|
|
203
|
-
name,
|
|
204
|
-
description: it.description || '',
|
|
205
|
-
category,
|
|
206
|
-
tags,
|
|
207
|
-
version: it.version || '0.0.0',
|
|
208
|
-
source: it.source || id,
|
|
209
|
-
path: it.path || null,
|
|
210
|
-
installed: true,
|
|
211
|
-
enabled,
|
|
212
|
-
mock: !!it.mock,
|
|
213
|
-
};
|
|
214
|
-
});
|
|
161
|
+
return getCache();
|
|
215
162
|
},
|
|
216
163
|
|
|
217
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* Fuzzy search by name + description.
|
|
166
|
+
* Returns plain objects - NO ANSI codes, NO formatted terminal output.
|
|
167
|
+
*/
|
|
218
168
|
async search(query) {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
if (out) {
|
|
224
|
-
try {
|
|
225
|
-
const parsed = JSON.parse(out);
|
|
226
|
-
items = Array.isArray(parsed) ? parsed : parsed.results || parsed.items || [];
|
|
227
|
-
} catch {
|
|
228
|
-
// Fall through to text-mode parse.
|
|
229
|
-
items = out
|
|
230
|
-
.split(/\r?\n/)
|
|
231
|
-
.filter((l) => l.trim())
|
|
232
|
-
.map((l) => ({ name: l.trim(), id: l.trim(), source: l.trim() }));
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (items.length === 0) {
|
|
237
|
-
// Mock fallback: filter the mock catalog by query.
|
|
238
|
-
const q = trimmed.toLowerCase();
|
|
239
|
-
items = mockCatalog().filter((m) =>
|
|
240
|
-
!q ||
|
|
241
|
-
m.name.toLowerCase().includes(q) ||
|
|
242
|
-
m.description.toLowerCase().includes(q) ||
|
|
243
|
-
m.tags.some((t) => t.toLowerCase().includes(q)),
|
|
244
|
-
);
|
|
245
|
-
if (items.length === 0 && !q) items = mockCatalog();
|
|
246
|
-
}
|
|
247
|
-
return items.map((it) => {
|
|
248
|
-
const id = it.id || it.name || it.source || 'unknown';
|
|
249
|
-
const name = it.name || id.split('/').pop() || id;
|
|
250
|
-
const tags = Array.isArray(it.tags) ? it.tags : [];
|
|
251
|
-
return {
|
|
252
|
-
id,
|
|
253
|
-
name,
|
|
254
|
-
description: it.description || '',
|
|
255
|
-
category: it.category || inferCategoryFromTags(tags),
|
|
256
|
-
tags,
|
|
257
|
-
version: it.version || '0.0.0',
|
|
258
|
-
source: it.source || id,
|
|
259
|
-
installCmd: it.installCmd || `skills add ${id}`,
|
|
260
|
-
};
|
|
261
|
-
});
|
|
262
|
-
},
|
|
263
|
-
|
|
264
|
-
/** Install a skill by name/source. */
|
|
265
|
-
async install(name, source) {
|
|
266
|
-
const target = source || name;
|
|
267
|
-
try {
|
|
268
|
-
const { stdout, stderr } = await execFileP('skills', ['add', target], {
|
|
269
|
-
timeout: 60_000,
|
|
270
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
271
|
-
});
|
|
272
|
-
return {
|
|
273
|
-
ok: true,
|
|
274
|
-
name: target,
|
|
275
|
-
output: [stdout, stderr].filter(Boolean).join('\n').trim(),
|
|
276
|
-
};
|
|
277
|
-
} catch (err) {
|
|
278
|
-
return {
|
|
279
|
-
ok: false,
|
|
280
|
-
name: target,
|
|
281
|
-
error: err instanceof Error ? err.message : String(err),
|
|
282
|
-
};
|
|
283
|
-
}
|
|
169
|
+
const q = (query || '').trim();
|
|
170
|
+
const all = getCache();
|
|
171
|
+
if (!q) return all;
|
|
172
|
+
return all.filter((s) => fuzzyMatch(s, q));
|
|
284
173
|
},
|
|
285
174
|
|
|
286
|
-
/**
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
return
|
|
175
|
+
/**
|
|
176
|
+
* Return a single skill by source + name.
|
|
177
|
+
*/
|
|
178
|
+
async get(source, name) {
|
|
179
|
+
const all = getCache();
|
|
180
|
+
return all.find(
|
|
181
|
+
(s) => s.source === source && (s.name === name || s.path.endsWith(`/${name}/SKILL.md`)),
|
|
182
|
+
) || null;
|
|
292
183
|
},
|
|
293
184
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return { ok: true, enabled: true };
|
|
185
|
+
/** Force a cache refresh. */
|
|
186
|
+
refresh() {
|
|
187
|
+
invalidateCache();
|
|
188
|
+
return getCache();
|
|
299
189
|
},
|
|
300
190
|
};
|