agentgui 1.0.903 → 1.0.905
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 +1 -1
- package/package.json +1 -1
- package/site/theme.mjs +1 -0
- package/test.js +125 -123
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:**
|
|
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
package/site/theme.mjs
CHANGED
|
@@ -134,6 +134,7 @@ const html = ({ site, nav, home }) => `<!DOCTYPE html>
|
|
|
134
134
|
<link rel="canonical" href="${escapeHtml(site.url || '')}" />
|
|
135
135
|
<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" />
|
|
136
136
|
<script type="importmap">{"imports":{"anentrypoint-design":"${SDK_URL}"}}</script>
|
|
137
|
+
<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>
|
|
137
138
|
</head>
|
|
138
139
|
<body>
|
|
139
140
|
<div id="app"></div>
|
package/test.js
CHANGED
|
@@ -1,184 +1,118 @@
|
|
|
1
1
|
import assert from 'assert';
|
|
2
|
-
import
|
|
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
|
|
10
|
-
import * as
|
|
11
|
-
import * as
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
await ok('db: init schema creates conversations table', () => {
|
|
46
44
|
const { db } = inMemDb();
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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', '
|
|
75
|
-
q.
|
|
76
|
-
assert.equal(q.
|
|
77
|
-
q.setIsStreaming(c.id,
|
|
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
|
-
|
|
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.
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: (
|
|
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.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
assert.
|
|
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
|
-
|
|
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
|
-
|
|
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();
|