agentgui 1.0.1126 → 1.0.1127

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.

Potentially problematic release.


This version of agentgui might be problematic. Click here for more details.

Files changed (170) hide show
  1. package/.gm/browser-config.json +1 -0
  2. package/.gm/disciplines/agentgui/mem-1779277005143-0-1352.json +1 -0
  3. package/.gm/disciplines/agentgui/memories/.flat-export-done +0 -0
  4. package/.gm/disciplines/agentgui/memories/mem-c07fa3b0836edbe2-1352.md +8 -0
  5. package/.gm/gm.db +0 -0
  6. package/.gm/memories/.flat-export-done +1 -0
  7. package/.gm/memories/mem-0131ff4869d86085-370.md +10 -0
  8. package/.gm/memories/mem-04049d11cb8c1661-2135.md +8 -0
  9. package/.gm/memories/mem-069ff10372ed1770-1375.md +8 -0
  10. package/.gm/memories/mem-08a4a9e050a4deff-3048.md +8 -0
  11. package/.gm/memories/mem-09597ee0df9c9224-965.md +8 -0
  12. package/.gm/memories/mem-0993d156f571b619-1059.md +8 -0
  13. package/.gm/memories/mem-0b681bf2340c8df1-3170.md +8 -0
  14. package/.gm/memories/mem-0c444abde86202f6-1681.md +8 -0
  15. package/.gm/memories/mem-0d1408fa048b9923-1159.md +8 -0
  16. package/.gm/memories/mem-0dad5d82fec829ba-887.md +8 -0
  17. package/.gm/memories/mem-10a776effb7ce3a6-935.md +8 -0
  18. package/.gm/memories/mem-10d264b4f8e66fb4-1492.md +8 -0
  19. package/.gm/memories/mem-118c35ac67541d98-2909.md +8 -0
  20. package/.gm/memories/mem-119cf70dd19aa0a5-2225.md +8 -0
  21. package/.gm/memories/mem-15a07cd761b337c4-4320.md +8 -0
  22. package/.gm/memories/mem-15c73aca528535cb-2183.md +8 -0
  23. package/.gm/memories/mem-166f52796185e0e7-1487.md +8 -0
  24. package/.gm/memories/mem-1a148cf9c3126df5-3963.md +8 -0
  25. package/.gm/memories/mem-1a459001058f618e-567.md +8 -0
  26. package/.gm/memories/mem-1af3e13f7618e668-2146.md +8 -0
  27. package/.gm/memories/mem-1fd31d6873e9c164-794.md +8 -0
  28. package/.gm/memories/mem-21f551be092fd77d-2340.md +8 -0
  29. package/.gm/memories/mem-231ff5de003f20f0-586.md +8 -0
  30. package/.gm/memories/mem-2567393d3adb03ab-347.md +8 -0
  31. package/.gm/memories/mem-27faf41cdd050c80-1261.md +8 -0
  32. package/.gm/memories/mem-28889e43e8ce64cc-3663.md +8 -0
  33. package/.gm/memories/mem-2f4fd06515abf439-2562.md +8 -0
  34. package/.gm/memories/mem-34a749430303c5f7-544.md +8 -0
  35. package/.gm/memories/mem-3602d11d7bce9446-1057.md +8 -0
  36. package/.gm/memories/mem-38b7e24dbcba2f3e-639.md +8 -0
  37. package/.gm/memories/mem-39a7cc2f887a90b1-1079.md +8 -0
  38. package/.gm/memories/mem-40d6169c8759061d-806.md +8 -0
  39. package/.gm/memories/mem-4420e6d00e5c8632-2318.md +8 -0
  40. package/.gm/memories/mem-447e5a54ddc9357a-654.md +8 -0
  41. package/.gm/memories/mem-455b3beb4afabe14-591.md +8 -0
  42. package/.gm/memories/mem-4845ee594759151c-560.md +8 -0
  43. package/.gm/memories/mem-4a1254c60b868b88-374.md +8 -0
  44. package/.gm/memories/mem-4bafafa47a0e4b6c-3237.md +8 -0
  45. package/.gm/memories/mem-4c06c45b4d31bc60-1202.md +8 -0
  46. package/.gm/memories/mem-4dfac801d0378448-3676.md +8 -0
  47. package/.gm/memories/mem-4ecd3b713e384170-122.md +10 -0
  48. package/.gm/memories/mem-51d8992a3c6f6c9c-1073.md +8 -0
  49. package/.gm/memories/mem-539eee7e8c6c10ee-2557.md +8 -0
  50. package/.gm/memories/mem-559b8b2abc4b1091-1665.md +8 -0
  51. package/.gm/memories/mem-59dd73a463e54948-871.md +8 -0
  52. package/.gm/memories/mem-5b99b882ffe1a3c2-2262.md +8 -0
  53. package/.gm/memories/mem-5feb252a5244149c-1791.md +8 -0
  54. package/.gm/memories/mem-6224f8d185797f2b-1047.md +8 -0
  55. package/.gm/memories/mem-6434ee8261a6a6a2-3126.md +8 -0
  56. package/.gm/memories/mem-66b0947b946f3437-1155.md +8 -0
  57. package/.gm/memories/mem-6bdf783a1f6be7c2-1062.md +8 -0
  58. package/.gm/memories/mem-6d49f08bebac54c9-744.md +8 -0
  59. package/.gm/memories/mem-70d96acb736af2d7-685.md +8 -0
  60. package/.gm/memories/mem-70de7d5e71fcd439-2680.md +8 -0
  61. package/.gm/memories/mem-713614f12d7029da-1577.md +8 -0
  62. package/.gm/memories/mem-720d76c88e64f6ee-1967.md +8 -0
  63. package/.gm/memories/mem-7535f34f430fc196-1340.md +8 -0
  64. package/.gm/memories/mem-861a588d2773681a-1311.md +8 -0
  65. package/.gm/memories/mem-8af7980a92e09959-891.md +8 -0
  66. package/.gm/memories/mem-8b438ef4c609b7af-401.md +8 -0
  67. package/.gm/memories/mem-8f59884039787ef3-743.md +8 -0
  68. package/.gm/memories/mem-92b199c2024ee1aa-2513.md +8 -0
  69. package/.gm/memories/mem-9329105b05b9c0f6-324.md +10 -0
  70. package/.gm/memories/mem-94f30cec3c283f97-782.md +10 -0
  71. package/.gm/memories/mem-956d4813319f65a3-1710.md +8 -0
  72. package/.gm/memories/mem-96fffbb6615e031b-1263.md +8 -0
  73. package/.gm/memories/mem-993ab31f7a10aefa-718.md +8 -0
  74. package/.gm/memories/mem-a3e440db285f655a-995.md +8 -0
  75. package/.gm/memories/mem-a52933cd6495e6f5-2624.md +8 -0
  76. package/.gm/memories/mem-a86426ac44138c96-1606.md +8 -0
  77. package/.gm/memories/mem-a95b1d49d71133ce-1007.md +8 -0
  78. package/.gm/memories/mem-a9968a186368b022-1150.md +8 -0
  79. package/.gm/memories/mem-aaeae38d8eb01809-1076.md +8 -0
  80. package/.gm/memories/mem-ade01d9c02677c55-1557.md +8 -0
  81. package/.gm/memories/mem-b06b8c85b81663e6-1250.md +8 -0
  82. package/.gm/memories/mem-b451e5e17e555202-1490.md +8 -0
  83. package/.gm/memories/mem-b64ad00dcb6f909e-937.md +8 -0
  84. package/.gm/memories/mem-b7d6018ceca55b6f-2220.md +8 -0
  85. package/.gm/memories/mem-bdf466854032cac9-1509.md +8 -0
  86. package/.gm/memories/mem-c0482039e6ab4c50-2047.md +8 -0
  87. package/.gm/memories/mem-c4aaec5c4e6f80c2-920.md +8 -0
  88. package/.gm/memories/mem-c66976ddacb4fbe8-1454.md +8 -0
  89. package/.gm/memories/mem-c7c5bc263850c332-1844.md +8 -0
  90. package/.gm/memories/mem-cda639547932c796-713.md +8 -0
  91. package/.gm/memories/mem-cdf04ff6a6a0aa6f-671.md +8 -0
  92. package/.gm/memories/mem-cf8472c84d9e04c4-1883.md +8 -0
  93. package/.gm/memories/mem-cfd4d894370c63c3-1288.md +8 -0
  94. package/.gm/memories/mem-d060480435f92eec-1146.md +8 -0
  95. package/.gm/memories/mem-d347c921b79e6d44-1099.md +8 -0
  96. package/.gm/memories/mem-d3c8ea6c861bfbb8-1457.md +8 -0
  97. package/.gm/memories/mem-d5e928c552cd87f9-214.md +10 -0
  98. package/.gm/memories/mem-d63bc3b8a0269844-406.md +10 -0
  99. package/.gm/memories/mem-d9c19257abc2fa5e-1061.md +8 -0
  100. package/.gm/memories/mem-d9f26358a7cd3bb8-866.md +8 -0
  101. package/.gm/memories/mem-da1c91acf63d81d3-863.md +8 -0
  102. package/.gm/memories/mem-dc1ac3b04d22317e-664.md +8 -0
  103. package/.gm/memories/mem-dceeae20bc56d407-711.md +8 -0
  104. package/.gm/memories/mem-dd98b21c0896dc04-1557.md +8 -0
  105. package/.gm/memories/mem-df74674eec7745ac-178.md +10 -0
  106. package/.gm/memories/mem-dfe3b466fd5d1199-802.md +8 -0
  107. package/.gm/memories/mem-e0f9286aed2d5a06-1116.md +8 -0
  108. package/.gm/memories/mem-e1950b064099b7be-1895.md +8 -0
  109. package/.gm/memories/mem-e1d5431ae2542872-961.md +8 -0
  110. package/.gm/memories/mem-e490a181de6469fc-487.md +8 -0
  111. package/.gm/memories/mem-e72c6efbb27326fb-392.md +8 -0
  112. package/.gm/memories/mem-e943b58115a02b4c-593.md +8 -0
  113. package/.gm/memories/mem-e9bc5f9ed6edbcf7-1580.md +8 -0
  114. package/.gm/memories/mem-eae1e383927854fb-2649.md +8 -0
  115. package/.gm/memories/mem-ebbe6c0694c526f0-1392.md +8 -0
  116. package/.gm/memories/mem-ebf85becc23410b0-1633.md +8 -0
  117. package/.gm/memories/mem-ee0596fa17c7102e-362.md +10 -0
  118. package/.gm/memories/mem-ee4450fbc311b583-1361.md +8 -0
  119. package/.gm/memories/mem-ee8858162518f144-1252.md +8 -0
  120. package/.gm/memories/mem-f3297e82df622649-1166.md +8 -0
  121. package/.gm/memories/mem-f3f317c083d979ee-1210.md +8 -0
  122. package/.gm/memories/mem-f714a86465fba158-2041.md +8 -0
  123. package/.gm/memories/mem-f786bc1626d92845-204.md +10 -0
  124. package/.gm/memories/mem-f9f0dc8895b21e18-1217.md +8 -0
  125. package/.gm/memories/mem-fb3b2a7395800041-525.md +8 -0
  126. package/.gm/memories/mem-fc5753f92ee7d654-966.md +8 -0
  127. package/.gm/memories/mem-fe3faa1652f07239-480.md +8 -0
  128. package/.gm/memories/mem-ff8669e527e5c427-2788.md +8 -0
  129. package/.gm/mutables.yml +53 -0
  130. package/.gm/prd.yml +3342 -1
  131. package/AGENTS.md +122 -122
  132. package/UX_OPTIMIZATION_SUMMARY.md +238 -0
  133. package/agentgui-after.png +0 -0
  134. package/agentgui-current.png +0 -0
  135. package/agentgui-final.png +0 -0
  136. package/agentgui-nobrand.png +0 -0
  137. package/agentgui-now.png +0 -0
  138. package/agentgui-v2.png +0 -0
  139. package/agentgui-v4.png +0 -0
  140. package/bash.exe.stackdump +28 -0
  141. package/database.js +110 -109
  142. package/design-reference.png +0 -0
  143. package/lib/acp-sdk-manager.js +9 -42
  144. package/lib/claude-runner-acp.js +155 -213
  145. package/lib/claude-runner-agents.js +157 -170
  146. package/lib/http-handler.js +931 -400
  147. package/lib/ws-handlers-util.js +466 -11
  148. package/package.json +1 -1
  149. package/site/app/js/app.js +4741 -4221
  150. package/site/app/js/backend.js +602 -621
  151. package/site/app/vendor/anentrypoint-design/247420.css +13673 -14799
  152. package/site/app/vendor/anentrypoint-design/247420.js +449 -735
  153. package/.gm/.embed-generation.code_chunks +0 -1
  154. package/.gm/.embed-generation.git_commit_vectors +0 -1
  155. package/.gm/.embed-generation.memories +0 -1
  156. package/.gm/.embed-generation.rssearch_vectors +0 -1
  157. package/lib/acp-http-protocol.js +0 -95
  158. package/lib/http-routes/mutations.js +0 -199
  159. package/lib/http-routes/reads.js +0 -212
  160. package/lib/http-routes/shared.js +0 -225
  161. package/lib/ws-handlers/agents.js +0 -137
  162. package/lib/ws-handlers/chat.js +0 -157
  163. package/lib/ws-handlers/git.js +0 -159
  164. package/lib/ws-handlers/misc.js +0 -36
  165. package/lib/ws-handlers/shared.js +0 -47
  166. package/lib/ws-handlers/terminal-state.js +0 -16
  167. package/site/app/js/chat-persistence.js +0 -123
  168. package/site/app/js/hash-routing.js +0 -62
  169. package/site/app/js/history.js +0 -435
  170. package/site/app/js/shortcuts.js +0 -128
@@ -1,225 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import os from 'os';
4
- import crypto from 'crypto';
5
-
6
- // Module-level platform constant — never changes at runtime.
7
- export const IS_WINDOWS = os.platform() === 'win32';
8
-
9
- // Confine a requested filesystem path to an allowlist of roots. Two layers:
10
- // 1. normalize + resolved-prefix check on the LEXICAL path (blocks `../`
11
- // traversal, which path.normalize collapses so a literal `..` test is a
12
- // no-op);
13
- // 2. fs.realpathSync + the SAME prefix check on the REAL path, so a symlink
14
- // that lives inside an allowed root but points outside it cannot escape
15
- // (the lexical path passes layer 1, the resolved target fails layer 2).
16
- // Returns { ok, realPath, reason }. realPath is the symlink-resolved absolute
17
- // path to stat/read; callers use it, never the raw input. A non-existent path
18
- // has no realpath yet, so it fails closed with reason 'not found'.
19
- export function confineToRoots(inputPath, allowRoots) {
20
- const norms = allowRoots.map(r => path.normalize(r));
21
- const expanded = inputPath && inputPath.startsWith('~') ? inputPath.replace('~', os.homedir()) : inputPath;
22
- const normalizedPath = path.normalize(expanded || '');
23
- const isAbsolute = IS_WINDOWS ? /^[A-Za-z]:[\\/]/.test(normalizedPath) : normalizedPath.startsWith('/');
24
- const within = (p) => {
25
- const np = IS_WINDOWS ? p.toLowerCase() : p;
26
- return norms.some(root => {
27
- const r = IS_WINDOWS ? root.toLowerCase() : root;
28
- return np === r || np.startsWith(r + path.sep);
29
- });
30
- };
31
- if (!isAbsolute || !within(normalizedPath)) return { ok: false, reason: 'path outside allowed roots' };
32
- // realpath the target and re-confine, defeating symlink escape (TOCTOU/link
33
- // traversal). If realpath throws (missing path / broken link) fail closed.
34
- let realPath;
35
- try { realPath = fs.realpathSync(normalizedPath); }
36
- catch (e) { return { ok: false, reason: e && e.code === 'ENOENT' ? 'not found' : 'cannot resolve path', code: e && e.code }; }
37
- if (!within(realPath)) return { ok: false, reason: 'symlink target outside allowed roots' };
38
- return { ok: true, realPath };
39
- }
40
-
41
- // Mask ?token=VALUE in a URL string before logging so credentials never
42
- // appear in server logs or error messages.
43
- export function maskToken(url) {
44
- if (typeof url !== 'string') return url;
45
- return url.replace(/([?&]token=)[^&]*/gi, '$1***');
46
- }
47
-
48
- // Secret-bearing basenames that must never be readable through ANY confined
49
- // raw-bytes route (preview or download), even when they sit inside an allowed
50
- // root: dotfiles, env/key/cert material, and credential stores. One const so
51
- // /api/file and /api/download can never drift apart.
52
- export const SECRET_RE = /(^\.|\.(env|pem|key|crt|p12|pfx)$|secret|credential|\.npmrc$|\.netrc$)/i;
53
-
54
- // The allowlist the Files surface operates within: server cwd + Claude
55
- // projects dir, widened via FS_ROOTS (path-separated). One construction so
56
- // /api/list,file,download and the mutation routes can never drift apart.
57
- export function fsAllowRoots() {
58
- const roots = [
59
- process.env.CLAUDE_PROJECTS_DIR || path.join(os.homedir(), '.claude', 'projects'),
60
- ];
61
- // The server cwd is only exposed when PASSWORD is set (the witnessed
62
- // localhost-PASSWORD deploy lists the repo tree) or FS_ALLOW_CWD=1 is opted
63
- // in. An open no-PASSWORD deploy must NOT expose the whole server tree.
64
- if (process.env.PASSWORD || process.env.FS_ALLOW_CWD === '1') {
65
- roots.push(process.env.STARTUP_CWD || process.cwd());
66
- }
67
- if (process.env.FS_ROOTS) roots.push(...process.env.FS_ROOTS.split(path.delimiter));
68
- return roots.map(r => path.normalize(r));
69
- }
70
-
71
- // A new file/dir name must be a single path component: no separators, no
72
- // traversal, no NTFS alternate-data-stream colon, no reserved Windows device
73
- // names, no trailing dot/space (Windows silently strips them, aliasing two
74
- // names onto one entry). Returns the trimmed name or null when inadmissible.
75
- export function sanitizeEntryName(name) {
76
- if (typeof name !== 'string') return null;
77
- const n = name.trim();
78
- if (!n || n.length > 255) return null;
79
- if (/[\\/:*?"<>|\x00-\x1f]/.test(n)) return null;
80
- if (n === '.' || n === '..') return null;
81
- if (/[. ]$/.test(n)) return null;
82
- if (/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/i.test(n)) return null;
83
- return n;
84
- }
85
-
86
- // Every confined filesystem route accepts its target path via a `?path=`/
87
- // `?dir=` query param, preferred over a path SEGMENT. A reverse proxy's
88
- // proxy_pass URI normalization can decode-then-reencode the request path,
89
- // collapsing an encoded %2F segment slash back into a literal '/' before
90
- // forwarding - the app then sees extra path segments instead of one opaque
91
- // one, strips what it thinks is a leading '/', and silently turns an absolute
92
- // path into a relative one that fails confinement even for genuinely
93
- // accessible directories/files. A query param is untouched by that
94
- // normalization on any proxy in front of this app. `legacyPrefix` is the old
95
- // `/api/xxx/<path>` route prefix, still accepted for any caller not yet
96
- // updated to the query-param form.
97
- export function resolveConfinedPath(req, routePath, queryKey, legacyPrefix) {
98
- const url = new URL(req.url, 'http://x');
99
- const qVal = url.searchParams.get(queryKey);
100
- if (qVal != null) return qVal;
101
- const raw = routePath.split('?')[0].slice(legacyPrefix.length).replace(/^\//, '');
102
- return raw ? decodeURIComponent(raw) : '';
103
- }
104
-
105
- // Map a Node.js filesystem error code to a safe human-readable string that
106
- // does not disclose host paths or internal stack context. Used everywhere an
107
- // err.message would otherwise be returned to the client.
108
- export function safeErrMsg(err) {
109
- if (!err) return 'unknown error';
110
- switch (err.code) {
111
- case 'ENOENT': return 'file not found';
112
- case 'EACCES': return 'permission denied';
113
- case 'EPERM': return 'operation not permitted';
114
- case 'EISDIR': return 'path is a directory';
115
- case 'ENOTDIR': return 'path is not a directory';
116
- case 'ENOTEMPTY': return 'directory is not empty';
117
- case 'EEXIST': return 'file already exists';
118
- case 'EXDEV': return 'cannot move across drives';
119
- case 'EMFILE': return 'too many open files';
120
- case 'ENOSPC': return 'no space left on device';
121
- default: return 'operation failed';
122
- }
123
- }
124
-
125
- // Read a request body with a hard size cap; resolves a Buffer or rejects with
126
- // .code='TOO_LARGE' so the caller can answer 413 without buffering the rest.
127
- // Stops accumulating further chunks (req.pause()) but does NOT destroy the
128
- // socket - a destroyed request cannot carry the caller's 413 response back to
129
- // the client, which instead sees a raw ECONNRESET with zero information
130
- // (live-witnessed: fetch() reported only "fetch failed", no status code).
131
- // The still-open connection is closed normally once the 413 response is sent.
132
- export function readBody(req, maxBytes) {
133
- return new Promise((resolve, reject) => {
134
- const chunks = []; let total = 0; let tooLarge = false;
135
- req.on('data', (c) => {
136
- if (tooLarge) return;
137
- total += c.length;
138
- if (total > maxBytes) {
139
- tooLarge = true;
140
- req.pause();
141
- const e = new Error('body too large'); e.code = 'TOO_LARGE';
142
- reject(e);
143
- return;
144
- }
145
- chunks.push(c);
146
- });
147
- req.on('end', () => { if (!tooLarge) resolve(Buffer.concat(chunks)); });
148
- req.on('error', reject);
149
- });
150
- }
151
-
152
- // True when the resolved path IS one of the allowlist roots - the roots
153
- // themselves are never mutation targets (rename/delete of a root would orphan
154
- // the whole surface).
155
- export function isAllowRoot(realPath, allowRoots) {
156
- const p = IS_WINDOWS ? realPath.toLowerCase() : realPath;
157
- return allowRoots.some(r => (IS_WINDOWS ? r.toLowerCase() : r) === p);
158
- }
159
-
160
- // --- Soft-delete (trash) -----------------------------------------------
161
- // /api/delete moves entries into <root>/.agentgui-trash/<trashId>__<name>
162
- // instead of unlinking, giving a short undo window. In-memory index only
163
- // (server restart forfeits the undo window - acceptable since the retention
164
- // window itself is short and this is a convenience net on top of, not a
165
- // replacement for, the pre-delete ConfirmDialog). Purged after
166
- // TRASH_RETENTION_MS or when trashIndex grows past TRASH_MAX_ENTRIES
167
- // (oldest-first), so a long-running server's trash dir can't grow unbounded.
168
- const TRASH_DIR_NAME = '.agentgui-trash';
169
- const TRASH_RETENTION_MS = parseInt(process.env.AGENTGUI_TRASH_RETENTION_MS || '', 10) || 10 * 60 * 1000;
170
- const TRASH_MAX_ENTRIES = 200;
171
- const trashIndex = new Map(); // trashId -> { trashPath, originalPath, root, deletedAt }
172
-
173
- function purgeExpiredTrash() {
174
- const now = Date.now();
175
- for (const [id, info] of trashIndex) {
176
- if (now - info.deletedAt > TRASH_RETENTION_MS) {
177
- try { fs.rmSync(info.trashPath, { recursive: true, force: true }); } catch { /* already gone */ }
178
- trashIndex.delete(id);
179
- }
180
- }
181
- if (trashIndex.size > TRASH_MAX_ENTRIES) {
182
- const sorted = [...trashIndex.entries()].sort((a, b) => a[1].deletedAt - b[1].deletedAt);
183
- for (const [id, info] of sorted.slice(0, trashIndex.size - TRASH_MAX_ENTRIES)) {
184
- try { fs.rmSync(info.trashPath, { recursive: true, force: true }); } catch {}
185
- trashIndex.delete(id);
186
- }
187
- }
188
- }
189
-
190
- // Which allowed root a confined realPath lives under - the trash dir sits
191
- // alongside it (still inside the SAME root, so confineToRoots covers the
192
- // trash path too - no new unconfined surface).
193
- function rootFor(realPath, allowRoots) {
194
- const p = IS_WINDOWS ? realPath.toLowerCase() : realPath;
195
- return allowRoots.find(r => { const rr = IS_WINDOWS ? r.toLowerCase() : r; return p === rr || p.startsWith(rr + path.sep); });
196
- }
197
-
198
- export function moveToTrash(realPath, allowRoots) {
199
- purgeExpiredTrash();
200
- const root = rootFor(realPath, allowRoots);
201
- if (!root) { const e = new Error('not confined to an allowed root'); e.code = 'EACCES'; throw e; }
202
- const trashDir = path.join(root, TRASH_DIR_NAME);
203
- fs.mkdirSync(trashDir, { recursive: true });
204
- const trashId = crypto.randomBytes(8).toString('hex');
205
- const base = path.basename(realPath);
206
- const trashPath = path.join(trashDir, trashId + '__' + base);
207
- fs.renameSync(realPath, trashPath);
208
- trashIndex.set(trashId, { trashPath, originalPath: realPath, root, deletedAt: Date.now() });
209
- return { trashId };
210
- }
211
-
212
- export function restoreFromTrash(trashId, allowRoots) {
213
- const info = trashIndex.get(trashId);
214
- if (!info) { const e = new Error('nothing to restore - the undo window has expired or this was already restored'); e.code = 'NOT_FOUND'; throw e; }
215
- // Re-confine the ORIGINAL path at restore time (not trust the cached one
216
- // blindly) - the allowlist itself doesn't change at runtime, but this keeps
217
- // restore honoring the exact same confinement contract every other route does.
218
- const conf = confineToRoots(info.originalPath, allowRoots);
219
- if (!conf.ok && conf.reason !== 'not found') { const e = new Error('restore target is no longer inside an accessible folder'); e.code = 'CONFLICT'; throw e; }
220
- if (fs.existsSync(info.originalPath)) { const e = new Error('a file already exists at the original location'); e.code = 'CONFLICT'; throw e; }
221
- fs.mkdirSync(path.dirname(info.originalPath), { recursive: true });
222
- fs.renameSync(info.trashPath, info.originalPath);
223
- trashIndex.delete(trashId);
224
- return { path: info.originalPath };
225
- }
@@ -1,137 +0,0 @@
1
- import { registry } from '../claude-runner-agents.js';
2
- import { restart as restartAcpAgent } from '../acp-sdk-manager.js';
3
- import { err } from './shared.js';
4
- import { spawnSync } from 'child_process';
5
-
6
- const SUB_AGENT_MAP = {
7
- 'opencode': [{ id: 'gm-oc', name: 'GM OpenCode' }], 'cli-opencode': [{ id: 'gm-oc', name: 'GM OpenCode' }],
8
- 'gemini': [{ id: 'gm-gc', name: 'GM Gemini' }], 'cli-gemini': [{ id: 'gm-gc', name: 'GM Gemini' }],
9
- 'kilo': [{ id: 'gm-kilo', name: 'GM Kilo' }], 'cli-kilo': [{ id: 'gm-kilo', name: 'GM Kilo' }],
10
- 'codex': [], 'cli-codex': []
11
- };
12
-
13
- export function register(router, deps) {
14
- const { getProviderConfigs, getModelsForAgent } = deps;
15
-
16
- // --- agents.list: enumerate registered ACP agents + claude-code ---
17
- router.handle('agents.list', () => {
18
- const agents = registry.list().map(a => ({
19
- id: a.id,
20
- name: a.name,
21
- protocol: a.protocol,
22
- supportsStdin: !!a.supportsStdin,
23
- features: a.supportedFeatures || [],
24
- available: registry.isAvailable(a.id),
25
- npxInstallable: !!a.npxPackage,
26
- // The CLI binary name a manual (non-npx) install would need - lets the
27
- // settings panel say what to install instead of just "not installed".
28
- cmd: a.command || null,
29
- }));
30
- return { agents };
31
- });
32
-
33
- // --- agents.models: model choices for a given agent ---
34
- router.handle('agents.models', async (p) => {
35
- const id = p?.id || p?.agentId;
36
- if (!id) err(400, 'agent id required');
37
- if (id === 'claude-code') {
38
- return { models: [
39
- { id: 'sonnet', name: 'Claude Sonnet (latest)' },
40
- { id: 'opus', name: 'Claude Opus (latest)' },
41
- { id: 'haiku', name: 'Claude Haiku (latest)' },
42
- ] };
43
- }
44
- // Other agents: discover their models via getModelsForAgent (queries the
45
- // running ACP server). Fail closed to an empty list on any error or when
46
- // the dep isn't a function.
47
- if (typeof getModelsForAgent === 'function') {
48
- try {
49
- const raw = await getModelsForAgent(id);
50
- const list = Array.isArray(raw) ? raw : (raw?.models || []);
51
- return { models: list.map(m => ({ id: m.id, name: m.name || m.label || m.id })) };
52
- } catch { return { models: [] }; }
53
- }
54
- return { models: [] };
55
- });
56
-
57
- router.handle('acp.restart', async (p) => {
58
- if (!p.id) err(400, 'Missing agent id');
59
- const ok = await restartAcpAgent(p.id);
60
- return { ok: !!ok };
61
- });
62
-
63
- router.handle('agent.subagents', async (p) => {
64
- if (!p.id) err(400, 'Missing agent id');
65
- if (p.id === 'claude-code' || p.id === 'cli-claude') {
66
- const spawnEnv = { ...process.env }; delete spawnEnv.CLAUDECODE;
67
- const result = spawnSync('claude', ['agents', 'list'], { encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'], env: spawnEnv });
68
- if (result.status !== 0 || !result.stdout) return { subAgents: [] };
69
- const agents = result.stdout.trim().split('\n').filter(l => l.trim()).map(l => l.match(/^ (\S+)\s+·/)).filter(Boolean).map(m => ({ id: m[1], name: m[1] }));
70
- return { subAgents: agents };
71
- }
72
- return { subAgents: SUB_AGENT_MAP[p.id] || [] };
73
- });
74
-
75
- // --- models.availability: composes agentgui's REAL model surface into the
76
- // ModelsConfig component's shape (design SDK's ModelsConfig.js). agentgui
77
- // has no freddie-style probed-availability matrix (no /v1/models round-trip
78
- // per mode) - what it actually has is: discovered agent CLIs (registry,
79
- // same data agents.list already exposes), each agent's model choices
80
- // (agents.models' getModelsForAgent), and per-provider API-key presence
81
- // (auth.configs' getProviderConfigs). This handler is a read-only
82
- // composition of those three real sources, not a new probe and not fake
83
- // data - "usable_in_any_mode" is derived from registry.isAvailable(agentId)
84
- // (the CLI binary was actually found on this server), and "key_present"
85
- // comes straight from getProviderConfigs' filesystem check.
86
- router.handle('models.availability', async () => {
87
- const agents = registry.list();
88
- const providerConfigs = typeof getProviderConfigs === 'function' ? getProviderConfigs() : {};
89
- const providers = [];
90
- for (const a of agents) {
91
- const available = registry.isAvailable(a.id);
92
- let models = [];
93
- if (a.id === 'claude-code') {
94
- models = [
95
- { id: 'sonnet', name: 'Claude Sonnet (latest)' },
96
- { id: 'opus', name: 'Claude Opus (latest)' },
97
- { id: 'haiku', name: 'Claude Haiku (latest)' },
98
- ];
99
- } else if (typeof getModelsForAgent === 'function') {
100
- try {
101
- const raw = await getModelsForAgent(a.id);
102
- const list = Array.isArray(raw) ? raw : (raw?.models || []);
103
- models = list.map((m) => ({ id: m.id, name: m.name || m.label || m.id }));
104
- } catch { models = []; }
105
- }
106
- // Map onto agentgui's one real signal per model: is the agent CLI itself
107
- // available on this server (registry.isAvailable). agentgui does not
108
- // probe individual (model, mode) cells the way freddie's matrix does,
109
- // so every model under an available agent reports the same single
110
- // 'cli' mode rather than fabricating per-mode probe results.
111
- providers.push({
112
- id: a.id,
113
- key_present: !!(providerConfigs[a.id] && providerConfigs[a.id].hasKey) || available,
114
- discovery_error: available ? null : ((a.name || a.id) + ' CLI not found on this server'),
115
- models: models.map((m) => ({
116
- id: m.id,
117
- name: m.name,
118
- discovered_via: a.protocol || 'cli',
119
- modes: { cli: { ok: !!available, skipped: !available, reason: available ? undefined : 'agent_not_installed' } },
120
- usable_in_any_mode: !!available,
121
- })),
122
- });
123
- }
124
- const totalModels = providers.reduce((n, p) => n + p.models.length, 0);
125
- const usableModels = providers.reduce((n, p) => n + p.models.filter((m) => m.usable_in_any_mode).length, 0);
126
- return {
127
- timestamp: new Date().toISOString(),
128
- providers,
129
- sampler: [],
130
- summary: {
131
- total_providers: providers.length,
132
- total_models: totalModels,
133
- usable_in_any_mode: usableModels,
134
- },
135
- };
136
- });
137
- }
@@ -1,157 +0,0 @@
1
- import crypto from 'crypto';
2
- import { runClaudeWithStreaming } from '../claude-runner-run.js';
3
- import { registry } from '../claude-runner-agents.js';
4
- import { confineToRoots, fsAllowRoots } from '../http-handler.js';
5
- import { err } from './shared.js';
6
- import { recordTerminal, getTerminal } from './terminal-state.js';
7
-
8
- export function register(router, deps) {
9
- const { wsOptimizer, broadcastSync, STARTUP_CWD, subscriptionIndex, activeChats } = deps;
10
-
11
- // --- conversation.subscribe: register this ws for sessionId broadcasts ---
12
- router.handle('conversation.subscribe', (p, ws) => {
13
- const sid = p?.sessionId;
14
- if (!sid || typeof sid !== 'string') err(400, 'sessionId required');
15
- if (!subscriptionIndex.has(sid)) subscriptionIndex.set(sid, new Set());
16
- subscriptionIndex.get(sid).add(ws);
17
- ws.subscriptions = ws.subscriptions || new Set();
18
- ws.subscriptions.add(sid);
19
- // Replay a buffered terminal frame (complete/error/cancelled) so a client
20
- // that re-subscribes after a ws drop learns the turn already ended.
21
- const term = getTerminal(sid);
22
- if (term) { try { wsOptimizer.sendToClient(ws, { ...term, replayed: true }); } catch {} }
23
- return { subscribed: true, sessionId: sid, replayedTerminal: !!term };
24
- });
25
-
26
- // --- chat.sendMessage: start a one-shot streaming chat with an agent.
27
- // Bypasses the gutted db-queries layer entirely; calls runClaudeWithStreaming
28
- // directly and broadcasts streaming_* events scoped to an ephemeral sessionId.
29
- router.handle('chat.sendMessage', async (p, ws) => {
30
- let content = (p?.content || '').toString();
31
- if (!content) err(400, 'content required');
32
- const agentId = p?.agentId || 'claude-code';
33
- // For non-resume agents (not claude-code which uses --resume), prepend prior
34
- // conversation turns so the agent has context. claude-code handles multi-turn
35
- // natively via resumeSessionId; direct runners (agy, etc.) get a preamble.
36
- const priorMessages = Array.isArray(p?.messages) ? p.messages.filter(m => m?.role && m?.content) : [];
37
- if (agentId !== 'claude-code' && !p?.resumeSid && !p?.resumeSessionId && priorMessages.length > 0) {
38
- const preamble = priorMessages.map(m => (m.role === 'user' ? 'User: ' : 'Assistant: ') + (m.content || '').trim()).join('\n\n');
39
- content = '[Prior conversation]\n' + preamble + '\n\n[Current message]\n' + content;
40
- }
41
- const model = p?.model || undefined;
42
- const subAgent = p?.subAgent || undefined;
43
- const cwd = p?.cwd || STARTUP_CWD;
44
- const resumeSessionId = p?.resumeSid || p?.resumeSessionId || undefined;
45
- if (!registry.has(agentId)) err(404, `Unknown agentId: ${agentId}`);
46
- // A client-supplied cwd must be confined to the SAME allowlist as the Files
47
- // routes (fsAllowRoots) - an unconfined cwd would let a client spawn the
48
- // agent CLI anywhere on disk. Use the realpath-resolved value as the spawn
49
- // cwd (defeats symlink escape, same as the HTTP file routes). No p.cwd ->
50
- // STARTUP_CWD, which is itself an allowed root, so it needs no check.
51
- let spawnCwd = STARTUP_CWD;
52
- if (p?.cwd) {
53
- const conf = confineToRoots(cwd, fsAllowRoots());
54
- if (!conf.ok) err(conf.reason === 'not found' ? 400 : 403, `cwd outside allowed roots: ${cwd}`);
55
- spawnCwd = conf.realPath;
56
- }
57
-
58
- const sessionId = 'chat-' + crypto.randomBytes(8).toString('hex');
59
- // Auto-subscribe the originating ws so it receives its own broadcasts.
60
- if (!subscriptionIndex.has(sessionId)) subscriptionIndex.set(sessionId, new Set());
61
- subscriptionIndex.get(sessionId).add(ws);
62
- ws.subscriptions = ws.subscriptions || new Set();
63
- ws.subscriptions.add(sessionId);
64
-
65
- const ctrl = { aborted: false, proc: null, agentId, model, cwd: spawnCwd, startedAt: Date.now() };
66
- activeChats.set(sessionId, ctrl);
67
- // Push-driven hint so clients refresh active-session state without
68
- // waiting for the 3s chat.active poll (finding 48).
69
- broadcastSync({ type: 'chat_active_changed', reason: 'started', sessionId, agentId, timestamp: Date.now() });
70
-
71
- // Fire-and-forget. Errors broadcast as streaming_error.
72
- (async () => {
73
- let eventCount = 0;
74
- broadcastSync({ type: 'streaming_start', sessionId, agentId, timestamp: Date.now() });
75
- let claudeSessionBroadcast = false;
76
- const onEvent = (parsed) => {
77
- eventCount++;
78
- // Surface claude's REAL session id (from the stream) once, so the client
79
- // can --resume this conversation on its next turn. The ephemeral
80
- // 'chat-...' sessionId is not a claude session id and cannot be resumed.
81
- if (!claudeSessionBroadcast && parsed?.session_id) {
82
- claudeSessionBroadcast = true;
83
- ctrl.claudeSessionId = parsed.session_id;
84
- broadcastSync({ type: 'streaming_session', sessionId, claudeSessionId: parsed.session_id, agentId, timestamp: Date.now() });
85
- }
86
- if (parsed?.type === 'assistant' && parsed.message?.content) {
87
- for (const block of parsed.message.content) {
88
- broadcastSync({ type: 'streaming_progress', sessionId, block, blockRole: 'assistant', seq: eventCount, timestamp: Date.now() });
89
- }
90
- } else if (parsed?.type === 'user' && parsed.message?.content) {
91
- const blocks = Array.isArray(parsed.message.content) ? parsed.message.content : [];
92
- for (const block of blocks) {
93
- if (block?.type === 'tool_result') {
94
- broadcastSync({ type: 'streaming_progress', sessionId, block, blockRole: 'tool_result', seq: eventCount, timestamp: Date.now() });
95
- }
96
- }
97
- } else if (parsed?.type === 'result') {
98
- const block = { type: 'result', result: parsed.result, subtype: parsed.subtype, duration_ms: parsed.duration_ms, total_cost_usd: parsed.total_cost_usd, is_error: !!parsed.is_error };
99
- broadcastSync({ type: 'streaming_progress', sessionId, block, blockRole: 'result', seq: eventCount, isResult: true, timestamp: Date.now() });
100
- }
101
- };
102
- try {
103
- const config = {
104
- verbose: true, outputFormat: 'stream-json', timeout: 1800000, print: true,
105
- model, subAgent, onEvent, resumeSessionId,
106
- onPid: () => {}, onProcess: (proc) => { ctrl.proc = proc; },
107
- };
108
- await runClaudeWithStreaming(content, spawnCwd, agentId, config);
109
- if (!ctrl.aborted) {
110
- const ev = { type: 'streaming_complete', sessionId, claudeSessionId: ctrl.claudeSessionId || null, agentId, eventCount, timestamp: Date.now() };
111
- recordTerminal(sessionId, ev);
112
- broadcastSync(ev);
113
- }
114
- } catch (e) {
115
- if (!ctrl.aborted) {
116
- const ev = { type: 'streaming_error', sessionId, claudeSessionId: ctrl.claudeSessionId || null, agentId, error: e.message || String(e), recoverable: false, timestamp: Date.now() };
117
- recordTerminal(sessionId, ev);
118
- broadcastSync(ev);
119
- }
120
- } finally {
121
- activeChats.delete(sessionId);
122
- broadcastSync({ type: 'chat_active_changed', reason: 'ended', sessionId, agentId, timestamp: Date.now() });
123
- }
124
- })();
125
-
126
- return { sessionId, started: true };
127
- });
128
-
129
- // --- chat.active: list in-flight chats started via this server ---
130
- router.handle('chat.active', () => {
131
- const sessions = [];
132
- for (const [sid, c] of activeChats) {
133
- sessions.push({ sessionId: sid, claudeSessionId: c.claudeSessionId || null, agentId: c.agentId || null, model: c.model || null, cwd: c.cwd || null, startedAt: c.startedAt || null, pid: c.proc?.pid || null });
134
- }
135
- return { sessions };
136
- });
137
-
138
- // --- chat.cancel: abort an in-flight chat ---
139
- router.handle('chat.cancel', (p) => {
140
- const sid = p?.sessionId;
141
- if (!sid) err(400, 'sessionId required');
142
- const ctrl = activeChats.get(sid);
143
- if (!ctrl) return { cancelled: false, reason: 'not-found' };
144
- ctrl.aborted = true;
145
- // Broadcast the terminal 'cancelled' frame BEFORE killing the proc so a
146
- // remote cancellation (other tab, dashboard stop-all) does not read as a
147
- // normal completion (finding 44). streaming_cancelled is in BROADCAST_TYPES
148
- // so every connected client sees it; it is also buffered for re-subscribers.
149
- const ev = { type: 'streaming_cancelled', sessionId: sid, claudeSessionId: ctrl.claudeSessionId || null, agentId: ctrl.agentId || null, cancelled: true, timestamp: Date.now() };
150
- recordTerminal(sid, ev);
151
- broadcastSync(ev);
152
- try { ctrl.proc?.kill?.(); } catch {}
153
- activeChats.delete(sid);
154
- broadcastSync({ type: 'chat_active_changed', reason: 'cancelled', sessionId: sid, agentId: ctrl.agentId || null, timestamp: Date.now() });
155
- return { cancelled: true };
156
- });
157
- }