agentgui 1.0.904 → 1.0.906

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/CLAUDE.md CHANGED
@@ -373,7 +373,7 @@ The README.md uses shields.io badges with a consistent pattern:
373
373
 
374
374
  **Codebase Insight Stale:** `.codeinsight` snapshot is outdated (v1.0.811 claimed server.js=3407L, db-queries.js=1412L, but actual as of 2026-04-17: server.js=201L, db-queries.js=94L). Heavy refactoring already done. Before acting on insight "issues" (SQL injection claims, hardcoded secrets, large files), always verify with grep/read against current state. Most reported issues are false positives or stale.
375
375
 
376
- **Test Harness Pattern:** Use better-sqlite3 in-memory Database, call `initSchema()` → `migrateConversationColumns()` → `migrateACPSchema()` in order (conversations table needs agentType column from second migration). `createQueries` signature: `(db, prep, generateId)` where `prep=(sql)=>db.prepare(sql)`. Silence console during schema init to keep test output clean. Node ships with better-sqlite3 available (optional dep resolved).
376
+ **Test Harness Pattern:** test.js prefers `bun:sqlite`, falls back to `better-sqlite3` via createRequire (so it runs under both `bun test.js` and `node test.js` provided `bun install` / `npm install` ran). Call `initSchema()` → `migrateConversationColumns()` → `migrateACPSchema()` in order (conversations table needs agentType column from second migration). `createQueries` signature: `(db, prep, generateId)` where `prep=(sql)=>db.prepare(sql)`. Silence console during schema init to keep test output clean. CI runs `bun test.js` via `.github/workflows/test.yml` on every push/PR to main.
377
377
 
378
378
  **CI Auto-Rewrites History:** Every push to main triggers Auto-Declaudeify workflow that filters Claude coauthor commits and force-pushes filtered history. After a push, `git fetch origin` is needed — local SHA drifts from origin SHA.
379
379
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.904",
3
+ "version": "1.0.906",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
package/site/theme.mjs CHANGED
@@ -26,17 +26,17 @@ function Hero() {
26
26
  if (!home || !home.hero) return null;
27
27
  return C.Panel({
28
28
  style: 'margin:8px',
29
- children: [
29
+ children: h('div', { style: 'padding:24px 22px' },
30
30
  C.Heading({ level: 1, style: 'margin:0 0 8px 0', children: home.hero.heading || site.title }),
31
31
  home.hero.subheading ? C.Lede({ children: home.hero.subheading }) : null,
32
32
  home.hero.body ? h('p', { style: 'margin:8px 0 16px 0;color:var(--panel-text-2);max-width:64ch' }, home.hero.body) : null,
33
- (home.hero.badges && home.hero.badges.length) ? h('div', { style: 'display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px' },
33
+ (home.hero.badges && home.hero.badges.length) ? h('div', { style: 'display:flex;gap:6px;flex-wrap:wrap;margin:0 0 12px 0' },
34
34
  ...home.hero.badges.map((b, i) => C.Chip({ key: 'b'+i, children: b.label }))
35
35
  ) : null,
36
36
  (home.hero.ctas && home.hero.ctas.length) ? h('div', { style: 'display:flex;gap:8px;flex-wrap:wrap' },
37
37
  ...home.hero.ctas.map((c, i) => C.Btn({ key: 'c'+i, href: c.href, primary: c.primary, children: c.label }))
38
38
  ) : null
39
- ]
39
+ )
40
40
  });
41
41
  }
42
42
 
@@ -60,13 +60,102 @@ function Features() {
60
60
  function Quickstart() {
61
61
  if (!home || !home.quickstart || !home.quickstart.lines || !home.quickstart.lines.length) return null;
62
62
  const lineNodes = home.quickstart.lines.map((l, i) => h('div', { key: 'q'+i, class: 'cli' },
63
- h('span', { class: 'prompt' }, (l.kind === 'cmt' ? '#' : '$')),
63
+ h('span', { class: 'prompt' }, (l.kind === 'cmt' ? '#' : '
64
+
65
+ function Examples() {
66
+ if (!home || !home.examples || !home.examples.items || !home.examples.items.length) return null;
67
+ const rows = home.examples.items.map((it, i) => C.RowLink({
68
+ key: 'e'+i,
69
+ title: it.name,
70
+ sub: it.desc || '',
71
+ meta: it.cta || 'open',
72
+ href: it.href || '#'
73
+ }));
74
+ return C.Panel({
75
+ title: home.examples.heading || 'examples',
76
+ style: 'margin:8px',
77
+ children: rows
78
+ });
79
+ }
80
+
81
+ function Footer() {
82
+ return h('footer', { class: 'app-status' },
83
+ h('span', { class: 'item' }, 'styled with '),
84
+ h('a', { class: 'item', href: 'https://anentrypoint.github.io/design/' }, 'anentrypoint-design'),
85
+ h('span', { class: 'item' }, '·'),
86
+ h('a', { class: 'item', href: 'https://247420.xyz' }, '247420.xyz'),
87
+ h('span', { class: 'spread' }),
88
+ site.repo ? h('a', { class: 'item', href: site.repo }, 'source ↗') : null
89
+ );
90
+ }
91
+
92
+ const navItems = (nav && nav.links ? nav.links : []).map(l => [String(l.label || ''), l.href]);
93
+
94
+ const App = C.AppShell({
95
+ topbar: C.Topbar({
96
+ brand: '247420',
97
+ leaf: site.title || '',
98
+ items: navItems
99
+ }),
100
+ crumb: C.Crumb({
101
+ trail: ['247420'],
102
+ leaf: site.title || ''
103
+ }),
104
+ main: h('div', {},
105
+ Hero(),
106
+ Features(),
107
+ Quickstart(),
108
+ Examples()
109
+ ),
110
+ status: Footer()
111
+ });
112
+
113
+ applyDiff(document.getElementById('app'), [App]);
114
+ `;
115
+
116
+ const html = ({ site, nav, home }) => `<!DOCTYPE html>
117
+ <html lang="en" class="ds-247420">
118
+ <head>
119
+ <meta charset="UTF-8" />
120
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
121
+ <title>${escapeHtml(site.title)}${site.tagline ? ' — ' + escapeHtml(site.tagline) : ''}</title>
122
+ <meta name="description" content="${escapeHtml(site.description || site.tagline || site.title)}" />
123
+ <meta property="og:title" content="${escapeHtml(site.title)}" />
124
+ <meta property="og:description" content="${escapeHtml(site.description || site.tagline || '')}" />
125
+ <meta property="og:url" content="${escapeHtml(site.url || '')}" />
126
+ <link rel="canonical" href="${escapeHtml(site.url || '')}" />
127
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ctext y='26' font-size='26'%3E${encodeURIComponent(site.glyph || '◆')}%3C/text%3E%3C/svg%3E" />
128
+ <script type="importmap">{"imports":{"anentrypoint-design":"${SDK_URL}"}}</script>
129
+ <style>html,body{margin:0;padding:0}body{background:var(--app-bg,#FBF6EB);color:var(--ink,#1F1B16);font-family:var(--ff-ui,'Nunito',system-ui,sans-serif)}</style>
130
+ </head>
131
+ <body>
132
+ <div id="app"></div>
133
+ <script type="application/json" id="__site__">${escapeJson({ site, nav, home })}</script>
134
+ <script type="module">${clientScript}</script>
135
+ </body>
136
+ </html>
137
+ `;
138
+
139
+ export default {
140
+ render: async (ctx) => {
141
+ const site = ctx.readGlobal('site') || {};
142
+ const nav = ctx.readGlobal('navigation') || { links: [] };
143
+ const homeDoc = ctx.read('pages').docs.find(p => p.id === 'home');
144
+ if (!homeDoc) throw new Error('config/pages/home.yaml missing or has no id: home');
145
+
146
+ return [{
147
+ path: 'index.html',
148
+ html: html({ site, nav, home: homeDoc })
149
+ }];
150
+ }
151
+ };
152
+ )),
64
153
  h('span', { class: 'cmd' }, l.text)
65
154
  ));
66
155
  return C.Panel({
67
156
  title: home.quickstart.heading || 'quick start',
68
157
  style: 'margin:8px',
69
- children: lineNodes
158
+ children: h('div', { style: 'padding:16px 22px' }, ...lineNodes)
70
159
  });
71
160
  }
72
161
 
package/test.js CHANGED
@@ -1,184 +1,118 @@
1
1
  import assert from 'assert';
2
- import Database from 'better-sqlite3';
2
+ import { createRequire } from 'module';
3
3
  import { initSchema } from './database-schema.js';
4
4
  import { migrateConversationColumns } from './database-migrations.js';
5
5
  import { migrateACPSchema } from './database-migrations-acp.js';
6
6
  import { createQueries } from './lib/db-queries.js';
7
7
  import { encode, decode } from './lib/codec.js';
8
8
  import { WsRouter } from './lib/ws-protocol.js';
9
- import * as toolInstallMachine from './lib/tool-install-machine.js';
10
- import * as execMachine from './lib/execution-machine.js';
11
- import * as acpServerMachine from './lib/acp-server-machine.js';
9
+ import { WSOptimizer } from './lib/ws-optimizer.js';
10
+ import * as tim from './lib/tool-install-machine.js';
11
+ import * as exm from './lib/execution-machine.js';
12
+ import * as asm from './lib/acp-server-machine.js';
13
+ import { maskKey, buildSystemPrompt } from './lib/provider-config.js';
14
+ import { buildBaseUrl, isRemoteRequest, encodeOAuthState, decodeOAuthState } from './lib/oauth-common.js';
15
+ import { compareVersions } from './lib/tool-version-check.js';
16
+ import { initializeDescriptors, getAgentDescriptor } from './lib/agent-descriptors.js';
17
+ const require = createRequire(import.meta.url);
18
+ let Database;
19
+ try { Database = (await import('bun:sqlite')).default; } catch { Database = require('better-sqlite3'); }
12
20
 
13
21
  let passed = 0, failed = 0;
14
- const section = (name, fn) => {
15
- try { fn(); console.log(`ok — ${name}`); passed++; }
16
- catch (err) { console.error(`FAIL — ${name}: ${err.message}`); failed++; }
17
- };
18
-
22
+ const ok = (name, fn) => Promise.resolve().then(fn).then(
23
+ () => { console.log(`ok — ${name}`); passed++; },
24
+ (err) => { console.error(`FAIL — ${name}: ${err.message}`); failed++; });
19
25
  function inMemDb() {
20
26
  const db = new Database(':memory:');
21
- db.pragma('foreign_keys = ON');
27
+ if (db.pragma) db.pragma('foreign_keys = ON'); else db.run('PRAGMA foreign_keys = ON');
22
28
  const origLog = console.log, origWarn = console.warn;
23
29
  console.log = () => {}; console.warn = () => {};
24
- try {
25
- initSchema(db);
26
- migrateConversationColumns(db);
27
- migrateACPSchema(db);
28
- } finally { console.log = origLog; console.warn = origWarn; }
29
- const prep = (sql) => db.prepare(sql);
30
+ try { initSchema(db); migrateConversationColumns(db); migrateACPSchema(db); }
31
+ finally { console.log = origLog; console.warn = origWarn; }
30
32
  const gid = (p) => `${p}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
31
- return { db, prep, gid };
33
+ return { db, prep: (sql) => db.prepare(sql), gid };
32
34
  }
33
-
34
- section('codec: roundtrip primitives', () => {
35
+ const run = async () => {
36
+ await ok('codec: roundtrip + binary', () => {
35
37
  const obj = { a: 1, b: 'str', c: [1, 2, 3], d: { nested: true } };
36
38
  assert.deepEqual(decode(encode(obj)), obj);
37
- });
38
-
39
- section('codec: binary payload', () => {
40
- const buf = Buffer.from([1, 2, 3, 4]);
41
- const round = decode(encode({ bin: buf }));
39
+ const round = decode(encode({ bin: Buffer.from([1, 2, 3, 4]) }));
42
40
  assert.deepEqual(Array.from(round.bin), [1, 2, 3, 4]);
43
41
  });
44
42
 
45
- section('db: init schema creates conversations table', () => {
43
+ await ok('db: init schema creates conversations table', () => {
46
44
  const { db } = inMemDb();
47
- const t = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='conversations'").get();
48
- assert.ok(t, 'conversations table exists');
45
+ assert.ok(db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='conversations'").get());
49
46
  });
50
47
 
51
- section('db-queries: createConversation round-trip', () => {
48
+ await ok('db-queries: createConversation round-trip', () => {
52
49
  const { db, prep, gid } = inMemDb();
53
50
  const q = createQueries(db, prep, gid);
54
51
  const c = q.createConversation('claude-code', 'Test', '/tmp', 'sonnet', null);
55
- assert.equal(c.title, 'Test');
56
- const fetched = q.getConversation(c.id);
57
- assert.equal(fetched.title, 'Test');
58
- assert.equal(fetched.status, 'active');
59
- });
60
-
61
- section('db-queries: archive + restore', () => {
62
- const { db, prep, gid } = inMemDb();
63
- const q = createQueries(db, prep, gid);
64
- const c = q.createConversation('claude-code', 'A');
65
- q.archiveConversation(c.id);
66
- assert.equal(q.getConversation(c.id).status, 'archived');
67
- q.restoreConversation(c.id);
52
+ assert.equal(q.getConversation(c.id).title, 'Test');
68
53
  assert.equal(q.getConversation(c.id).status, 'active');
69
54
  });
70
55
 
71
- section('db-queries: streaming flag', () => {
56
+ await ok('db-queries: archive + restore + streaming flag', () => {
72
57
  const { db, prep, gid } = inMemDb();
73
58
  const q = createQueries(db, prep, gid);
74
- const c = q.createConversation('claude-code', 'S');
75
- q.setIsStreaming(c.id, true);
76
- assert.equal(q.getIsStreaming(c.id), true);
77
- q.setIsStreaming(c.id, false);
78
- assert.equal(q.getIsStreaming(c.id), false);
59
+ const c = q.createConversation('claude-code', 'A');
60
+ q.archiveConversation(c.id); assert.equal(q.getConversation(c.id).status, 'archived');
61
+ q.restoreConversation(c.id); assert.equal(q.getConversation(c.id).status, 'active');
62
+ q.setIsStreaming(c.id, true); assert.equal(q.getIsStreaming(c.id), true);
63
+ q.setIsStreaming(c.id, false); assert.equal(q.getIsStreaming(c.id), false);
79
64
  });
80
65
 
81
- section('acp-queries: createThread + getThread + patchThread', () => {
66
+ await ok('acp-queries: thread crud + search', () => {
82
67
  const { db, prep, gid } = inMemDb();
83
68
  const q = createQueries(db, prep, gid);
84
69
  const t = q.createThread({ foo: 'bar' });
85
- assert.ok(t.thread_id);
86
- const got = q.getThread(t.thread_id);
87
- assert.deepEqual(got.metadata, { foo: 'bar' });
88
- const patched = q.patchThread(t.thread_id, { metadata: { foo: 'baz' }, status: 'active' });
89
- assert.equal(patched.status, 'active');
70
+ assert.deepEqual(q.getThread(t.thread_id).metadata, { foo: 'bar' });
71
+ q.patchThread(t.thread_id, { metadata: { foo: 'baz' }, status: 'active' });
90
72
  assert.deepEqual(q.getThread(t.thread_id).metadata, { foo: 'baz' });
91
- });
92
-
93
- section('acp-queries: searchThreads parameterized', () => {
94
- const { db, prep, gid } = inMemDb();
95
- const q = createQueries(db, prep, gid);
96
- q.createThread({ kind: 'a' });
97
73
  q.createThread({ kind: 'b' });
98
- const r = q.searchThreads({});
99
- assert.equal(r.total, 2);
100
- assert.equal(r.threads.length, 2);
74
+ assert.equal(q.searchThreads({}).total, 2);
101
75
  });
102
76
 
103
- section('WsRouter: dispatch registered method', async () => {
77
+ await ok('WsRouter: dispatch + 404 + error + legacy', async () => {
104
78
  const router = new WsRouter();
105
79
  router.handle('ping', async (p) => ({ pong: p.n }));
80
+ router.handle('boom', async () => { throw Object.assign(new Error('kaboom'), { code: 422 }); });
81
+ let legacy = null;
82
+ router.onLegacy((m) => { legacy = m; });
106
83
  const replies = [];
107
- const ws = { readyState: 1, send: (buf) => replies.push(decode(buf)), clientId: 'c1' };
84
+ const ws = { readyState: 1, send: (b) => replies.push(decode(b)), clientId: 'c' };
108
85
  await router.onMessage(ws, encode({ r: 1, m: 'ping', p: { n: 7 } }));
109
- assert.deepEqual(replies[0], { r: 1, d: { pong: 7 } });
110
- });
111
-
112
- section('WsRouter: unknown method replies 404', async () => {
113
- const router = new WsRouter();
114
- const replies = [];
115
- const ws = { readyState: 1, send: (buf) => replies.push(decode(buf)), clientId: 'c' };
116
86
  await router.onMessage(ws, encode({ r: 2, m: 'nope', p: {} }));
117
- assert.equal(replies[0].r, 2);
118
- assert.equal(replies[0].e.c, 404);
119
- });
120
-
121
- section('WsRouter: handler exception becomes error reply', async () => {
122
- const router = new WsRouter();
123
- router.handle('boom', async () => { throw Object.assign(new Error('kaboom'), { code: 422 }); });
124
- const replies = [];
125
- const ws = { readyState: 1, send: (buf) => replies.push(decode(buf)), clientId: 'c' };
126
87
  await router.onMessage(ws, encode({ r: 3, m: 'boom', p: {} }));
127
- assert.equal(replies[0].e.c, 422);
128
- assert.equal(replies[0].e.m, 'kaboom');
129
- });
130
-
131
- section('WsRouter: legacy handler called for non-RPC messages', async () => {
132
- const router = new WsRouter();
133
- let seen = null;
134
- router.onLegacy((msg) => { seen = msg; });
135
- const ws = { readyState: 1, send: () => {}, clientId: 'c' };
136
88
  await router.onMessage(ws, encode({ type: 'subscribe', id: 'x' }));
137
- assert.equal(seen.type, 'subscribe');
138
- });
139
-
140
- section('tool-install-machine: actors map exposed', () => {
141
- const actors = toolInstallMachine.getMachineActors();
142
- assert.ok(actors instanceof Map);
143
- });
144
-
145
- section('execution-machine: snapshot returns null for unknown id', () => {
146
- assert.equal(execMachine.snapshot('nonexistent-conv-id'), null);
147
- });
148
-
149
- section('acp-server-machine: lifecycle transitions', () => {
150
- const actor = acpServerMachine.getOrCreate('test-tool');
151
- assert.equal(actor.getSnapshot().value, 'stopped');
152
- acpServerMachine.send('test-tool', { type: 'START', pid: 123 });
153
- assert.equal(acpServerMachine.get('test-tool').getSnapshot().value, 'starting');
154
- acpServerMachine.send('test-tool', { type: 'HEALTHY', providerInfo: { ok: true } });
155
- assert.equal(acpServerMachine.isHealthy('test-tool'), true);
156
- acpServerMachine.stopAll();
89
+ assert.deepEqual(replies[0], { r: 1, d: { pong: 7 } });
90
+ assert.equal(replies[1].e.c, 404);
91
+ assert.equal(replies[2].e.c, 422);
92
+ assert.equal(legacy.type, 'subscribe');
157
93
  });
158
94
 
159
- section('acp-server-machine: getMachineActors returns Map', () => {
160
- const actors = acpServerMachine.getMachineActors();
161
- assert.ok(actors instanceof Map);
95
+ await ok('machines: tool-install + execution + acp-server lifecycle', () => {
96
+ assert.ok(tim.getMachineActors() instanceof Map);
97
+ assert.equal(exm.snapshot('nonexistent-conv-id'), null);
98
+ assert.equal(asm.getOrCreate('test-tool').getSnapshot().value, 'stopped');
99
+ asm.send('test-tool', { type: 'START', pid: 123 });
100
+ assert.equal(asm.get('test-tool').getSnapshot().value, 'starting');
101
+ asm.send('test-tool', { type: 'HEALTHY', providerInfo: { ok: true } });
102
+ assert.equal(asm.isHealthy('test-tool'), true);
103
+ asm.stopAll();
162
104
  });
163
105
 
164
- section('workflow-plugin: deps only list active plugins', async () => {
106
+ await ok('workflow-plugin + agent-registry hermes', async () => {
165
107
  const wp = await import('./lib/plugins/workflow-plugin.js');
166
108
  assert.deepEqual(wp.default.dependencies, ['database']);
167
- assert.equal(typeof wp.default.init, 'function');
168
- });
169
-
170
- section('agent-registry: hermes registered as stdio ACP', async () => {
171
109
  const { registry } = await import('./lib/claude-runner-agents.js');
172
- assert.ok(registry.has('hermes'));
173
110
  const h = registry.get('hermes');
174
- assert.equal(h.name, 'Hermes Agent');
175
- assert.equal(h.command, 'hermes');
176
111
  assert.equal(h.protocol, 'acp');
177
112
  assert.deepEqual(h.buildArgs(), ['acp']);
178
- assert.ok(h.supportedFeatures.includes('acp-protocol'));
179
113
  });
180
114
 
181
- section('delete-all: soft-deletes conv rows + wipes related data', () => {
115
+ await ok('delete-all: soft-deletes + wipes related', () => {
182
116
  const { db, prep, gid } = inMemDb();
183
117
  const q = createQueries(db, prep, gid);
184
118
  const c1 = q.createConversation('claude-code', 'A');
@@ -190,9 +124,77 @@ section('delete-all: soft-deletes conv rows + wipes related data', () => {
190
124
  const rows = db.prepare('SELECT status, count(*) as c FROM conversations GROUP BY status').all();
191
125
  assert.deepEqual(rows, [{ status: 'deleted', c: 2 }]);
192
126
  assert.equal(db.prepare('SELECT count(*) as c FROM messages').get().c, 0);
193
- assert.equal(db.prepare('SELECT count(*) as c FROM sessions').get().c, 0);
194
127
  assert.equal(q.getConversationsList().length, 0);
195
128
  });
196
129
 
130
+ await ok('provider-config: maskKey + buildSystemPrompt', () => {
131
+ assert.equal(maskKey(''), '****');
132
+ assert.equal(maskKey('short'), '****');
133
+ assert.equal(maskKey('sk-abcd1234efgh'), '****efgh');
134
+ assert.equal(buildSystemPrompt('claude-code'), '');
135
+ assert.equal(buildSystemPrompt('opencode', 'sonnet'), 'Use opencode subagent for all tasks. Model: sonnet.');
136
+ assert.equal(buildSystemPrompt('foo-·-bar', null, 'sub'), 'Use foo subagent for all tasks. Subagent: sub.');
137
+ });
138
+
139
+ await ok('oauth-common: state codec + url helpers', () => {
140
+ const enc = encodeOAuthState('csrf-tok', 'https://relay.test/cb');
141
+ const dec = decodeOAuthState(enc);
142
+ assert.equal(dec.csrfToken, 'csrf-tok');
143
+ assert.equal(dec.relayUrl, 'https://relay.test/cb');
144
+ const fallback = decodeOAuthState('not-base64-json');
145
+ assert.equal(fallback.csrfToken, 'not-base64-json');
146
+ assert.equal(fallback.relayUrl, null);
147
+ const reqRemote = { headers: { 'x-forwarded-host': 'a.com', 'x-forwarded-proto': 'https' }, socket: {} };
148
+ assert.equal(buildBaseUrl(reqRemote, 3000), 'https://a.com');
149
+ assert.equal(isRemoteRequest(reqRemote), true);
150
+ assert.equal(buildBaseUrl({ headers: {}, socket: {} }, 3000), 'http://127.0.0.1:3000');
151
+ assert.equal(isRemoteRequest({ headers: {} }), false);
152
+ });
153
+
154
+ await ok('tool-version-check: compareVersions', () => {
155
+ assert.equal(compareVersions('1.0.0', '1.0.1'), true);
156
+ assert.equal(compareVersions('1.0.1', '1.0.0'), false);
157
+ assert.equal(compareVersions('1.0.0', '1.0.0'), false);
158
+ assert.equal(compareVersions('1.2', '1.2.1'), true);
159
+ assert.equal(compareVersions('2.0.0', '1.99.99'), false);
160
+ assert.equal(compareVersions(null, '1.0.0'), false);
161
+ assert.equal(compareVersions('1.0.0', null), false);
162
+ });
163
+
164
+ await ok('agent-descriptors: initialize + cache', () => {
165
+ const n = initializeDescriptors([{ id: 'claude-code', name: 'Claude Code', path: '/x' }]);
166
+ assert.equal(n, 1);
167
+ const d = getAgentDescriptor('claude-code');
168
+ assert.equal(d.metadata.ref.name, 'Claude Code');
169
+ assert.ok(d.specs.input.properties.model);
170
+ assert.ok(d.specs.thread_state.properties.sessionId);
171
+ assert.equal(getAgentDescriptor('nope'), null);
172
+ });
173
+
174
+ await ok('ws-optimizer: high-priority flushes immediately', () => {
175
+ const opt = new WSOptimizer();
176
+ const sent = [];
177
+ const ws = { readyState: 1, clientId: 'c1', send: (b) => sent.push(decode(b)) };
178
+ opt.sendToClient(ws, { type: 'streaming_start', id: 1 });
179
+ assert.equal(sent.length, 1);
180
+ assert.equal(sent[0].type, 'streaming_start');
181
+ opt.removeClient(ws);
182
+ assert.equal(opt.getStats().clients, 0);
183
+ });
184
+
185
+ await ok('ws-optimizer: low-priority batches via timer', async () => {
186
+ const opt = new WSOptimizer();
187
+ const sent = [];
188
+ const ws = { readyState: 1, clientId: 'c2', latencyTier: 'excellent', send: (b) => sent.push(decode(b)) };
189
+ opt.sendToClient(ws, { type: 'tts_audio', n: 1 });
190
+ opt.sendToClient(ws, { type: 'tts_audio', n: 2 });
191
+ assert.equal(sent.length, 0);
192
+ await new Promise(r => setTimeout(r, 40));
193
+ assert.equal(sent.length, 1);
194
+ assert.equal(sent[0].length, 2);
195
+ opt.removeClient(ws);
196
+ });
197
+
197
198
  console.log(`\n${passed} passed, ${failed} failed`);
198
199
  process.exit(failed === 0 ? 0 : 1);
200
+ }; run();