agentgui 1.0.867 → 1.0.869

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/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [Unreleased] - observability + dead-code sweep + root test suite
2
+
3
+ - Expose acp-server-machine snapshots in /api/debug/machines (previously only tool-install + execution machines)
4
+ - Add /api/debug/state endpoint consolidating active executions, queues, wsClients, machine snapshots, ws-stats, recentErrors, uptime, memory (DEBUG=1 gated)
5
+ - Alias /api/debug/ws-stats to /api/ws-stats so docs match runtime
6
+ - Add getMachineActors() export to lib/acp-server-machine.js
7
+ - Delete verified-orphan files: lib/rate-limit-machine.js, lib/model-download-machine.js, lib/agent-registry-configs.js, lib/plugin-interface.js, lib/plugins/git-plugin.js
8
+ - Add root test.js (16 cases): codec roundtrip, DB init+queries, acp-queries helpers, WsRouter dispatch/errors/legacy, XState machine actor getters + lifecycle. Uses better-sqlite3 in-memory — no mocks.
9
+ - CLAUDE.md: remove stale entries for deleted files
10
+
1
11
  ## [Unreleased] - merge: integrate remote UI redesign + cleanup CLAUDE.md
2
12
 
3
13
  - Merge origin/main: resolve UU conflicts in server.js (take remote _jsonlWatcher setter) and static/index.html (take remote UI redesign with overflow menu + SVG icons)
package/CLAUDE.md CHANGED
@@ -25,7 +25,6 @@ lib/acp-protocol.js ACP session/update message normalization (shared by all A
25
25
  lib/acp-sdk-manager.js ACP tool lifecycle - on-demand start opencode/kilo/codex, health checks, idle timeout
26
26
  lib/acp-server-machine.js XState v5 machine per ACP tool: stopped/starting/running/crashed/restarting states
27
27
  lib/agent-discovery.js Agent binary detection (findCommand), ACP server query, discoverAgents, CLI wrapper logic
28
- lib/agent-registry-configs.js Agent registration configs (Claude Code, OpenCode, Gemini, 10+ ACP agents)
29
28
  lib/agent-descriptors.js Data-driven ACP agent descriptor builder
30
29
  lib/checkpoint-manager.js Session recovery - load checkpoints, inject into resume flow, idempotency
31
30
  lib/codec.js msgpack encode/decode (pack/unpack wrappers)
@@ -37,7 +36,6 @@ lib/jsonl-watcher.js Watches ~/.claude/projects for JSONL file changes, delega
37
36
  lib/oauth-common.js Shared OAuth helpers (buildBaseUrl, isRemoteRequest, encodeOAuthState, result/relay pages)
38
37
  lib/oauth-gemini.js Gemini OAuth flow (credential discovery, token exchange, callback handling)
39
38
  lib/oauth-codex.js Codex CLI OAuth flow (PKCE S256, token exchange, callback handling)
40
- lib/plugin-interface.js Plugin interface contract definition
41
39
  lib/plugin-loader.js Plugin discovery and loading (EventEmitter-based)
42
40
  lib/pm2-manager.js PM2 process management wrapper
43
41
  lib/speech.js Speech-to-text and text-to-speech via @huggingface/transformers
@@ -370,3 +368,15 @@ The README.md uses shields.io badges with a consistent pattern:
370
368
  - **`JsonlWatcher._read(fp)` override:** Captures `this._currentFp` before calling `super._read(fp)`, making the file path available to `_line()` callbacks for project-directory decoding in `_conv()`. JSONL project dirs are encoded (e.g., `-config-workspace-agentgui`) — decoded via `'/' + dirName.slice(1).replace(/-/g, '/')`.
371
369
  - **`createHttpHandler` uses `getWss: () => wss` (lazy getter):** Passing `wss` directly would crash with TDZ since `wss` is declared after `createHttpHandler` is called. The function form defers access until request time when `wss` is initialized.
372
370
  - **`_promptPushIfWeOwnRemote` fires after every `streaming_complete`:** `client-streaming4.js` calls `git.check` on the server after each agent turn. If `ownsRemote && (hasChanges || hasUnpushed)`, it auto-sends "Push the changes to the remote repository." to the current conversation. `ownsRemote` is true for non-github remotes, and for github.com remotes only when `GITHUB_USER` env var is set and appears in the URL. Without `GITHUB_USER`, github.com remotes return `ownsRemote=false` (safe default).
371
+
372
+ ## Critical Knowledge for Future Sessions
373
+
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
+
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).
377
+
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
+
380
+ **Debug Endpoints Scattered:** Routes at `/api/debug`, `/api/debug/machines`, `/api/debug/state`, `/api/ws-stats`, `/api/debug/ws-stats` (alias). `routes-debug.js` wires all five + backup/restore. DEBUG API in browser: `window.__debug.getSyncState()`.
381
+
382
+ **Plugin Files NOT Orphans:** lib/plugins/* are dynamically loaded via `lib/plugin-loader.js` (import with file:// URL + cache-busting `?v=` timestamp). Only truly dead plugin was `lib/plugins/git-plugin.js`. Static scan showing orphans is misleading.
@@ -160,6 +160,10 @@ export function getBackoffDelay(toolId) {
160
160
  return calcBackoff(purgeOldRestarts(s.context.restarts));
161
161
  }
162
162
 
163
+ export function getMachineActors() {
164
+ return actors;
165
+ }
166
+
163
167
  export function stopAll() {
164
168
  for (const [, actor] of actors) actor.stop();
165
169
  actors.clear();
@@ -3,28 +3,94 @@ import path from 'path';
3
3
  import os from 'os';
4
4
  import * as toolInstallMachine from './tool-install-machine.js';
5
5
  import * as execMachine from './execution-machine.js';
6
+ import * as acpServerMachine from './acp-server-machine.js';
7
+
8
+ function collectMachineSnapshots(activeExecutions) {
9
+ const toolInstall = {};
10
+ for (const [id, actor] of toolInstallMachine.getMachineActors()) {
11
+ const s = actor.getSnapshot();
12
+ toolInstall[id] = { state: s.value, context: s.context };
13
+ }
14
+ const execution = {};
15
+ for (const id of activeExecutions.keys()) {
16
+ const s = execMachine.snapshot(id);
17
+ if (s) execution[id] = { state: s.value, context: { pid: s.context.pid, sessionId: s.context.sessionId, queueLen: s.context.queue?.length } };
18
+ }
19
+ const acpServer = {};
20
+ for (const [id, actor] of acpServerMachine.getMachineActors()) {
21
+ const s = actor.getSnapshot();
22
+ acpServer[id] = { state: s.value, context: { pid: s.context.pid, healthy: s.context.healthy, startedAt: s.context.startedAt, lastUsed: s.context.lastUsed, restarts: s.context.restarts?.length || 0, providerInfo: s.context.providerInfo } };
23
+ }
24
+ return { toolInstall, execution, acpServer };
25
+ }
26
+
27
+ function readRecentErrors(errLogPath, limit = 50) {
28
+ try {
29
+ const raw = fs.readFileSync(errLogPath, 'utf8');
30
+ return raw.trim().split('\n').slice(-limit).map(l => { try { return JSON.parse(l); } catch { return l; } });
31
+ } catch { return []; }
32
+ }
6
33
 
7
34
  export function register(deps) {
8
35
  const { sendJSON, queries, activeExecutions, messageQueues, syncClients, wsOptimizer, _errLogPath } = deps;
9
36
  const routes = {};
10
37
 
38
+ function snapshotExecutions() {
39
+ const o = {};
40
+ for (const [k, v] of activeExecutions) o[k] = { pid: v.pid, startTime: v.startTime, sessionId: v.sessionId, lastActivity: v.lastActivity };
41
+ return o;
42
+ }
43
+ function snapshotQueues() {
44
+ const o = {};
45
+ for (const [k, v] of messageQueues) o[k] = { length: v.length, messageIds: v.map(m => m.messageId) };
46
+ return o;
47
+ }
48
+ function snapshotWsClients() {
49
+ const out = [];
50
+ for (const ws of syncClients) out.push({ clientId: ws.clientId, subs: ws.subscriptions ? [...ws.subscriptions] : [] });
51
+ return out;
52
+ }
53
+ function streamingConvs() {
54
+ try { return queries.getConversationsList().filter(c => c.isStreaming).map(c => ({ id: c.id, isStreaming: c.isStreaming })); }
55
+ catch { return []; }
56
+ }
57
+
11
58
  routes['GET /api/debug'] = async (req, res) => {
12
- const execSnap = {};
13
- for (const [k, v] of activeExecutions) execSnap[k] = { pid: v.pid, startTime: v.startTime, sessionId: v.sessionId, lastActivity: v.lastActivity };
14
- const queueSnap = {};
15
- for (const [k, v] of messageQueues) queueSnap[k] = { length: v.length, messageIds: v.map(m => m.messageId) };
16
- let streamingConvs = [];
17
- try { streamingConvs = queries.getConversationsList().filter(c => c.isStreaming).map(c => ({ id: c.id, isStreaming: c.isStreaming })); } catch (_) {}
18
- const clientSubs = [];
19
- for (const ws of syncClients) clientSubs.push({ clientId: ws.clientId, subs: ws.subscriptions ? [...ws.subscriptions] : [] });
20
- let recentErrors = [];
21
- try {
22
- const raw = fs.readFileSync(_errLogPath, 'utf8');
23
- recentErrors = raw.trim().split('\n').slice(-20).map(l => { try { return JSON.parse(l); } catch { return l; } });
24
- } catch (_) {}
25
- sendJSON(req, res, 200, { activeExecutions: execSnap, messageQueues: queueSnap, streamingConversations: streamingConvs, wsClients: clientSubs, recentErrors });
59
+ sendJSON(req, res, 200, {
60
+ activeExecutions: snapshotExecutions(),
61
+ messageQueues: snapshotQueues(),
62
+ streamingConversations: streamingConvs(),
63
+ wsClients: snapshotWsClients(),
64
+ recentErrors: readRecentErrors(_errLogPath, 20),
65
+ });
66
+ };
67
+
68
+ routes['GET /api/debug/state'] = async (req, res) => {
69
+ if (!process.env.DEBUG) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
70
+ sendJSON(req, res, 200, {
71
+ activeExecutions: snapshotExecutions(),
72
+ messageQueues: snapshotQueues(),
73
+ streamingConversations: streamingConvs(),
74
+ wsClients: snapshotWsClients(),
75
+ machines: collectMachineSnapshots(activeExecutions),
76
+ wsStats: wsOptimizer.getStats(),
77
+ recentErrors: readRecentErrors(_errLogPath, 50),
78
+ uptime: process.uptime(),
79
+ memory: process.memoryUsage(),
80
+ });
81
+ };
82
+
83
+ routes['GET /api/debug/machines'] = async (req, res) => {
84
+ if (!process.env.DEBUG) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
85
+ sendJSON(req, res, 200, collectMachineSnapshots(activeExecutions));
26
86
  };
27
87
 
88
+ routes['GET /api/ws-stats'] = async (req, res) => {
89
+ sendJSON(req, res, 200, wsOptimizer.getStats());
90
+ };
91
+
92
+ routes['GET /api/debug/ws-stats'] = routes['GET /api/ws-stats'];
93
+
28
94
  routes['GET /api/backup'] = async (req, res) => {
29
95
  const dbPath = path.join(os.homedir(), '.gmgui', 'data.db');
30
96
  if (!fs.existsSync(dbPath)) { sendJSON(req, res, 404, { error: 'Database not found' }); return; }
@@ -53,28 +119,7 @@ export function register(deps) {
53
119
  });
54
120
  };
55
121
 
56
- routes['GET /api/debug/machines'] = async (req, res) => {
57
- if (!process.env.DEBUG) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
58
- const toolSnap = {};
59
- for (const [id, actor] of toolInstallMachine.getMachineActors()) {
60
- const s = actor.getSnapshot();
61
- toolSnap[id] = { state: s.value, context: s.context };
62
- }
63
- const execSnap = {};
64
- for (const id of activeExecutions.keys()) {
65
- const s = execMachine.snapshot(id);
66
- if (s) execSnap[id] = { state: s.value, context: { pid: s.context.pid, sessionId: s.context.sessionId, queueLen: s.context.queue?.length } };
67
- }
68
- sendJSON(req, res, 200, { toolInstall: toolSnap, execution: execSnap });
69
- };
70
-
71
- routes['GET /api/ws-stats'] = async (req, res) => {
72
- sendJSON(req, res, 200, wsOptimizer.getStats());
73
- };
74
-
75
- routes['_match'] = (method, pathOnly) => {
76
- return routes[`${method} ${pathOnly}`] || null;
77
- };
122
+ routes['_match'] = (method, pathOnly) => routes[`${method} ${pathOnly}`] || null;
78
123
 
79
124
  return routes;
80
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.867",
3
+ "version": "1.0.869",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
package/test.js ADDED
@@ -0,0 +1,165 @@
1
+ import assert from 'assert';
2
+ import Database from 'better-sqlite3';
3
+ import { initSchema } from './database-schema.js';
4
+ import { migrateConversationColumns } from './database-migrations.js';
5
+ import { migrateACPSchema } from './database-migrations-acp.js';
6
+ import { createQueries } from './lib/db-queries.js';
7
+ import { encode, decode } from './lib/codec.js';
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';
12
+
13
+ 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
+
19
+ function inMemDb() {
20
+ const db = new Database(':memory:');
21
+ db.pragma('foreign_keys = ON');
22
+ const origLog = console.log, origWarn = console.warn;
23
+ 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
+ const gid = (p) => `${p}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
31
+ return { db, prep, gid };
32
+ }
33
+
34
+ section('codec: roundtrip primitives', () => {
35
+ const obj = { a: 1, b: 'str', c: [1, 2, 3], d: { nested: true } };
36
+ 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 }));
42
+ assert.deepEqual(Array.from(round.bin), [1, 2, 3, 4]);
43
+ });
44
+
45
+ section('db: init schema creates conversations table', () => {
46
+ 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');
49
+ });
50
+
51
+ section('db-queries: createConversation round-trip', () => {
52
+ const { db, prep, gid } = inMemDb();
53
+ const q = createQueries(db, prep, gid);
54
+ 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);
68
+ assert.equal(q.getConversation(c.id).status, 'active');
69
+ });
70
+
71
+ section('db-queries: streaming flag', () => {
72
+ const { db, prep, gid } = inMemDb();
73
+ 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);
79
+ });
80
+
81
+ section('acp-queries: createThread + getThread + patchThread', () => {
82
+ const { db, prep, gid } = inMemDb();
83
+ const q = createQueries(db, prep, gid);
84
+ 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');
90
+ 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
+ q.createThread({ kind: 'b' });
98
+ const r = q.searchThreads({});
99
+ assert.equal(r.total, 2);
100
+ assert.equal(r.threads.length, 2);
101
+ });
102
+
103
+ section('WsRouter: dispatch registered method', async () => {
104
+ const router = new WsRouter();
105
+ router.handle('ping', async (p) => ({ pong: p.n }));
106
+ const replies = [];
107
+ const ws = { readyState: 1, send: (buf) => replies.push(decode(buf)), clientId: 'c1' };
108
+ 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
+ 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
+ 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
+ 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();
157
+ });
158
+
159
+ section('acp-server-machine: getMachineActors returns Map', () => {
160
+ const actors = acpServerMachine.getMachineActors();
161
+ assert.ok(actors instanceof Map);
162
+ });
163
+
164
+ console.log(`\n${passed} passed, ${failed} failed`);
165
+ process.exit(failed === 0 ? 0 : 1);
@@ -1,125 +0,0 @@
1
- import { acpProtocolHandler } from './acp-protocol.js';
2
-
3
- const SIMPLE_ACP_AGENTS = [
4
- { id: 'goose', name: 'Goose', command: 'goose' },
5
- { id: 'openhands', name: 'OpenHands', command: 'openhands' },
6
- { id: 'augment', name: 'Augment Code', command: 'augment' },
7
- { id: 'cline', name: 'Cline', command: 'cline' },
8
- { id: 'kimi', name: 'Kimi CLI', command: 'kimi' },
9
- { id: 'qwen', name: 'Qwen Code', command: 'qwen-code' },
10
- { id: 'codex', name: 'Codex CLI', command: 'codex' },
11
- { id: 'mistral', name: 'Mistral Vibe', command: 'mistral-vibe' },
12
- { id: 'kiro', name: 'Kiro CLI', command: 'kiro' },
13
- { id: 'fast-agent', name: 'fast-agent', command: 'fast-agent' },
14
- ];
15
-
16
- function parseClaudeOutput(line) {
17
- try {
18
- const entry = JSON.parse(line);
19
- if (!entry || typeof entry !== 'object') return null;
20
- if (entry.type === 'user' && entry.isMeta === true) return null;
21
- if (entry.isApiErrorMessage === true && entry.error === 'rate_limit') {
22
- entry._rateLimitDetected = true;
23
- }
24
- if (entry.type === 'assistant' && entry.message) {
25
- entry._isFragment = entry.message.stop_reason === null || entry.message.stop_reason === undefined;
26
- }
27
- if (entry.type === 'system' && entry.subtype === 'turn_duration' && entry.durationMs) {
28
- entry._turnDurationMs = entry.durationMs;
29
- }
30
- if (entry.type === 'system' && entry.subtype === 'compact_boundary' && entry.compactMetadata) {
31
- entry._preTokens = entry.compactMetadata.preTokens;
32
- }
33
- if (entry.message?.usage) {
34
- const u = entry.message.usage;
35
- entry._cacheUsage = {
36
- cache_creation: u.cache_creation_input_tokens || u['cache_creation.ephemeral_1h_input_tokens'] || u['cache_creation.ephemeral_5m_input_tokens'] || 0,
37
- cache_read: u.cache_read_input_tokens || 0
38
- };
39
- }
40
- return entry;
41
- } catch {
42
- return null;
43
- }
44
- }
45
-
46
- export function registerAllAgents(registry) {
47
- registry.register({
48
- id: 'claude-code',
49
- name: 'Claude Code',
50
- command: 'claude',
51
- protocol: 'direct',
52
- supportsStdin: false,
53
- closeStdin: true,
54
- useJsonRpcStdin: false,
55
- supportedFeatures: ['streaming', 'resume', 'system-prompt', 'permissions-skip', 'steering'],
56
- spawnEnv: { MAX_THINKING_TOKENS: '0', AGENTGUI_SUBPROCESS: '1' },
57
- buildArgs(prompt, config) {
58
- const { verbose = true, outputFormat = 'stream-json', print = true, resumeSessionId = null, systemPrompt = null, model = null } = config;
59
- const flags = [];
60
- if (print) flags.push('--print');
61
- if (verbose) flags.push('--verbose');
62
- flags.push(`--output-format=${outputFormat}`);
63
- if (model) flags.push('--model', model);
64
- if (resumeSessionId) flags.push('--resume', resumeSessionId);
65
- if (systemPrompt) flags.push('--append-system-prompt', systemPrompt);
66
- flags.push('--dangerously-skip-permissions');
67
- flags.push(typeof prompt === 'string' ? prompt : String(prompt));
68
- return flags;
69
- },
70
- parseOutput: parseClaudeOutput,
71
- });
72
-
73
- registry.register({
74
- id: 'opencode',
75
- name: 'OpenCode',
76
- command: 'opencode',
77
- protocol: 'acp',
78
- supportsStdin: false,
79
- npxPackage: 'opencode-ai',
80
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
81
- buildArgs: () => ['acp'],
82
- protocolHandler: acpProtocolHandler,
83
- });
84
-
85
- registry.register({
86
- id: 'gemini',
87
- name: 'Gemini CLI',
88
- command: 'gemini',
89
- protocol: 'acp',
90
- supportsStdin: false,
91
- npxPackage: '@google/gemini-cli',
92
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
93
- buildArgs(prompt, config) {
94
- const args = ['--experimental-acp', '--yolo'];
95
- if (config?.model) args.push('--model', config.model);
96
- return args;
97
- },
98
- protocolHandler: acpProtocolHandler,
99
- });
100
-
101
- registry.register({
102
- id: 'kilo',
103
- name: 'Kilo CLI',
104
- command: 'kilo',
105
- protocol: 'acp',
106
- supportsStdin: false,
107
- npxPackage: '@kilocode/cli',
108
- supportedFeatures: ['streaming', 'resume', 'acp-protocol', 'models'],
109
- buildArgs: () => ['acp'],
110
- protocolHandler: acpProtocolHandler,
111
- });
112
-
113
- for (const agent of SIMPLE_ACP_AGENTS) {
114
- registry.register({
115
- id: agent.id,
116
- name: agent.name,
117
- command: agent.command,
118
- protocol: 'acp',
119
- supportsStdin: false,
120
- supportedFeatures: ['streaming', 'resume', 'acp-protocol'],
121
- buildArgs: () => ['acp'],
122
- protocolHandler: acpProtocolHandler,
123
- });
124
- }
125
- }
@@ -1,107 +0,0 @@
1
- import { createMachine, createActor, assign } from 'xstate';
2
-
3
- const machine = createMachine({
4
- id: 'model-download',
5
- initial: 'idle',
6
- context: {
7
- progress: null,
8
- error: null,
9
- startTime: null,
10
- waiters: [],
11
- },
12
- states: {
13
- idle: {
14
- on: {
15
- START: {
16
- target: 'downloading',
17
- actions: assign({ startTime: () => Date.now(), error: null }),
18
- },
19
- WAIT: {
20
- actions: assign(({ context, event }) => ({
21
- waiters: [...context.waiters, event.resolve],
22
- })),
23
- },
24
- },
25
- },
26
- downloading: {
27
- on: {
28
- PROGRESS: {
29
- actions: assign(({ event }) => ({ progress: event.progress })),
30
- },
31
- COMPLETE: { target: 'complete' },
32
- ERROR: {
33
- target: 'error',
34
- actions: assign(({ event }) => ({ error: event.error })),
35
- },
36
- WAIT: {
37
- actions: assign(({ context, event }) => ({
38
- waiters: [...context.waiters, event.resolve],
39
- })),
40
- },
41
- },
42
- },
43
- complete: {
44
- entry: assign(({ context }) => {
45
- for (const resolve of context.waiters) resolve(true);
46
- return { waiters: [] };
47
- }),
48
- on: {
49
- RESET: { target: 'idle' },
50
- },
51
- },
52
- error: {
53
- entry: assign(({ context }) => {
54
- for (const resolve of context.waiters) resolve(false);
55
- return { waiters: [] };
56
- }),
57
- on: {
58
- RESET: { target: 'idle' },
59
- START: {
60
- target: 'downloading',
61
- actions: assign({ startTime: () => Date.now(), error: null }),
62
- },
63
- },
64
- },
65
- },
66
- });
67
-
68
- let actor = null;
69
-
70
- export function getActor() {
71
- if (!actor) {
72
- actor = createActor(machine);
73
- actor.start();
74
- }
75
- return actor;
76
- }
77
-
78
- export function send(event) {
79
- return getActor().send(event);
80
- }
81
-
82
- export function snapshot() {
83
- return getActor().getSnapshot();
84
- }
85
-
86
- export function isDownloading() {
87
- const s = snapshot();
88
- return s.value === 'downloading';
89
- }
90
-
91
- export function isComplete() {
92
- const s = snapshot();
93
- return s.value === 'complete';
94
- }
95
-
96
- export function isError() {
97
- const s = snapshot();
98
- return s.value === 'error';
99
- }
100
-
101
- export function getState() {
102
- return snapshot().value;
103
- }
104
-
105
- export function getContext() {
106
- return snapshot().context;
107
- }
@@ -1,36 +0,0 @@
1
- // Plugin interface contract - every plugin must implement this
2
-
3
- export default {
4
- // Plugin metadata
5
- name: 'plugin-name', // unique identifier
6
- version: '1.0.0',
7
- dependencies: [], // list of other plugin names this depends on
8
-
9
- // Lifecycle methods (all required)
10
- async init(config, plugins) {
11
- // config = { router, wsManager, db, logger, env }
12
- // plugins = Map<name, plugin> of all loaded plugins
13
- // MUST return: { routes[], wsHandlers{}, api{}, stop() }
14
- return {
15
- routes: [], // [ { method, path, handler } ]
16
- wsHandlers: {}, // { eventType: handler(data, clients) }
17
- api: {}, // exported functions for other plugins
18
- };
19
- },
20
-
21
- async reload(state) {
22
- // Called on hot reload. Preserve state from previous instance.
23
- // Return new state (or updated state from previous)
24
- return state;
25
- },
26
-
27
- async stop() {
28
- // Graceful shutdown. Clean up resources.
29
- // No need to return anything.
30
- },
31
-
32
- // Optional: Called when another plugin throws error
33
- async handleError(error, context) {
34
- // context = { pluginName, phase, ... }
35
- },
36
- };
@@ -1,117 +0,0 @@
1
- // Git plugin - version control, workflow detection, push events
2
-
3
- import { execSync } from 'child_process';
4
- import path from 'path';
5
- import fs from 'fs';
6
-
7
- export default {
8
- name: 'git',
9
- version: '1.0.0',
10
- dependencies: [],
11
-
12
- async init(config, plugins) {
13
- let lastPushSha = null;
14
- let workflowsList = [];
15
- let pushInProgress = false;
16
-
17
- const getRepoRoot = () => {
18
- try {
19
- return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
20
- } catch {
21
- return process.cwd();
22
- }
23
- };
24
-
25
- const getStatus = async () => {
26
- try {
27
- const status = execSync('git status --short', { encoding: 'utf8' });
28
- const unpushed = execSync('git rev-list --count @{u}..HEAD', { encoding: 'utf8' }).trim();
29
- return { dirty: status.length > 0, unpushedCount: parseInt(unpushed) || 0 };
30
- } catch (e) {
31
- return { dirty: false, unpushedCount: 0 };
32
- }
33
- };
34
-
35
- const listWorkflows = () => {
36
- const root = getRepoRoot();
37
- const workflowDir = path.join(root, '.github', 'workflows');
38
- if (!fs.existsSync(workflowDir)) return [];
39
- return fs.readdirSync(workflowDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
40
- };
41
-
42
- const push = async (message) => {
43
- if (pushInProgress) throw new Error('Push already in progress');
44
- pushInProgress = true;
45
- try {
46
- execSync('git add -A');
47
- execSync(`git commit -m "${message}"`);
48
- const result = execSync('git push', { encoding: 'utf8' });
49
- lastPushSha = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
50
- return { success: true, sha: lastPushSha };
51
- } catch (error) {
52
- return { success: false, error: error.message };
53
- } finally {
54
- pushInProgress = false;
55
- }
56
- };
57
-
58
- workflowsList = listWorkflows();
59
-
60
- return {
61
- routes: [
62
- {
63
- method: 'GET',
64
- path: '/api/git/status',
65
- handler: async (req, res) => {
66
- const status = await getStatus();
67
- res.json({ ...status, workflows: workflowsList });
68
- },
69
- },
70
- {
71
- method: 'POST',
72
- path: '/api/git/push',
73
- handler: async (req, res) => {
74
- const { message } = req.body;
75
- try {
76
- const result = await push(message);
77
- res.json(result);
78
- } catch (e) {
79
- res.status(400).json({ error: e.message });
80
- }
81
- },
82
- },
83
- {
84
- method: 'GET',
85
- path: '/api/git/workflows',
86
- handler: (req, res) => {
87
- res.json({ workflows: workflowsList });
88
- },
89
- },
90
- {
91
- method: 'POST',
92
- path: '/api/git/workflow/:name/run',
93
- handler: (req, res) => {
94
- res.json({ status: 'not-implemented', message: 'Use GitHub Actions API' });
95
- },
96
- },
97
- ],
98
- wsHandlers: {
99
- git_status_changed: (data, clients) => {
100
- // Broadcast git status to all clients
101
- },
102
- },
103
- api: {
104
- getStatus,
105
- push,
106
- listWorkflows,
107
- },
108
- stop: async () => {},
109
- };
110
- },
111
-
112
- async reload(state) {
113
- return state;
114
- },
115
-
116
- async stop() {},
117
- };
@@ -1,99 +0,0 @@
1
- import { createMachine, createActor, assign } from 'xstate';
2
-
3
- const machine = createMachine({
4
- id: 'rate-limit',
5
- initial: 'normal',
6
- context: {
7
- retryCount: 0,
8
- retryAt: null,
9
- cooldownMs: null,
10
- isStreamDetected: false,
11
- },
12
- states: {
13
- normal: {
14
- entry: assign({ retryCount: 0, retryAt: null, cooldownMs: null, isStreamDetected: false }),
15
- on: {
16
- HIT: {
17
- target: 'limited',
18
- actions: assign(({ context, event }) => ({
19
- retryCount: context.retryCount + 1,
20
- retryAt: event.retryAt,
21
- cooldownMs: event.cooldownMs,
22
- isStreamDetected: event.isStreamDetected || false,
23
- })),
24
- },
25
- },
26
- },
27
- limited: {
28
- on: {
29
- CLEAR: [
30
- { guard: ({ context }) => context.retryCount >= 3, target: 'exceeded' },
31
- { target: 'normal' },
32
- ],
33
- HIT: {
34
- actions: assign(({ context, event }) => ({
35
- retryCount: context.retryCount + 1,
36
- retryAt: event.retryAt,
37
- cooldownMs: event.cooldownMs,
38
- isStreamDetected: event.isStreamDetected || false,
39
- })),
40
- },
41
- },
42
- },
43
- exceeded: {
44
- on: {
45
- RESET: { target: 'normal' },
46
- },
47
- },
48
- },
49
- });
50
-
51
- const actors = new Map();
52
-
53
- export function getOrCreate(convId) {
54
- if (actors.has(convId)) return actors.get(convId);
55
- const actor = createActor(machine);
56
- actor.start();
57
- actors.set(convId, actor);
58
- return actor;
59
- }
60
-
61
- export function get(convId) {
62
- return actors.get(convId) || null;
63
- }
64
-
65
- export function remove(convId) {
66
- const actor = actors.get(convId);
67
- if (actor) { actor.stop(); actors.delete(convId); }
68
- }
69
-
70
- export function snapshot(convId) {
71
- const actor = actors.get(convId);
72
- return actor ? actor.getSnapshot() : null;
73
- }
74
-
75
- export function send(convId, event) {
76
- const actor = getOrCreate(convId);
77
- actor.send(event);
78
- return actor.getSnapshot();
79
- }
80
-
81
- export function isLimited(convId) {
82
- const s = snapshot(convId);
83
- return s ? s.value === 'limited' : false;
84
- }
85
-
86
- export function isExceeded(convId) {
87
- const s = snapshot(convId);
88
- return s ? s.value === 'exceeded' : false;
89
- }
90
-
91
- export function getContext(convId) {
92
- const s = snapshot(convId);
93
- return s ? s.context : null;
94
- }
95
-
96
- export function has(convId) {
97
- const s = snapshot(convId);
98
- return s ? s.value !== 'normal' : false;
99
- }