janissary 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/LICENSE +75 -0
  2. package/README.md +28 -0
  3. package/agent-names.json +52 -0
  4. package/bin/janus.mjs +27 -0
  5. package/chrome-extension/_metadata/generated_indexed_rulesets/_ruleset1 +0 -0
  6. package/chrome-extension/content-script.js +62 -0
  7. package/chrome-extension/manifest.json +25 -0
  8. package/chrome-extension/rules.json +16 -0
  9. package/dist/acp/index.js +93 -0
  10. package/dist/acp/loop.js +75 -0
  11. package/dist/acp/manager.js +126 -0
  12. package/dist/acp/rate-limit.js +8 -0
  13. package/dist/acp/runner.js +18 -0
  14. package/dist/acp/tools.js +27 -0
  15. package/dist/acp-loop.js +75 -0
  16. package/dist/acp-manager.js +109 -0
  17. package/dist/acp-runner.js +18 -0
  18. package/dist/acp.js +91 -0
  19. package/dist/agent/commands.js +29 -0
  20. package/dist/agent/communication-manager.js +71 -0
  21. package/dist/agent/state.js +58 -0
  22. package/dist/agent-commands.js +29 -0
  23. package/dist/agent-communication-manager.js +71 -0
  24. package/dist/agent-state.js +58 -0
  25. package/dist/app-themes.js +13 -0
  26. package/dist/browser/command.js +104 -0
  27. package/dist/browser/index.js +124 -0
  28. package/dist/browser/tab-helpers.js +51 -0
  29. package/dist/browser/tab.js +99 -0
  30. package/dist/browser-command.js +104 -0
  31. package/dist/browser-tab-helpers.js +51 -0
  32. package/dist/browser-tab.js +99 -0
  33. package/dist/browser.js +124 -0
  34. package/dist/buffer.js +42 -0
  35. package/dist/bus.js +52 -0
  36. package/dist/capture/manager.js +48 -0
  37. package/dist/capture/router.js +18 -0
  38. package/dist/capture-manager.js +48 -0
  39. package/dist/capture-router.js +20 -0
  40. package/dist/chrome-extension-loader.js +62 -0
  41. package/dist/cli-args.js +80 -0
  42. package/dist/command-manager.js +126 -0
  43. package/dist/command-router.js +14 -0
  44. package/dist/commands/acp-reset.js +13 -0
  45. package/dist/commands/acp.js +5 -0
  46. package/dist/commands/agent.js +5 -0
  47. package/dist/commands/broadcast.js +22 -0
  48. package/dist/commands/browser.js +5 -0
  49. package/dist/commands/clear.js +5 -0
  50. package/dist/commands/close.js +42 -0
  51. package/dist/commands/connection.js +5 -0
  52. package/dist/commands/db.js +5 -0
  53. package/dist/commands/edit.js +18 -0
  54. package/dist/commands/files.js +13 -0
  55. package/dist/commands/hist.js +7 -0
  56. package/dist/commands/index.js +62 -0
  57. package/dist/commands/monitor.js +61 -0
  58. package/dist/commands/msg.js +17 -0
  59. package/dist/commands/next.js +7 -0
  60. package/dist/commands/notifications.js +16 -0
  61. package/dist/commands/notify.js +20 -0
  62. package/dist/commands/open.js +41 -0
  63. package/dist/commands/profile.js +5 -0
  64. package/dist/commands/queue.js +37 -0
  65. package/dist/commands/quit.js +6 -0
  66. package/dist/commands/rename.js +12 -0
  67. package/dist/commands/resolve-target.js +12 -0
  68. package/dist/commands/schedule.js +74 -0
  69. package/dist/commands/search.js +26 -0
  70. package/dist/commands/send.js +51 -0
  71. package/dist/commands/state.js +9 -0
  72. package/dist/commands/syntax.js +36 -0
  73. package/dist/commands/tasks.js +7 -0
  74. package/dist/commands/theme.js +41 -0
  75. package/dist/commands/types.js +1 -0
  76. package/dist/commands.js +70 -0
  77. package/dist/completion/browser.js +24 -0
  78. package/dist/completion/fs.js +26 -0
  79. package/dist/completion/handlers.js +81 -0
  80. package/dist/completion/helpers.js +43 -0
  81. package/dist/completion/index.js +43 -0
  82. package/dist/completion-browser.js +24 -0
  83. package/dist/completion-fs.js +26 -0
  84. package/dist/completion-handlers.js +69 -0
  85. package/dist/completion-helpers.js +43 -0
  86. package/dist/completion.js +42 -0
  87. package/dist/config.js +66 -0
  88. package/dist/connection/close.js +41 -0
  89. package/dist/connection/manager.js +93 -0
  90. package/dist/connection/parsing.js +28 -0
  91. package/dist/connection-close.js +41 -0
  92. package/dist/connection-manager.js +93 -0
  93. package/dist/connection-parsing.js +28 -0
  94. package/dist/connections.js +75 -0
  95. package/dist/controller-events.js +34 -0
  96. package/dist/controller-file-tree.js +36 -0
  97. package/dist/controller.js +201 -0
  98. package/dist/database/index.js +83 -0
  99. package/dist/database/manager.js +67 -0
  100. package/dist/database/parsing.js +54 -0
  101. package/dist/database/query.js +41 -0
  102. package/dist/database-manager.js +67 -0
  103. package/dist/database-parsing.js +54 -0
  104. package/dist/database-query.js +41 -0
  105. package/dist/database.js +83 -0
  106. package/dist/datetime.js +15 -0
  107. package/dist/editor/save.js +27 -0
  108. package/dist/editor/sync.js +10 -0
  109. package/dist/editor/watch-manager.js +84 -0
  110. package/dist/editor-save.js +24 -0
  111. package/dist/editor-watch-manager.js +84 -0
  112. package/dist/file-tree-args.js +36 -0
  113. package/dist/file-tree-git-refresh.js +33 -0
  114. package/dist/file-tree-manager.js +252 -0
  115. package/dist/file-tree-moves.js +26 -0
  116. package/dist/file-tree-open.js +56 -0
  117. package/dist/file-tree-rebuild.js +22 -0
  118. package/dist/file-tree-watch.js +27 -0
  119. package/dist/file-tree.js +82 -0
  120. package/dist/git-status.js +64 -0
  121. package/dist/github-token.js +19 -0
  122. package/dist/global-history.js +48 -0
  123. package/dist/harness/auto-approve.js +79 -0
  124. package/dist/harness/busy-status.js +91 -0
  125. package/dist/harness/capture-file.js +28 -0
  126. package/dist/harness/index.js +87 -0
  127. package/dist/harness/manager.js +182 -0
  128. package/dist/harness/models.js +8 -0
  129. package/dist/harness/recorder.js +89 -0
  130. package/dist/harness/recording-file.js +25 -0
  131. package/dist/harness/screen.js +79 -0
  132. package/dist/harness-capture-file.js +28 -0
  133. package/dist/harness-manager.js +112 -0
  134. package/dist/harness-models.js +8 -0
  135. package/dist/harness-screen.js +72 -0
  136. package/dist/harness.js +54 -0
  137. package/dist/index.js +163 -0
  138. package/dist/instance-lock.js +34 -0
  139. package/dist/interactive.js +33 -0
  140. package/dist/main.js +203 -0
  141. package/dist/managers.js +1 -0
  142. package/dist/message-handler.js +134 -0
  143. package/dist/messaging.js +47 -0
  144. package/dist/monitor/acp.js +24 -0
  145. package/dist/monitor/ask.js +31 -0
  146. package/dist/monitor/context.js +27 -0
  147. package/dist/monitor/editor-feed.js +49 -0
  148. package/dist/monitor/feed-diff.js +22 -0
  149. package/dist/monitor/feeds.js +19 -0
  150. package/dist/monitor/framing.js +29 -0
  151. package/dist/monitor/harness-feed.js +21 -0
  152. package/dist/monitor/info.js +29 -0
  153. package/dist/monitor/manager.js +221 -0
  154. package/dist/monitor/page-feed.js +21 -0
  155. package/dist/monitor/parsing.js +82 -0
  156. package/dist/monitor/reply.js +11 -0
  157. package/dist/monitor/session.js +36 -0
  158. package/dist/monitor/stop.js +33 -0
  159. package/dist/monitor/suggestion.js +12 -0
  160. package/dist/monitor/targets.js +69 -0
  161. package/dist/monitor/window.js +109 -0
  162. package/dist/monitor-acp.js +22 -0
  163. package/dist/monitor-info.js +18 -0
  164. package/dist/monitor-manager.js +233 -0
  165. package/dist/monitor-parsing.js +68 -0
  166. package/dist/monitor-session.js +19 -0
  167. package/dist/monitor-targets.js +37 -0
  168. package/dist/monitor-window.js +81 -0
  169. package/dist/notifications-tab.js +32 -0
  170. package/dist/notifications.js +87 -0
  171. package/dist/open-file-manager.js +100 -0
  172. package/dist/openers/editor.js +41 -0
  173. package/dist/openers/external-viewer.js +11 -0
  174. package/dist/openers/image.js +27 -0
  175. package/dist/openers/index.js +16 -0
  176. package/dist/openers/markdown.js +20 -0
  177. package/dist/openers/os-open.js +24 -0
  178. package/dist/openers/page.js +51 -0
  179. package/dist/openers/size.js +13 -0
  180. package/dist/openers/types.js +1 -0
  181. package/dist/page/sync.js +10 -0
  182. package/dist/paths.js +45 -0
  183. package/dist/persona-parsing.js +49 -0
  184. package/dist/personas.js +44 -0
  185. package/dist/profile/agent-opener.js +154 -0
  186. package/dist/profile/files.js +17 -0
  187. package/dist/profile/manager.js +72 -0
  188. package/dist/profile/monitors.js +23 -0
  189. package/dist/profile/notifications.js +11 -0
  190. package/dist/profile-agent-opener.js +136 -0
  191. package/dist/profile-manager.js +72 -0
  192. package/dist/profiles.js +143 -0
  193. package/dist/protocol.js +1 -0
  194. package/dist/pseudoterminal-manager.js +106 -0
  195. package/dist/pty.js +41 -0
  196. package/dist/recognizers/acp.js +38 -0
  197. package/dist/recognizers/analyze.js +49 -0
  198. package/dist/recognizers/bash.js +51 -0
  199. package/dist/recognizers/db.js +26 -0
  200. package/dist/recognizers/index.js +1 -0
  201. package/dist/recognizers/lexicon.js +11 -0
  202. package/dist/recognizers/types.js +5 -0
  203. package/dist/resolve.js +39 -0
  204. package/dist/route-choice.js +11 -0
  205. package/dist/sandbox/index.js +217 -0
  206. package/dist/sandbox/paths.js +178 -0
  207. package/dist/sandbox/profile.js +161 -0
  208. package/dist/sandbox-paths.js +159 -0
  209. package/dist/sandbox-profile.js +161 -0
  210. package/dist/sandbox.js +217 -0
  211. package/dist/schedule/display.js +19 -0
  212. package/dist/schedule/helpers.js +76 -0
  213. package/dist/schedule/index.js +76 -0
  214. package/dist/schedule/manager.js +101 -0
  215. package/dist/schedule/parsing.js +63 -0
  216. package/dist/schedule/time.js +34 -0
  217. package/dist/schedule-display.js +19 -0
  218. package/dist/schedule-helpers.js +76 -0
  219. package/dist/schedule-manager.js +98 -0
  220. package/dist/schedule-parsing.js +63 -0
  221. package/dist/schedule-time.js +34 -0
  222. package/dist/schedule.js +76 -0
  223. package/dist/search-matches.js +40 -0
  224. package/dist/security.js +44 -0
  225. package/dist/shell-manager.js +139 -0
  226. package/dist/shell.js +59 -0
  227. package/dist/ssh-manager.js +42 -0
  228. package/dist/ssh.js +39 -0
  229. package/dist/startup-errors.js +21 -0
  230. package/dist/state-event.js +14 -0
  231. package/dist/state-format.js +38 -0
  232. package/dist/syntax-themes.js +14 -0
  233. package/dist/tab/agent-state.js +19 -0
  234. package/dist/tab/cleanup.js +33 -0
  235. package/dist/tab/colors.js +43 -0
  236. package/dist/tab/creators.js +95 -0
  237. package/dist/tab/dock.js +35 -0
  238. package/dist/tab/focus-history.js +41 -0
  239. package/dist/tab/formatting-handlers.js +77 -0
  240. package/dist/tab/formatting.js +21 -0
  241. package/dist/tab/history.js +14 -0
  242. package/dist/tab/index.js +65 -0
  243. package/dist/tab/manager.js +329 -0
  244. package/dist/tab/openers.js +35 -0
  245. package/dist/tab/queue.js +33 -0
  246. package/dist/tab/rehydrate.js +15 -0
  247. package/dist/tab/reorder.js +15 -0
  248. package/dist/tab/transcript.js +15 -0
  249. package/dist/tab/utils.js +55 -0
  250. package/dist/tab/view.js +42 -0
  251. package/dist/tab-cleanup.js +27 -0
  252. package/dist/tab-colors.js +43 -0
  253. package/dist/tab-creators.js +85 -0
  254. package/dist/tab-formatting-handlers.js +77 -0
  255. package/dist/tab-formatting.js +21 -0
  256. package/dist/tab-manager.js +418 -0
  257. package/dist/tab-utils.js +55 -0
  258. package/dist/tab.js +57 -0
  259. package/dist/tasks.js +32 -0
  260. package/dist/transcript/logger.js +36 -0
  261. package/dist/transcript/store.js +66 -0
  262. package/dist/types.js +6 -0
  263. package/dist/user-agent.js +65 -0
  264. package/dist/word-wrapping.js +42 -0
  265. package/dist/workspace-manager.js +40 -0
  266. package/dist/workspace.js +120 -0
  267. package/help.md +97 -0
  268. package/package.json +138 -0
@@ -0,0 +1,83 @@
1
+ import { databaseFileExists as databaseFileExists, listDatabaseFiles, removeDatabaseFile, getConnection, closeConnection, } from '../connections.js';
2
+ import { parseDatabaseCommand } from './parsing.js';
3
+ export { parseDatabaseCommand } from './parsing.js';
4
+ import { queryDatabase } from './query.js';
5
+ function errorMessage(error) {
6
+ return error instanceof Error ? error.message : String(error);
7
+ }
8
+ function createDatabase(name) {
9
+ const isExisted = databaseFileExists(name);
10
+ try {
11
+ getConnection(name);
12
+ return isExisted ? `Database "${name}" already exists.` : `Created sqlite database "${name}".`;
13
+ }
14
+ catch (error) {
15
+ return `Failed to create database "${name}": ${errorMessage(error)}`;
16
+ }
17
+ }
18
+ function deleteDatabase(name) {
19
+ closeConnection(name);
20
+ if (!databaseFileExists(name))
21
+ return `Database "${name}" does not exist.`;
22
+ try {
23
+ removeDatabaseFile(name);
24
+ return `Deleted sqlite database "${name}".`;
25
+ }
26
+ catch (error) {
27
+ return `Failed to delete database "${name}": ${errorMessage(error)}`;
28
+ }
29
+ }
30
+ function listDbs() {
31
+ const names = listDatabaseFiles();
32
+ return names.length > 0 ? names.join('\n') : 'No databases.';
33
+ }
34
+ /** Execute a `db ...` command and return the text to show in the transcript. */
35
+ export function runDatabaseCommand(input) {
36
+ const parsed = parseDatabaseCommand(input);
37
+ if ('error' in parsed)
38
+ return parsed.error;
39
+ switch (parsed.action) {
40
+ case 'create': {
41
+ return createDatabase(parsed.name);
42
+ }
43
+ case 'delete': {
44
+ return deleteDatabase(parsed.name);
45
+ }
46
+ case 'list': {
47
+ return listDbs();
48
+ }
49
+ case 'query': {
50
+ return queryDatabase(parsed.name, parsed.query);
51
+ }
52
+ }
53
+ }
54
+ // Primer injected into an ACP agent so it understands the `db` grammar and can
55
+ // drive an autonomous tool loop: it emits a command, the host runs it, and the
56
+ // output is fed back until the agent answers without a command.
57
+ export const DB_PRIMER = [
58
+ 'This host CLI can manage SQLite databases via `db` commands. Syntax:',
59
+ ' db sqlite create <name> # create an empty database',
60
+ ' db sqlite delete <name> # delete a database',
61
+ ' db sqlite query <name> <sql> # run SQL against a database',
62
+ ' db sqlite list # list databases',
63
+ 'Database names use letters, numbers, "-" and "_" only; the engine is always "sqlite".',
64
+ 'To inspect or change a database, end your reply with exactly one `db` command on its',
65
+ 'own final line (no code fence, nothing after it). The host runs it automatically and',
66
+ 'returns the output to you, so you can issue further commands. When the task is done,',
67
+ 'reply with the final answer and NO trailing `db` command.',
68
+ 'Be concise: do not explain what you are doing. Only output `db` commands and the final answer.',
69
+ ].join('\n');
70
+ /**
71
+ * Pull a proposed `db ...` command out of an agent reply, if present. Scans
72
+ * bottom-up (the primer asks for the command on the last line) and tolerates a
73
+ * surrounding code fence or a leading `$ `/`> ` prompt marker.
74
+ */
75
+ export function extractDatabaseCommand(text) {
76
+ const lines = text.split('\n');
77
+ for (let index = lines.length - 1; index >= 0; index--) {
78
+ const line = lines[index].replace(/^[\s`$>]+/, '').replace(/`+\s*$/, '').trim();
79
+ if (/^db\s+sqlite\s+(create|delete|query|list)\b/i.test(line))
80
+ return line;
81
+ }
82
+ // not a db command
83
+ }
@@ -0,0 +1,67 @@
1
+ import { runDatabaseCommand, parseDatabaseCommand, extractDatabaseCommand, DB_PRIMER } from './index.js';
2
+ import { isConnectionOpen, closeConnection, closeAllConnections, listOpenConnections } from '../connections.js';
3
+ // Owns each tab's view of the SQLite databases it has opened, and acts as the controller's facade
4
+ // over the global connection registry (connections.ts) and the `db` command surface (database.ts).
5
+ // SQLite connections themselves are global; this manager attributes them to the tab(s) that ran a
6
+ // `db` command against them — so a tab's connections panel reflects only the databases it opened —
7
+ // and keeps that attribution in sync as databases are created, queried, deleted, or closed.
8
+ export class DatabaseManager {
9
+ // Tab label → the SQLite database names that tab has opened (sorted, deduped).
10
+ tabConns = new Map();
11
+ // The instructions describing the `db` command surface, injected into the ACP agent's primer.
12
+ get primer() {
13
+ return DB_PRIMER;
14
+ }
15
+ // Run a `db` command on behalf of a tab, keeping that tab's tracked SQLite connections in sync so
16
+ // its connections panel reflects what it has open. `delete` forgets the connection; any opening
17
+ // command (create/query) records it once.
18
+ runInTab(label, command) {
19
+ const output = runDatabaseCommand(command);
20
+ const parsed = parseDatabaseCommand(command);
21
+ if (!('error' in parsed)) {
22
+ if (parsed.action === 'delete')
23
+ this.forgetConn(parsed.name);
24
+ else if (parsed.action !== 'list' && isConnectionOpen(parsed.name)) {
25
+ const current = this.tabConns.get(label) ?? [];
26
+ if (!current.includes(parsed.name))
27
+ this.tabConns.set(label, [...current, parsed.name].toSorted((a, b) => a.localeCompare(b)));
28
+ }
29
+ }
30
+ return output;
31
+ }
32
+ // The SQLite databases a tab has opened that are still live (filtered against the global registry
33
+ // so a closed/deleted db drops out). Drives the per-tab connections panel and command recognition.
34
+ openDbs(label) {
35
+ return (this.tabConns.get(label) ?? []).filter(isConnectionOpen);
36
+ }
37
+ // A `db`-shaped command embedded in agent text (for the ACP tool loop), or undefined if none.
38
+ extract(text) {
39
+ return extractDatabaseCommand(text);
40
+ }
41
+ // Every globally open SQLite database (for the connections panel and completion).
42
+ listOpen() {
43
+ return listOpenConnections();
44
+ }
45
+ // Close one globally open SQLite connection by name; returns whether one was open (drives the
46
+ // `connection close sqlite` result message).
47
+ close(name) {
48
+ return closeConnection(name);
49
+ }
50
+ // Forget a tab's tracked connections (on tab close). The connections stay globally open.
51
+ forgetTab(label) {
52
+ this.tabConns.delete(label);
53
+ }
54
+ // Close every globally open SQLite connection and forget all per-tab attribution (last tab closed
55
+ // / app shutdown) — connections are global, so they're closed only when no tab remains.
56
+ closeAll() {
57
+ closeAllConnections();
58
+ this.tabConns.clear();
59
+ }
60
+ // Drop a database name from every tab's tracked connections (on `db delete`).
61
+ forgetConn(name) {
62
+ for (const [label, names] of this.tabConns) {
63
+ if (names.includes(name))
64
+ this.tabConns.set(label, names.filter((n) => n !== name));
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,54 @@
1
+ const VALID_NAME = /^[A-Za-z0-9_-]+$/;
2
+ const USAGE = 'Usage: db sqlite <create|delete|query|list> [name] [query]';
3
+ const ACTIONS = new Set(['create', 'delete', 'query', 'list']);
4
+ function engineError(engine) {
5
+ return { error: `Unsupported engine "${engine}". Only "sqlite" is supported.` };
6
+ }
7
+ function nameError(name) {
8
+ return { error: `Invalid database name "${name}". Use letters, numbers, "-" and "_" only.` };
9
+ }
10
+ function unwrapQuotes(s) {
11
+ const q = s[0];
12
+ if (s.length >= 2 && (q === '"' || q === "'") && s.at(-1) === q) {
13
+ return s.slice(1, -1);
14
+ }
15
+ return s;
16
+ }
17
+ /** Parse a `db sqlite ...` command into an action. Pure — performs no I/O. */
18
+ export function parseDatabaseCommand(input) {
19
+ const rest = input.trim().replace(/^db\b\s*/i, '').trim();
20
+ if (!rest)
21
+ return { error: USAGE };
22
+ const [engineRaw, ...tail] = rest.split(/\s+/);
23
+ const engine = engineRaw.toLowerCase();
24
+ if (engine !== 'sqlite') {
25
+ return ACTIONS.has(engine) ? { error: USAGE } : engineError(engine);
26
+ }
27
+ const action = tail[0]?.toLowerCase();
28
+ if (!action)
29
+ return { error: USAGE };
30
+ if (action === 'list')
31
+ return { action: 'list' };
32
+ if (action === 'create' || action === 'delete') {
33
+ const name = tail[1];
34
+ if (!name)
35
+ return { error: `Usage: db sqlite ${action} <name>` };
36
+ if (!VALID_NAME.test(name))
37
+ return nameError(name);
38
+ return { action, name };
39
+ }
40
+ if (action === 'query') {
41
+ return parseQueryAction(rest);
42
+ }
43
+ return { error: USAGE };
44
+ }
45
+ function parseQueryAction(rest) {
46
+ const m = rest.match(/^sqlite\s+query\s+(\S+)\s+([\s\S]+)$/i);
47
+ if (!m)
48
+ return { error: 'Usage: db sqlite query <name> <sql>' };
49
+ const name = m[1];
50
+ if (!VALID_NAME.test(name))
51
+ return nameError(name);
52
+ const query = unwrapQuotes(m[2].trim());
53
+ return { action: 'query', name, query };
54
+ }
@@ -0,0 +1,41 @@
1
+ import { databaseFileExists, getConnection, isConnectionOpen, } from '../connections.js';
2
+ function errorMessage(error) {
3
+ return error instanceof Error ? error.message : String(error);
4
+ }
5
+ function formatRows(rows) {
6
+ if (rows.length === 0)
7
+ return '(0 rows)';
8
+ const cols = Object.keys(rows[0]);
9
+ const cell = (v) => (v === null || v === undefined ? '' : String(v));
10
+ const widths = cols.map((c) => Math.max(c.length, ...rows.map((r) => cell(r[c]).length)));
11
+ const fmt = (cells) => cells.map((s, index) => s.padEnd(widths[index])).join(' ');
12
+ const header = fmt(cols);
13
+ const separator = widths.map((w) => '-'.repeat(w)).join(' ');
14
+ const body = rows.map((r) => fmt(cols.map((c) => cell(r[c]))));
15
+ const count = `(${rows.length} row${rows.length === 1 ? '' : 's'})`;
16
+ return [header, separator, ...body, '', count].join('\n');
17
+ }
18
+ const READ_QUERY = /^\s*(select|pragma|with|explain)\b/i;
19
+ export function queryDatabase(name, query) {
20
+ if (!databaseFileExists(name) && !isConnectionOpen(name)) {
21
+ return `Database "${name}" does not exist. Create it with: db sqlite create ${name}`;
22
+ }
23
+ let database;
24
+ try {
25
+ database = getConnection(name);
26
+ }
27
+ catch (error) {
28
+ return `Failed to open database "${name}": ${errorMessage(error)}`;
29
+ }
30
+ try {
31
+ if (READ_QUERY.test(query)) {
32
+ const rows = database.prepare(query).all();
33
+ return formatRows(rows);
34
+ }
35
+ database.exec(query);
36
+ return 'OK.';
37
+ }
38
+ catch (error) {
39
+ return `Query error: ${errorMessage(error)}`;
40
+ }
41
+ }
@@ -0,0 +1,67 @@
1
+ import { runDatabaseCommand, parseDatabaseCommand, extractDatabaseCommand, DB_PRIMER } from './database.js';
2
+ import { isConnectionOpen, closeConnection, closeAllConnections, listOpenConnections } from './connections.js';
3
+ // Owns each tab's view of the SQLite databases it has opened, and acts as the controller's facade
4
+ // over the global connection registry (connections.ts) and the `db` command surface (database.ts).
5
+ // SQLite connections themselves are global; this manager attributes them to the tab(s) that ran a
6
+ // `db` command against them — so a tab's connections panel reflects only the databases it opened —
7
+ // and keeps that attribution in sync as databases are created, queried, deleted, or closed.
8
+ export class DatabaseManager {
9
+ // Tab label → the SQLite database names that tab has opened (sorted, deduped).
10
+ tabConns = new Map();
11
+ // The instructions describing the `db` command surface, injected into the ACP agent's primer.
12
+ get primer() {
13
+ return DB_PRIMER;
14
+ }
15
+ // Run a `db` command on behalf of a tab, keeping that tab's tracked SQLite connections in sync so
16
+ // its connections panel reflects what it has open. `delete` forgets the connection; any opening
17
+ // command (create/query) records it once.
18
+ runInTab(label, command) {
19
+ const output = runDatabaseCommand(command);
20
+ const parsed = parseDatabaseCommand(command);
21
+ if (!('error' in parsed)) {
22
+ if (parsed.action === 'delete')
23
+ this.forgetConn(parsed.name);
24
+ else if (parsed.action !== 'list' && isConnectionOpen(parsed.name)) {
25
+ const current = this.tabConns.get(label) ?? [];
26
+ if (!current.includes(parsed.name))
27
+ this.tabConns.set(label, [...current, parsed.name].toSorted((a, b) => a.localeCompare(b)));
28
+ }
29
+ }
30
+ return output;
31
+ }
32
+ // The SQLite databases a tab has opened that are still live (filtered against the global registry
33
+ // so a closed/deleted db drops out). Drives the per-tab connections panel and command recognition.
34
+ openDbs(label) {
35
+ return (this.tabConns.get(label) ?? []).filter(isConnectionOpen);
36
+ }
37
+ // A `db`-shaped command embedded in agent text (for the ACP tool loop), or undefined if none.
38
+ extract(text) {
39
+ return extractDatabaseCommand(text);
40
+ }
41
+ // Every globally open SQLite database (for the connections panel and completion).
42
+ listOpen() {
43
+ return listOpenConnections();
44
+ }
45
+ // Close one globally open SQLite connection by name; returns whether one was open (drives the
46
+ // `connection close sqlite` result message).
47
+ close(name) {
48
+ return closeConnection(name);
49
+ }
50
+ // Forget a tab's tracked connections (on tab close). The connections stay globally open.
51
+ forgetTab(label) {
52
+ this.tabConns.delete(label);
53
+ }
54
+ // Close every globally open SQLite connection and forget all per-tab attribution (last tab closed
55
+ // / app shutdown) — connections are global, so they're closed only when no tab remains.
56
+ closeAll() {
57
+ closeAllConnections();
58
+ this.tabConns.clear();
59
+ }
60
+ // Drop a database name from every tab's tracked connections (on `db delete`).
61
+ forgetConn(name) {
62
+ for (const [label, names] of this.tabConns) {
63
+ if (names.includes(name))
64
+ this.tabConns.set(label, names.filter((n) => n !== name));
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,54 @@
1
+ const VALID_NAME = /^[A-Za-z0-9_-]+$/;
2
+ const USAGE = 'Usage: db sqlite <create|delete|query|list> [name] [query]';
3
+ const ACTIONS = new Set(['create', 'delete', 'query', 'list']);
4
+ function engineError(engine) {
5
+ return { error: `Unsupported engine "${engine}". Only "sqlite" is supported.` };
6
+ }
7
+ function nameError(name) {
8
+ return { error: `Invalid database name "${name}". Use letters, numbers, "-" and "_" only.` };
9
+ }
10
+ function unwrapQuotes(s) {
11
+ const q = s[0];
12
+ if (s.length >= 2 && (q === '"' || q === "'") && s.at(-1) === q) {
13
+ return s.slice(1, -1);
14
+ }
15
+ return s;
16
+ }
17
+ /** Parse a `db sqlite ...` command into an action. Pure — performs no I/O. */
18
+ export function parseDatabaseCommand(input) {
19
+ const rest = input.trim().replace(/^db\b\s*/i, '').trim();
20
+ if (!rest)
21
+ return { error: USAGE };
22
+ const [engineRaw, ...tail] = rest.split(/\s+/);
23
+ const engine = engineRaw.toLowerCase();
24
+ if (engine !== 'sqlite') {
25
+ return ACTIONS.has(engine) ? { error: USAGE } : engineError(engine);
26
+ }
27
+ const action = tail[0]?.toLowerCase();
28
+ if (!action)
29
+ return { error: USAGE };
30
+ if (action === 'list')
31
+ return { action: 'list' };
32
+ if (action === 'create' || action === 'delete') {
33
+ const name = tail[1];
34
+ if (!name)
35
+ return { error: `Usage: db sqlite ${action} <name>` };
36
+ if (!VALID_NAME.test(name))
37
+ return nameError(name);
38
+ return { action, name };
39
+ }
40
+ if (action === 'query') {
41
+ return parseQueryAction(rest);
42
+ }
43
+ return { error: USAGE };
44
+ }
45
+ function parseQueryAction(rest) {
46
+ const m = rest.match(/^sqlite\s+query\s+(\S+)\s+([\s\S]+)$/i);
47
+ if (!m)
48
+ return { error: 'Usage: db sqlite query <name> <sql>' };
49
+ const name = m[1];
50
+ if (!VALID_NAME.test(name))
51
+ return nameError(name);
52
+ const query = unwrapQuotes(m[2].trim());
53
+ return { action: 'query', name, query };
54
+ }
@@ -0,0 +1,41 @@
1
+ import { databaseFileExists, getConnection, isConnectionOpen, } from './connections.js';
2
+ function errorMessage(error) {
3
+ return error instanceof Error ? error.message : String(error);
4
+ }
5
+ function formatRows(rows) {
6
+ if (rows.length === 0)
7
+ return '(0 rows)';
8
+ const cols = Object.keys(rows[0]);
9
+ const cell = (v) => (v === null || v === undefined ? '' : String(v));
10
+ const widths = cols.map((c) => Math.max(c.length, ...rows.map((r) => cell(r[c]).length)));
11
+ const fmt = (cells) => cells.map((s, index) => s.padEnd(widths[index])).join(' ');
12
+ const header = fmt(cols);
13
+ const separator = widths.map((w) => '-'.repeat(w)).join(' ');
14
+ const body = rows.map((r) => fmt(cols.map((c) => cell(r[c]))));
15
+ const count = `(${rows.length} row${rows.length === 1 ? '' : 's'})`;
16
+ return [header, separator, ...body, '', count].join('\n');
17
+ }
18
+ const READ_QUERY = /^\s*(select|pragma|with|explain)\b/i;
19
+ export function queryDatabase(name, query) {
20
+ if (!databaseFileExists(name) && !isConnectionOpen(name)) {
21
+ return `Database "${name}" does not exist. Create it with: db sqlite create ${name}`;
22
+ }
23
+ let database;
24
+ try {
25
+ database = getConnection(name);
26
+ }
27
+ catch (error) {
28
+ return `Failed to open database "${name}": ${errorMessage(error)}`;
29
+ }
30
+ try {
31
+ if (READ_QUERY.test(query)) {
32
+ const rows = database.prepare(query).all();
33
+ return formatRows(rows);
34
+ }
35
+ database.exec(query);
36
+ return 'OK.';
37
+ }
38
+ catch (error) {
39
+ return `Query error: ${errorMessage(error)}`;
40
+ }
41
+ }
@@ -0,0 +1,83 @@
1
+ import { databaseFileExists as databaseFileExists, listDatabaseFiles, removeDatabaseFile, getConnection, closeConnection, } from './connections.js';
2
+ import { parseDatabaseCommand } from './database-parsing.js';
3
+ export { parseDatabaseCommand } from './database-parsing.js';
4
+ import { queryDatabase } from './database-query.js';
5
+ function errorMessage(error) {
6
+ return error instanceof Error ? error.message : String(error);
7
+ }
8
+ function createDatabase(name) {
9
+ const isExisted = databaseFileExists(name);
10
+ try {
11
+ getConnection(name);
12
+ return isExisted ? `Database "${name}" already exists.` : `Created sqlite database "${name}".`;
13
+ }
14
+ catch (error) {
15
+ return `Failed to create database "${name}": ${errorMessage(error)}`;
16
+ }
17
+ }
18
+ function deleteDatabase(name) {
19
+ closeConnection(name);
20
+ if (!databaseFileExists(name))
21
+ return `Database "${name}" does not exist.`;
22
+ try {
23
+ removeDatabaseFile(name);
24
+ return `Deleted sqlite database "${name}".`;
25
+ }
26
+ catch (error) {
27
+ return `Failed to delete database "${name}": ${errorMessage(error)}`;
28
+ }
29
+ }
30
+ function listDbs() {
31
+ const names = listDatabaseFiles();
32
+ return names.length > 0 ? names.join('\n') : 'No databases.';
33
+ }
34
+ /** Execute a `db ...` command and return the text to show in the transcript. */
35
+ export function runDatabaseCommand(input) {
36
+ const parsed = parseDatabaseCommand(input);
37
+ if ('error' in parsed)
38
+ return parsed.error;
39
+ switch (parsed.action) {
40
+ case 'create': {
41
+ return createDatabase(parsed.name);
42
+ }
43
+ case 'delete': {
44
+ return deleteDatabase(parsed.name);
45
+ }
46
+ case 'list': {
47
+ return listDbs();
48
+ }
49
+ case 'query': {
50
+ return queryDatabase(parsed.name, parsed.query);
51
+ }
52
+ }
53
+ }
54
+ // Primer injected into an ACP agent so it understands the `db` grammar and can
55
+ // drive an autonomous tool loop: it emits a command, the host runs it, and the
56
+ // output is fed back until the agent answers without a command.
57
+ export const DB_PRIMER = [
58
+ 'This host CLI can manage SQLite databases via `db` commands. Syntax:',
59
+ ' db sqlite create <name> # create an empty database',
60
+ ' db sqlite delete <name> # delete a database',
61
+ ' db sqlite query <name> <sql> # run SQL against a database',
62
+ ' db sqlite list # list databases',
63
+ 'Database names use letters, numbers, "-" and "_" only; the engine is always "sqlite".',
64
+ 'To inspect or change a database, end your reply with exactly one `db` command on its',
65
+ 'own final line (no code fence, nothing after it). The host runs it automatically and',
66
+ 'returns the output to you, so you can issue further commands. When the task is done,',
67
+ 'reply with the final answer and NO trailing `db` command.',
68
+ 'Be concise: do not explain what you are doing. Only output `db` commands and the final answer.',
69
+ ].join('\n');
70
+ /**
71
+ * Pull a proposed `db ...` command out of an agent reply, if present. Scans
72
+ * bottom-up (the primer asks for the command on the last line) and tolerates a
73
+ * surrounding code fence or a leading `$ `/`> ` prompt marker.
74
+ */
75
+ export function extractDatabaseCommand(text) {
76
+ const lines = text.split('\n');
77
+ for (let index = lines.length - 1; index >= 0; index--) {
78
+ const line = lines[index].replace(/^[\s`$>]+/, '').replace(/`+\s*$/, '').trim();
79
+ if (/^db\s+sqlite\s+(create|delete|query|list)\b/i.test(line))
80
+ return line;
81
+ }
82
+ // not a db command
83
+ }
@@ -0,0 +1,15 @@
1
+ export function getDateStr() {
2
+ const now = new Date();
3
+ const y = now.getFullYear();
4
+ const m = String(now.getMonth() + 1).padStart(2, '0');
5
+ const d = String(now.getDate()).padStart(2, '0');
6
+ return `${y}-${m}-${d}`;
7
+ }
8
+ export function getTimeStr() {
9
+ const now = new Date();
10
+ const h = String(now.getHours()).padStart(2, '0');
11
+ const m = String(now.getMinutes()).padStart(2, '0');
12
+ const s = String(now.getSeconds()).padStart(2, '0');
13
+ const ms = String(now.getMilliseconds()).padStart(3, '0');
14
+ return `${h}:${m}:${s}.${ms}`;
15
+ }
@@ -0,0 +1,27 @@
1
+ import { statSync, writeFileSync } from 'node:fs';
2
+ import { humanSize } from '../openers/size.js';
3
+ import { messageBus } from '../bus.js';
4
+ // Write an editor tab's buffer back to disk. `url` is the tab's `/open/<id>` ref, resolved
5
+ // through the open-file allow-list — the client can only ever write to files the user explicitly
6
+ // opened. Throws on an unknown ref or a write failure; the RPC layer turns that into an error
7
+ // reply for the client's save feedback.
8
+ export function saveFile(managers, url, content) {
9
+ const id = url.startsWith('/open/') ? url.slice('/open/'.length) : '';
10
+ const filePath = id ? managers.tab.openFilePath(id) : undefined;
11
+ if (!filePath)
12
+ throw new Error(`saveFile: unknown file ref "${url}"`);
13
+ writeFileSync(filePath, content, 'utf8');
14
+ // Refresh the owning tab's displayed size from the file's new on-disk size.
15
+ const stat = statSync(filePath);
16
+ const tab = managers.tab.tabs.find((t) => t.editor?.url === url);
17
+ if (tab?.editor)
18
+ tab.editor = { ...tab.editor, size: humanSize(stat.size) };
19
+ // The content is now canonical on disk, so any transient draft is superseded — drop it.
20
+ if (tab)
21
+ tab.editorDraft = undefined;
22
+ // Move the watcher's baseline forward first, so its own `fs.watch` event for this write isn't
23
+ // mistaken for an external change.
24
+ if (tab)
25
+ managers.editorWatch.markSaved(tab.label, stat.mtimeMs);
26
+ messageBus.emit('state', { type: 'dirty' });
27
+ }
@@ -0,0 +1,10 @@
1
+ // Cache an editor tab's in-progress (unsaved) buffer as transient draft state on the tab. `url`
2
+ // is the tab's `/open/<id>` ref, matched the same way saveFile's handler resolves its tab — but
3
+ // unlike saveFile this never touches the filesystem or the open-file allow-list; it only writes
4
+ // the in-memory `editorDraft`. An unresolvable `url` (e.g. a tab closed mid-debounce) is a no-op,
5
+ // never an error.
6
+ export function syncEditorBuffer(managers, url, content) {
7
+ const tab = managers.tab.tabs.find((t) => t.editor?.url === url);
8
+ if (tab)
9
+ tab.editorDraft = { content, updatedAt: Date.now() };
10
+ }
@@ -0,0 +1,84 @@
1
+ import { watch, statSync } from 'node:fs';
2
+ import { messageBus } from '../bus.js';
3
+ const DEBOUNCE_MS = 100;
4
+ // Watches an open editor tab's underlying file for changes made by other processes, keyed by tab
5
+ // label. `markSaved` moves the baseline mtime forward before the write's own `fs.watch` event
6
+ // arrives, so the app's own saves never appear as an external change.
7
+ export class EditorWatchManager {
8
+ managers;
9
+ tabs = new Map();
10
+ constructor(managers) {
11
+ this.managers = managers;
12
+ }
13
+ watch(label, filePath) {
14
+ this.unwatch(label);
15
+ let baselineMtimeMs;
16
+ try {
17
+ baselineMtimeMs = statSync(filePath).mtimeMs;
18
+ }
19
+ catch {
20
+ return;
21
+ }
22
+ try {
23
+ const watcher = watch(filePath, () => this.scheduleCheck(label));
24
+ this.tabs.set(label, { filePath, watcher, baselineMtimeMs });
25
+ }
26
+ catch {
27
+ // Exotic filesystems, fd limits, races — the editor still works, just without live reload.
28
+ }
29
+ }
30
+ // Move the baseline forward after the app's own write, so the resulting `fs.watch` event isn't
31
+ // mistaken for an external change.
32
+ markSaved(label, mtimeMs) {
33
+ const state = this.tabs.get(label);
34
+ if (state)
35
+ state.baselineMtimeMs = mtimeMs;
36
+ }
37
+ closeTab(label) {
38
+ this.unwatch(label);
39
+ }
40
+ dispose() {
41
+ for (const label of this.tabs.keys())
42
+ this.unwatch(label);
43
+ }
44
+ unwatch(label) {
45
+ const state = this.tabs.get(label);
46
+ if (!state)
47
+ return;
48
+ if (state.debounce)
49
+ clearTimeout(state.debounce);
50
+ try {
51
+ state.watcher.close();
52
+ }
53
+ catch { /* already gone */ }
54
+ this.tabs.delete(label);
55
+ }
56
+ scheduleCheck(label) {
57
+ const state = this.tabs.get(label);
58
+ if (!state)
59
+ return;
60
+ if (state.debounce)
61
+ clearTimeout(state.debounce);
62
+ state.debounce = setTimeout(() => this.check(label), DEBOUNCE_MS);
63
+ }
64
+ check(label) {
65
+ const state = this.tabs.get(label);
66
+ if (!state)
67
+ return;
68
+ let mtimeMs;
69
+ try {
70
+ mtimeMs = statSync(state.filePath).mtimeMs;
71
+ }
72
+ catch {
73
+ return;
74
+ }
75
+ if (mtimeMs === state.baselineMtimeMs)
76
+ return;
77
+ state.baselineMtimeMs = mtimeMs;
78
+ const tab = this.managers.tab.tabs.find((t) => t.label === label);
79
+ if (!tab?.editor)
80
+ return;
81
+ tab.editor = { ...tab.editor, mtimeMs };
82
+ messageBus.emit('state', { type: 'dirty' });
83
+ }
84
+ }
@@ -0,0 +1,24 @@
1
+ import { statSync, writeFileSync } from 'node:fs';
2
+ import { humanSize } from './openers/size.js';
3
+ import { messageBus } from './bus.js';
4
+ // Write an editor tab's buffer back to disk. `url` is the tab's `/open/<id>` ref, resolved
5
+ // through the open-file allow-list — the client can only ever write to files the user explicitly
6
+ // opened. Throws on an unknown ref or a write failure; the RPC layer turns that into an error
7
+ // reply for the client's save feedback.
8
+ export function saveFile(managers, url, content) {
9
+ const id = url.startsWith('/open/') ? url.slice('/open/'.length) : '';
10
+ const filePath = id ? managers.tab.openFilePath(id) : undefined;
11
+ if (!filePath)
12
+ throw new Error(`saveFile: unknown file ref "${url}"`);
13
+ writeFileSync(filePath, content, 'utf8');
14
+ // Refresh the owning tab's displayed size from the file's new on-disk size.
15
+ const stat = statSync(filePath);
16
+ const tab = managers.tab.tabs.find((t) => t.editor?.url === url);
17
+ if (tab?.editor)
18
+ tab.editor = { ...tab.editor, size: humanSize(stat.size) };
19
+ // Move the watcher's baseline forward first, so its own `fs.watch` event for this write isn't
20
+ // mistaken for an external change.
21
+ if (tab)
22
+ managers.editorWatch.markSaved(tab.label, stat.mtimeMs);
23
+ messageBus.emit('state', { type: 'dirty' });
24
+ }