@polderlabs/bizar-dash 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-B5X9g8B4.css +1 -0
- package/dist/assets/index-LqQuSp9d.js +388 -0
- package/dist/assets/index-LqQuSp9d.js.map +1 -0
- package/dist/index.html +18 -0
- package/package.json +67 -0
- package/src/cli.mjs +228 -0
- package/src/server/agents-store.mjs +190 -0
- package/src/server/api.mjs +913 -0
- package/src/server/browser.mjs +40 -0
- package/src/server/diagnostics-store.mjs +138 -0
- package/src/server/mods-loader.mjs +361 -0
- package/src/server/projects-store.mjs +198 -0
- package/src/server/providers-store.mjs +183 -0
- package/src/server/schedules-runner.mjs +150 -0
- package/src/server/schedules-store.mjs +233 -0
- package/src/server/search-store.mjs +120 -0
- package/src/server/server.mjs +388 -0
- package/src/server/state.mjs +357 -0
- package/src/server/tailscale-store.mjs +113 -0
- package/src/server/tasks-store.mjs +275 -0
- package/src/server/tui.mjs +844 -0
- package/src/server/watcher.mjs +81 -0
- package/src/web/App.tsx +316 -0
- package/src/web/components/Button.tsx +55 -0
- package/src/web/components/Card.tsx +40 -0
- package/src/web/components/EmptyState.tsx +30 -0
- package/src/web/components/Modal.tsx +137 -0
- package/src/web/components/SearchModal.tsx +185 -0
- package/src/web/components/Spinner.tsx +19 -0
- package/src/web/components/StatusBadge.tsx +25 -0
- package/src/web/components/Tag.tsx +28 -0
- package/src/web/components/Toast.tsx +142 -0
- package/src/web/components/Topbar.tsx +203 -0
- package/src/web/index.html +17 -0
- package/src/web/lib/api.ts +71 -0
- package/src/web/lib/markdown.tsx +59 -0
- package/src/web/lib/types.ts +388 -0
- package/src/web/lib/utils.ts +79 -0
- package/src/web/lib/ws.ts +132 -0
- package/src/web/main.tsx +12 -0
- package/src/web/styles/main.css +3148 -0
- package/src/web/views/Agents.tsx +406 -0
- package/src/web/views/Chat.tsx +527 -0
- package/src/web/views/Config.tsx +683 -0
- package/src/web/views/Mods.tsx +350 -0
- package/src/web/views/Overview.tsx +350 -0
- package/src/web/views/Plans.tsx +667 -0
- package/src/web/views/Schedules.tsx +299 -0
- package/src/web/views/Settings.tsx +571 -0
- package/src/web/views/Tasks.tsx +761 -0
- package/templates/mod/FORMAT.md +76 -0
- package/templates/mod/hello-mod/README.md +19 -0
- package/templates/mod/hello-mod/agents/greeter.md +8 -0
- package/templates/mod/hello-mod/commands/hello.md +6 -0
- package/templates/mod/hello-mod/mod.json +20 -0
- package/templates/mod/hello-mod/routes/ping.mjs +9 -0
- package/templates/mod/hello-mod/views/HelloView.tsx +10 -0
- package/tsconfig.json +23 -0
- package/vite.config.ts +24 -0
package/dist/index.html
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Bizar Dashboard</title>
|
|
7
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🪩</text></svg>" />
|
|
8
|
+
<link rel="preconnect" href="https://rsms.me/" />
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
10
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
11
|
+
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
|
12
|
+
<script type="module" crossorigin src="./assets/index-LqQuSp9d.js"></script>
|
|
13
|
+
<link rel="stylesheet" crossorigin href="./assets/index-B5X9g8B4.css">
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div id="root"></div>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@polderlabs/bizar-dash",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Web + TUI dashboard for the Bizar agent platform (optional companion to @polderlabs/bizar)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"bizar-dash": "src/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"main": "src/server/server.mjs",
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"dist/",
|
|
13
|
+
"vite.config.ts",
|
|
14
|
+
"tsconfig.json",
|
|
15
|
+
"templates/"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"dev": "vite",
|
|
19
|
+
"build": "vite build",
|
|
20
|
+
"typecheck": "tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"blessed": "^0.1.81",
|
|
24
|
+
"chokidar": "^3.6.0",
|
|
25
|
+
"express": "^4.22.0",
|
|
26
|
+
"fuse.js": "^7.0.0",
|
|
27
|
+
"lucide-react": "^0.460.0",
|
|
28
|
+
"react": "^18.3.0",
|
|
29
|
+
"react-dom": "^18.3.0",
|
|
30
|
+
"react-markdown": "^9.0.0",
|
|
31
|
+
"remark-gfm": "^4.0.0",
|
|
32
|
+
"ws": "^8.18.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/react": "^18.3.0",
|
|
36
|
+
"@types/react-dom": "^18.3.0",
|
|
37
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
38
|
+
"typescript": "^5.5.0",
|
|
39
|
+
"vite": "^5.4.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@polderlabs/bizar": ">=2.7.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"@polderlabs/bizar-dash": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20"
|
|
51
|
+
},
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
},
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "git+https://github.com/DrB0rk/BizarHarness.git"
|
|
59
|
+
},
|
|
60
|
+
"keywords": [
|
|
61
|
+
"opencode",
|
|
62
|
+
"ai-agent",
|
|
63
|
+
"dashboard",
|
|
64
|
+
"bizar",
|
|
65
|
+
"norse"
|
|
66
|
+
]
|
|
67
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* src/cli.mjs
|
|
4
|
+
*
|
|
5
|
+
* v3.0.0 — `bizar-dash` command.
|
|
6
|
+
*
|
|
7
|
+
* Subcommands:
|
|
8
|
+
* start — launch dashboard (default)
|
|
9
|
+
* stop — kill the running dashboard
|
|
10
|
+
* status — print port + URL of any running dashboard
|
|
11
|
+
* tui — run the TUI dashboard
|
|
12
|
+
* --no-web — TUI only (no browser)
|
|
13
|
+
* --web-only — web only
|
|
14
|
+
* --bg, --detach — start in background and return
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { spawn } from 'node:child_process';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { dirname } from 'node:path';
|
|
22
|
+
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = dirname(__filename);
|
|
25
|
+
const BIZAR_HOME = join(homedir(), '.config', 'bizar');
|
|
26
|
+
const PORT_FILE = join(BIZAR_HOME, 'dashboard.port');
|
|
27
|
+
const PID_FILE = join(BIZAR_HOME, 'dashboard.pid');
|
|
28
|
+
const DEFAULT_PORT = 4321;
|
|
29
|
+
|
|
30
|
+
function showHelp() {
|
|
31
|
+
console.log(`
|
|
32
|
+
bizar-dash — Web + TUI dashboard for the Bizar agent platform
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
bizar-dash Launch the dashboard (default = web)
|
|
36
|
+
bizar-dash start Start the dashboard in this process
|
|
37
|
+
bizar-dash stop Kill the running dashboard
|
|
38
|
+
bizar-dash status Show port + URL of any running dashboard
|
|
39
|
+
bizar-dash tui [--no-web] Run the TUI dashboard
|
|
40
|
+
bizar-dash --bg, --detach Start in background, return to shell
|
|
41
|
+
bizar-dash --web-only Web dashboard only (no TUI)
|
|
42
|
+
bizar-dash --no-web TUI only (no browser)
|
|
43
|
+
bizar-dash --help Show this help
|
|
44
|
+
|
|
45
|
+
Notes:
|
|
46
|
+
The dashboard reads your opencode config at
|
|
47
|
+
~/.config/opencode/. The per-project state lives in
|
|
48
|
+
~/.config/opencode/projects/<id>/. Mods are installed to
|
|
49
|
+
~/.config/bizar/mods/.
|
|
50
|
+
|
|
51
|
+
Install:
|
|
52
|
+
npm install -g @polderlabs/bizar-dash
|
|
53
|
+
npm install -g @polderlabs/bizar # required peer
|
|
54
|
+
`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function findFreePort(preferred) {
|
|
58
|
+
const net = await import('node:net');
|
|
59
|
+
for (let p = preferred; p < preferred + 100; p++) {
|
|
60
|
+
if (await isPortFree(net, p)) return p;
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`no free port in range ${preferred}..${preferred + 99}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isPortFree(net, port) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const server = net.createServer();
|
|
68
|
+
let settled = false;
|
|
69
|
+
const finish = (ok) => {
|
|
70
|
+
if (settled) return;
|
|
71
|
+
settled = true;
|
|
72
|
+
resolve(ok);
|
|
73
|
+
};
|
|
74
|
+
server.once('error', () => finish(false));
|
|
75
|
+
server.once('listening', () => server.close(() => finish(true)));
|
|
76
|
+
const t = setTimeout(() => finish(false), 1000);
|
|
77
|
+
server.listen(port, '127.0.0.1', () => clearTimeout(t));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function startDashboard({ port, projectRoot, opencodeConfigDir, bizarRoot } = {}) {
|
|
82
|
+
const { createServer } = await import('./server/server.mjs');
|
|
83
|
+
const { launchBrowser } = await import('./server/browser.mjs');
|
|
84
|
+
|
|
85
|
+
const usePort = port || (await findFreePort(DEFAULT_PORT));
|
|
86
|
+
const { server, close } = await createServer({
|
|
87
|
+
port: usePort,
|
|
88
|
+
projectRoot: projectRoot || process.cwd(),
|
|
89
|
+
opencodeConfigDir: opencodeConfigDir || join(homedir(), '.config', 'opencode'),
|
|
90
|
+
bizarRoot: bizarRoot || join(__dirname, '..', '..'),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await new Promise((resolve, reject) => {
|
|
94
|
+
server.once('error', reject);
|
|
95
|
+
server.listen(usePort, '127.0.0.1', () => {
|
|
96
|
+
server.off('error', reject);
|
|
97
|
+
resolve();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
mkdirSync(BIZAR_HOME, { recursive: true });
|
|
102
|
+
writeFileSync(PORT_FILE, String(usePort), 'utf8');
|
|
103
|
+
writeFileSync(PID_FILE, String(process.pid), 'utf8');
|
|
104
|
+
|
|
105
|
+
const url = `http://localhost:${usePort}/`;
|
|
106
|
+
await launchBrowser(url);
|
|
107
|
+
console.log(`Bizar dashboard: ${url}`);
|
|
108
|
+
console.log('Press Ctrl-C to stop the dashboard.');
|
|
109
|
+
|
|
110
|
+
await new Promise(() => {});
|
|
111
|
+
// Caller keeps the process alive
|
|
112
|
+
return { url, port: usePort, close };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function startInBackground(args) {
|
|
116
|
+
const binPath = join(__dirname, 'cli.mjs');
|
|
117
|
+
const child = spawn(process.execPath, [binPath, ...args], {
|
|
118
|
+
detached: true,
|
|
119
|
+
stdio: 'ignore',
|
|
120
|
+
cwd: process.cwd(),
|
|
121
|
+
env: process.env,
|
|
122
|
+
});
|
|
123
|
+
child.on('error', (err) => {
|
|
124
|
+
console.error(`Failed to start background dashboard: ${err.message}`);
|
|
125
|
+
});
|
|
126
|
+
child.unref();
|
|
127
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
128
|
+
if (existsSync(PORT_FILE)) {
|
|
129
|
+
const port = readFileSync(PORT_FILE, 'utf8').trim();
|
|
130
|
+
console.log(`Bizar dashboard started in background on http://localhost:${port}/`);
|
|
131
|
+
console.log(`Use 'bizar-dash status' to check, 'bizar-dash stop' to stop.`);
|
|
132
|
+
} else {
|
|
133
|
+
console.log('Bizar dashboard starting in background (port file not yet written)...');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function stopDashboard() {
|
|
138
|
+
if (!existsSync(PID_FILE)) {
|
|
139
|
+
console.log('No Bizar dashboard is running.');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const pid = parseInt(readFileSync(PID_FILE, 'utf8').trim(), 10);
|
|
143
|
+
if (!Number.isFinite(pid)) {
|
|
144
|
+
console.log(`Bad PID file: ${PID_FILE}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
process.kill(pid, 'SIGTERM');
|
|
149
|
+
console.log(`Stopped Bizar dashboard (pid ${pid}).`);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.log(`Could not stop dashboard (pid ${pid}): ${err.message}`);
|
|
152
|
+
}
|
|
153
|
+
try { unlinkSync(PORT_FILE); } catch { /* ignore */ }
|
|
154
|
+
try { unlinkSync(PID_FILE); } catch { /* ignore */ }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function showStatus() {
|
|
158
|
+
if (existsSync(PORT_FILE)) {
|
|
159
|
+
const port = readFileSync(PORT_FILE, 'utf8').trim();
|
|
160
|
+
console.log(`Bizar dashboard is running at http://localhost:${port}/`);
|
|
161
|
+
if (existsSync(PID_FILE)) {
|
|
162
|
+
console.log(`PID: ${readFileSync(PID_FILE, 'utf8').trim()}`);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
console.log('No Bizar dashboard is running. Use: bizar-dash start');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function runTui({ launchWeb } = {}) {
|
|
170
|
+
const { createServer } = await import('./server/server.mjs');
|
|
171
|
+
const { launchBrowser } = await import('./server/browser.mjs');
|
|
172
|
+
const { launchTui } = await import('./server/tui.mjs');
|
|
173
|
+
|
|
174
|
+
const port = await findFreePort(DEFAULT_PORT);
|
|
175
|
+
const { server, close: closeServer } = await createServer({
|
|
176
|
+
port,
|
|
177
|
+
projectRoot: process.cwd(),
|
|
178
|
+
opencodeConfigDir: join(homedir(), '.config', 'opencode'),
|
|
179
|
+
bizarRoot: join(__dirname, '..', '..'),
|
|
180
|
+
});
|
|
181
|
+
await new Promise((resolve, reject) => {
|
|
182
|
+
server.once('error', reject);
|
|
183
|
+
server.listen(port, '127.0.0.1', () => {
|
|
184
|
+
server.off('error', reject);
|
|
185
|
+
resolve();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
mkdirSync(BIZAR_HOME, { recursive: true });
|
|
189
|
+
writeFileSync(PORT_FILE, String(port), 'utf8');
|
|
190
|
+
writeFileSync(PID_FILE, String(process.pid), 'utf8');
|
|
191
|
+
|
|
192
|
+
if (launchWeb) {
|
|
193
|
+
const url = `http://localhost:${port}/`;
|
|
194
|
+
launchBrowser(url).catch(() => {});
|
|
195
|
+
console.log(`Web dashboard: ${url}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await launchTui({ port });
|
|
200
|
+
} finally {
|
|
201
|
+
try { closeServer(); } catch { /* ignore */ }
|
|
202
|
+
try { unlinkSync(PORT_FILE); } catch { /* ignore */ }
|
|
203
|
+
try { unlinkSync(PID_FILE); } catch { /* ignore */ }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const args = process.argv.slice(2);
|
|
208
|
+
|
|
209
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
210
|
+
showHelp();
|
|
211
|
+
} else if (args[0] === 'stop') {
|
|
212
|
+
await stopDashboard();
|
|
213
|
+
} else if (args[0] === 'status') {
|
|
214
|
+
showStatus();
|
|
215
|
+
} else if (args[0] === 'tui') {
|
|
216
|
+
const rest = args.slice(1);
|
|
217
|
+
const skipWeb = rest.includes('--no-web');
|
|
218
|
+
await runTui({ launchWeb: !skipWeb });
|
|
219
|
+
} else if (args.includes('--bg') || args.includes('--detach')) {
|
|
220
|
+
await startInBackground(['start']);
|
|
221
|
+
} else if (args.includes('--web-only')) {
|
|
222
|
+
await startDashboard();
|
|
223
|
+
} else if (args[0] === 'start' || args.length === 0) {
|
|
224
|
+
await startDashboard();
|
|
225
|
+
} else {
|
|
226
|
+
// Default: show help if we don't understand
|
|
227
|
+
showHelp();
|
|
228
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/agents-store.mjs
|
|
3
|
+
*
|
|
4
|
+
* v3.0.0 — Editable agents.
|
|
5
|
+
*
|
|
6
|
+
* Each agent is a markdown file with frontmatter at:
|
|
7
|
+
* ~/.config/opencode/agents/<name>.md
|
|
8
|
+
*
|
|
9
|
+
* The store reads / writes these files. The format is:
|
|
10
|
+
* ---
|
|
11
|
+
* description: ...
|
|
12
|
+
* model: ...
|
|
13
|
+
* mode: ...
|
|
14
|
+
* color: ...
|
|
15
|
+
* tools: ["bash","read","edit"]
|
|
16
|
+
* permissions: {...}
|
|
17
|
+
* ---
|
|
18
|
+
* <prompt body>
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
existsSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
readdirSync,
|
|
25
|
+
statSync,
|
|
26
|
+
mkdirSync,
|
|
27
|
+
unlinkSync,
|
|
28
|
+
} from 'node:fs';
|
|
29
|
+
import { join, dirname, basename } from 'node:path';
|
|
30
|
+
import { homedir } from 'node:os';
|
|
31
|
+
|
|
32
|
+
const HOME = homedir();
|
|
33
|
+
const AGENTS_DIR = join(HOME, '.config', 'opencode', 'agents');
|
|
34
|
+
|
|
35
|
+
function safeReadText(file, fallback = '') {
|
|
36
|
+
try {
|
|
37
|
+
if (!existsSync(file)) return fallback;
|
|
38
|
+
return readFileSync(file, 'utf8');
|
|
39
|
+
} catch {
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseFrontmatter(raw) {
|
|
45
|
+
if (!raw.startsWith('---')) return { frontmatter: {}, body: raw };
|
|
46
|
+
const end = raw.indexOf('\n---', 3);
|
|
47
|
+
if (end === -1) return { frontmatter: {}, body: raw };
|
|
48
|
+
const fmBlock = raw.slice(3, end).trim();
|
|
49
|
+
const body = raw.slice(end + 4).replace(/^\s+/, '');
|
|
50
|
+
const frontmatter = {};
|
|
51
|
+
for (const line of fmBlock.split(/\r?\n/)) {
|
|
52
|
+
const m = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line);
|
|
53
|
+
if (!m) continue;
|
|
54
|
+
const key = m[1];
|
|
55
|
+
let val = m[2].trim();
|
|
56
|
+
if (
|
|
57
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
58
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
59
|
+
) {
|
|
60
|
+
val = val.slice(1, -1);
|
|
61
|
+
}
|
|
62
|
+
frontmatter[key] = val;
|
|
63
|
+
}
|
|
64
|
+
return { frontmatter, body };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Serialize a frontmatter key — string is bare, value with special chars gets quoted. */
|
|
68
|
+
function fmVal(v) {
|
|
69
|
+
if (typeof v !== 'string') return JSON.stringify(v);
|
|
70
|
+
if (v === '') return '""';
|
|
71
|
+
if (/[:#\-?{}[\],&*!|>'"%@`]/.test(v) || v.includes(' ')) {
|
|
72
|
+
return JSON.stringify(v);
|
|
73
|
+
}
|
|
74
|
+
return v;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function serializeFrontmatter(fm) {
|
|
78
|
+
const lines = ['---'];
|
|
79
|
+
for (const [k, v] of Object.entries(fm)) {
|
|
80
|
+
if (v == null) continue;
|
|
81
|
+
if (Array.isArray(v)) {
|
|
82
|
+
lines.push(`${k}: [${v.map((x) => (typeof x === 'string' ? JSON.stringify(x) : x)).join(', ')}]`);
|
|
83
|
+
} else if (typeof v === 'object') {
|
|
84
|
+
lines.push(`${k}:`);
|
|
85
|
+
for (const [k2, v2] of Object.entries(v)) {
|
|
86
|
+
lines.push(` ${k2}: ${fmVal(String(v2))}`);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
lines.push(`${k}: ${fmVal(String(v))}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
lines.push('---');
|
|
93
|
+
return lines.join('\n') + '\n';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readAgent(name) {
|
|
97
|
+
const file = join(AGENTS_DIR, `${name}.md`);
|
|
98
|
+
if (!existsSync(file)) return null;
|
|
99
|
+
const raw = safeReadText(file);
|
|
100
|
+
const { frontmatter, body } = parseFrontmatter(raw);
|
|
101
|
+
const st = statSync(file);
|
|
102
|
+
return {
|
|
103
|
+
name,
|
|
104
|
+
description: frontmatter.description || '',
|
|
105
|
+
model: frontmatter.model || '',
|
|
106
|
+
mode: frontmatter.mode || 'subagent',
|
|
107
|
+
color: frontmatter.color || '',
|
|
108
|
+
tools: typeof frontmatter.tools === 'string'
|
|
109
|
+
? frontmatter.tools.split(',').map((s) => s.trim()).filter(Boolean)
|
|
110
|
+
: Array.isArray(frontmatter.tools) ? frontmatter.tools : [],
|
|
111
|
+
permissions: frontmatter.permissions || null,
|
|
112
|
+
prompt: body.trim(),
|
|
113
|
+
file,
|
|
114
|
+
path: file,
|
|
115
|
+
mtime: st.mtimeMs,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const agentsStore = {
|
|
120
|
+
AGENTS_DIR,
|
|
121
|
+
|
|
122
|
+
ensure() {
|
|
123
|
+
mkdirSync(AGENTS_DIR, { recursive: true });
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
list() {
|
|
127
|
+
this.ensure();
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const f of readdirSync(AGENTS_DIR)) {
|
|
130
|
+
if (!f.endsWith('.md')) continue;
|
|
131
|
+
const name = basename(f, '.md');
|
|
132
|
+
const agent = readAgent(name);
|
|
133
|
+
if (agent) out.push(agent);
|
|
134
|
+
}
|
|
135
|
+
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
136
|
+
return out;
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
get(name) {
|
|
140
|
+
return readAgent(name);
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
create(input) {
|
|
144
|
+
if (!input || typeof input !== 'object') throw new Error('agent input required');
|
|
145
|
+
if (!input.name || !/^[a-z0-9][a-z0-9-]{0,63}$/i.test(input.name)) {
|
|
146
|
+
throw new Error('invalid agent name');
|
|
147
|
+
}
|
|
148
|
+
const file = join(AGENTS_DIR, `${input.name}.md`);
|
|
149
|
+
if (existsSync(file)) {
|
|
150
|
+
throw new Error(`agent "${input.name}" already exists`);
|
|
151
|
+
}
|
|
152
|
+
const data = {
|
|
153
|
+
name: input.name,
|
|
154
|
+
description: input.description || '',
|
|
155
|
+
model: input.model || '',
|
|
156
|
+
mode: input.mode || 'subagent',
|
|
157
|
+
color: input.color || '',
|
|
158
|
+
tools: Array.isArray(input.tools) ? input.tools : [],
|
|
159
|
+
permissions: input.permissions || null,
|
|
160
|
+
prompt: input.prompt || '',
|
|
161
|
+
};
|
|
162
|
+
this.ensure();
|
|
163
|
+
writeFileSync(file, serializeFrontmatter(data) + '\n' + data.prompt, 'utf8');
|
|
164
|
+
return readAgent(input.name);
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
update(name, patch) {
|
|
168
|
+
if (!patch || typeof patch !== 'object') throw new Error('agent patch required');
|
|
169
|
+
const cur = readAgent(name);
|
|
170
|
+
if (!cur) throw new Error(`agent "${name}" not found`);
|
|
171
|
+
const data = {
|
|
172
|
+
description: patch.description ?? cur.description,
|
|
173
|
+
model: patch.model ?? cur.model,
|
|
174
|
+
mode: patch.mode ?? cur.mode,
|
|
175
|
+
color: patch.color ?? cur.color,
|
|
176
|
+
tools: Array.isArray(patch.tools) ? patch.tools : cur.tools,
|
|
177
|
+
permissions: patch.permissions ?? cur.permissions,
|
|
178
|
+
prompt: patch.prompt ?? cur.prompt,
|
|
179
|
+
};
|
|
180
|
+
writeFileSync(cur.file, serializeFrontmatter(data) + '\n' + data.prompt, 'utf8');
|
|
181
|
+
return readAgent(name);
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
delete(name) {
|
|
185
|
+
const file = join(AGENTS_DIR, `${name}.md`);
|
|
186
|
+
if (!existsSync(file)) return false;
|
|
187
|
+
unlinkSync(file);
|
|
188
|
+
return true;
|
|
189
|
+
},
|
|
190
|
+
};
|