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,41 @@
1
+ import { messageBus } from '../bus.js';
2
+ import { SHELL_NAME } from '../shell-manager.js';
3
+ export function closeConnection(kind, id, managers, label, out) {
4
+ switch (kind) {
5
+ case 'sqlite': {
6
+ out(managers.database.close(id) ? `Closed connection sqlite:${id}.` : `No open connection sqlite:${id}.`);
7
+ break;
8
+ }
9
+ case 'shell': {
10
+ if (managers.shell.close(label))
11
+ out(`Closed connection shell:${SHELL_NAME}.`);
12
+ else
13
+ out(`No open connection shell:${id}.`);
14
+ break;
15
+ }
16
+ case 'acp': {
17
+ if (managers.acp.close(label)) {
18
+ messageBus.emit('state', { type: 'dirty' });
19
+ out('Closed connection acp:opencode.');
20
+ }
21
+ else
22
+ out('No open connection acp:opencode.');
23
+ break;
24
+ }
25
+ case 'ssh': {
26
+ const tabs = managers.tab.tabs;
27
+ const tab = tabs.find((t) => t.harness?.name === 'ssh' && t.label === id)
28
+ ?? tabs.find((t) => t.harness?.name === 'ssh' && t.harness.destination === id);
29
+ if (tab?.harness) {
30
+ managers.pty.kill(tab.harness.ptyId);
31
+ out(`Closed connection ssh:${id}.`);
32
+ }
33
+ else
34
+ out(`No open connection ssh:${id}.`);
35
+ break;
36
+ }
37
+ default: {
38
+ out(`Closing ${kind} connections is not yet available in the web UI.`);
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,93 @@
1
+ import { parseConnectionCommand } from '../connections.js';
2
+ import { SHELL_NAME } from '../shell-manager.js';
3
+ import { closeConnection } from './close.js';
4
+ // The global `connection list` lines: shell/acp are per-issuing-tab, the rest (terminals, ssh
5
+ // tabs, sqlite) span every tab, since they have no command bar of their own to list from.
6
+ function listLines(managers, label) {
7
+ const lines = [];
8
+ if (managers.shell.has(label))
9
+ lines.push(`shell:${SHELL_NAME}`);
10
+ if (managers.acp.has(label))
11
+ lines.push('acp:opencode');
12
+ const b = managers.browser.info(label);
13
+ if (b)
14
+ for (const id of b.ids)
15
+ lines.push(`browser:${id}`);
16
+ for (const program of managers.pty.terminalsFor(label))
17
+ lines.push(`terminal:${program}`);
18
+ for (const t of managers.tab.tabs) {
19
+ if (t.harness?.name === 'ssh' && t.harness.destination)
20
+ lines.push(`ssh:${t.harness.destination}`);
21
+ }
22
+ for (const n of managers.database.listOpen())
23
+ lines.push(`sqlite:${n}`);
24
+ return lines;
25
+ }
26
+ export class ConnectionManager {
27
+ managers;
28
+ constructor(managers) {
29
+ this.managers = managers;
30
+ }
31
+ connectionsFor(label) {
32
+ const rows = [];
33
+ if (this.managers.shell.has(label)) {
34
+ rows.push({ text: `${SHELL_NAME}:${this.managers.tab.shorten(this.managers.tab.cwdOf(label) ?? process.cwd())}`, kind: 'shell' });
35
+ }
36
+ const acp = this.managers.acp.label(label);
37
+ if (acp)
38
+ rows.push({ text: `acp:${acp}`, kind: 'acp' });
39
+ rows.push(...this.managers.monitor.connectionsFor(label));
40
+ const b = this.managers.browser.info(label);
41
+ if (b)
42
+ for (const id of b.ids)
43
+ rows.push({ text: `browser:${id} (${b.mode})`, kind: 'browser' });
44
+ const tab = this.managers.tab.tabs.find((t) => t.label === label);
45
+ if (tab?.harness?.name === 'ssh' && tab.harness.destination) {
46
+ rows.push({ text: `ssh:${tab.harness.destination}`, kind: 'ssh' });
47
+ }
48
+ else {
49
+ for (const program of this.managers.pty.terminalsFor(label))
50
+ rows.push({ text: `terminal:${program}`, kind: 'terminal' });
51
+ }
52
+ for (const n of this.managers.database.openDbs(label))
53
+ rows.push({ text: `sqlite:${n}`, kind: 'sqlite' });
54
+ return rows;
55
+ }
56
+ run(command, label) {
57
+ const parsed = parseConnectionCommand(command);
58
+ const out = (text) => this.managers.tab.append(label, { input: command, output: text });
59
+ if ('error' in parsed) {
60
+ out(parsed.error);
61
+ return;
62
+ }
63
+ if (parsed.action === 'list') {
64
+ const lines = listLines(this.managers, label);
65
+ out(lines.length > 0 ? lines.join('\n') : 'No open connections.');
66
+ return;
67
+ }
68
+ if (parsed.kind === 'browser') {
69
+ this.managers.tab.startRunning(label, command);
70
+ void this.managers.browser.run(label, `browser window close ${parsed.id}`).then((o) => this.managers.tab.finishRunning(label, o));
71
+ return;
72
+ }
73
+ closeConnection(parsed.kind, parsed.id, this.managers, label, out);
74
+ }
75
+ completionConnections(label) {
76
+ const out = [];
77
+ if (this.managers.shell.has(label))
78
+ out.push(`shell:${SHELL_NAME}`);
79
+ if (this.managers.acp.has(label))
80
+ out.push('acp:opencode');
81
+ const b = this.managers.browser.info(label);
82
+ if (b)
83
+ for (const id of b.ids)
84
+ out.push(`browser:${id}`);
85
+ for (const n of this.managers.database.listOpen())
86
+ out.push(`sqlite:${n}`);
87
+ for (const t of this.managers.tab.tabs) {
88
+ if (t.harness?.name === 'ssh')
89
+ out.push(`ssh:${t.label}`);
90
+ }
91
+ return out;
92
+ }
93
+ }
@@ -0,0 +1,28 @@
1
+ const KINDS = ['sqlite', 'shell', 'acp', 'browser', 'ssh'];
2
+ const USAGE = 'Usage: connection <list|close> [kind:id] (e.g. connection close sqlite:mydb)';
3
+ export function parseConnectionCommand(input) {
4
+ const rest = input.trim().replace(/^connection\b\s*/i, '').trim();
5
+ if (!rest)
6
+ return { error: USAGE };
7
+ const [actionRaw, target] = rest.split(/\s+/);
8
+ const action = actionRaw.toLowerCase();
9
+ if (action === 'list')
10
+ return { action: 'list' };
11
+ if (action === 'close') {
12
+ if (!target)
13
+ return { error: 'Usage: connection close <kind>:<id>' };
14
+ const index = target.indexOf(':');
15
+ if (index === -1) {
16
+ return { error: `Invalid connection "${target}". Expected <kind>:<id>, e.g. sqlite:mydb.` };
17
+ }
18
+ const kind = target.slice(0, index).toLowerCase();
19
+ if (!KINDS.includes(kind)) {
20
+ return { error: `Unknown connection kind "${kind}". Expected one of: ${KINDS.join(', ')}.` };
21
+ }
22
+ const id = target.slice(index + 1);
23
+ if (!id)
24
+ return { error: `Missing id in "${target}".` };
25
+ return { action: 'close', kind: kind, id };
26
+ }
27
+ return { error: USAGE };
28
+ }
@@ -0,0 +1,41 @@
1
+ import { messageBus } from './bus.js';
2
+ import { SHELL_NAME } from './shell-manager.js';
3
+ export function closeConnection(kind, id, managers, label, out) {
4
+ switch (kind) {
5
+ case 'sqlite': {
6
+ out(managers.database.close(id) ? `Closed connection sqlite:${id}.` : `No open connection sqlite:${id}.`);
7
+ break;
8
+ }
9
+ case 'shell': {
10
+ if (managers.shell.close(label))
11
+ out(`Closed connection shell:${SHELL_NAME}.`);
12
+ else
13
+ out(`No open connection shell:${id}.`);
14
+ break;
15
+ }
16
+ case 'acp': {
17
+ if (managers.acp.close(label)) {
18
+ messageBus.emit('state', { type: 'dirty' });
19
+ out('Closed connection acp:opencode.');
20
+ }
21
+ else
22
+ out('No open connection acp:opencode.');
23
+ break;
24
+ }
25
+ case 'ssh': {
26
+ const tabs = managers.tab.tabs;
27
+ const tab = tabs.find((t) => t.harness?.name === 'ssh' && t.label === id)
28
+ ?? tabs.find((t) => t.harness?.name === 'ssh' && t.harness.destination === id);
29
+ if (tab?.harness) {
30
+ managers.pty.kill(tab.harness.ptyId);
31
+ out(`Closed connection ssh:${id}.`);
32
+ }
33
+ else
34
+ out(`No open connection ssh:${id}.`);
35
+ break;
36
+ }
37
+ default: {
38
+ out(`Closing ${kind} connections is not yet available in the web UI.`);
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,93 @@
1
+ import { parseConnectionCommand } from './connections.js';
2
+ import { SHELL_NAME } from './shell-manager.js';
3
+ import { closeConnection } from './connection-close.js';
4
+ // The global `connection list` lines: shell/acp are per-issuing-tab, the rest (terminals, ssh
5
+ // tabs, sqlite) span every tab, since they have no command bar of their own to list from.
6
+ function listLines(managers, label) {
7
+ const lines = [];
8
+ if (managers.shell.has(label))
9
+ lines.push(`shell:${SHELL_NAME}`);
10
+ if (managers.acp.has(label))
11
+ lines.push('acp:opencode');
12
+ const b = managers.browser.info(label);
13
+ if (b)
14
+ for (const id of b.ids)
15
+ lines.push(`browser:${id}`);
16
+ for (const program of managers.pty.terminalsFor(label))
17
+ lines.push(`terminal:${program}`);
18
+ for (const t of managers.tab.tabs) {
19
+ if (t.harness?.name === 'ssh' && t.harness.destination)
20
+ lines.push(`ssh:${t.harness.destination}`);
21
+ }
22
+ for (const n of managers.database.listOpen())
23
+ lines.push(`sqlite:${n}`);
24
+ return lines;
25
+ }
26
+ export class ConnectionManager {
27
+ managers;
28
+ constructor(managers) {
29
+ this.managers = managers;
30
+ }
31
+ connectionsFor(label) {
32
+ const rows = [];
33
+ if (this.managers.shell.has(label)) {
34
+ rows.push({ text: `${SHELL_NAME}:${this.managers.tab.shorten(this.managers.tab.cwdOf(label) ?? process.cwd())}`, kind: 'shell' });
35
+ }
36
+ const acp = this.managers.acp.label(label);
37
+ if (acp)
38
+ rows.push({ text: `acp:${acp}`, kind: 'acp' });
39
+ rows.push(...this.managers.monitor.connectionsFor(label));
40
+ const b = this.managers.browser.info(label);
41
+ if (b)
42
+ for (const id of b.ids)
43
+ rows.push({ text: `browser:${id} (${b.mode})`, kind: 'browser' });
44
+ const tab = this.managers.tab.tabs.find((t) => t.label === label);
45
+ if (tab?.harness?.name === 'ssh' && tab.harness.destination) {
46
+ rows.push({ text: `ssh:${tab.harness.destination}`, kind: 'ssh' });
47
+ }
48
+ else {
49
+ for (const program of this.managers.pty.terminalsFor(label))
50
+ rows.push({ text: `terminal:${program}`, kind: 'terminal' });
51
+ }
52
+ for (const n of this.managers.database.openDbs(label))
53
+ rows.push({ text: `sqlite:${n}`, kind: 'sqlite' });
54
+ return rows;
55
+ }
56
+ run(command, label) {
57
+ const parsed = parseConnectionCommand(command);
58
+ const out = (text) => this.managers.tab.append(label, { input: command, output: text });
59
+ if ('error' in parsed) {
60
+ out(parsed.error);
61
+ return;
62
+ }
63
+ if (parsed.action === 'list') {
64
+ const lines = listLines(this.managers, label);
65
+ out(lines.length > 0 ? lines.join('\n') : 'No open connections.');
66
+ return;
67
+ }
68
+ if (parsed.kind === 'browser') {
69
+ this.managers.tab.startRunning(label, command);
70
+ void this.managers.browser.run(label, `browser window close ${parsed.id}`).then((o) => this.managers.tab.finishRunning(label, o));
71
+ return;
72
+ }
73
+ closeConnection(parsed.kind, parsed.id, this.managers, label, out);
74
+ }
75
+ completionConnections(label) {
76
+ const out = [];
77
+ if (this.managers.shell.has(label))
78
+ out.push(`shell:${SHELL_NAME}`);
79
+ if (this.managers.acp.has(label))
80
+ out.push('acp:opencode');
81
+ const b = this.managers.browser.info(label);
82
+ if (b)
83
+ for (const id of b.ids)
84
+ out.push(`browser:${id}`);
85
+ for (const n of this.managers.database.listOpen())
86
+ out.push(`sqlite:${n}`);
87
+ for (const t of this.managers.tab.tabs) {
88
+ if (t.harness?.name === 'ssh')
89
+ out.push(`ssh:${t.label}`);
90
+ }
91
+ return out;
92
+ }
93
+ }
@@ -0,0 +1,28 @@
1
+ const KINDS = ['sqlite', 'shell', 'acp', 'browser', 'ssh'];
2
+ const USAGE = 'Usage: connection <list|close> [kind:id] (e.g. connection close sqlite:mydb)';
3
+ export function parseConnectionCommand(input) {
4
+ const rest = input.trim().replace(/^connection\b\s*/i, '').trim();
5
+ if (!rest)
6
+ return { error: USAGE };
7
+ const [actionRaw, target] = rest.split(/\s+/);
8
+ const action = actionRaw.toLowerCase();
9
+ if (action === 'list')
10
+ return { action: 'list' };
11
+ if (action === 'close') {
12
+ if (!target)
13
+ return { error: 'Usage: connection close <kind>:<id>' };
14
+ const index = target.indexOf(':');
15
+ if (index === -1) {
16
+ return { error: `Invalid connection "${target}". Expected <kind>:<id>, e.g. sqlite:mydb.` };
17
+ }
18
+ const kind = target.slice(0, index).toLowerCase();
19
+ if (!KINDS.includes(kind)) {
20
+ return { error: `Unknown connection kind "${kind}". Expected one of: ${KINDS.join(', ')}.` };
21
+ }
22
+ const id = target.slice(index + 1);
23
+ if (!id)
24
+ return { error: `Missing id in "${target}".` };
25
+ return { action: 'close', kind: kind, id };
26
+ }
27
+ return { error: USAGE };
28
+ }
@@ -0,0 +1,75 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { mkdirSync, rmSync, existsSync, readdirSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ export { parseConnectionCommand } from './connection/parsing.js';
5
+ // SQLite databases live under .janissary/db/sqlite/<name>.sqlite and, unlike
6
+ // agent state and workspaces, persist across launches — that is the whole point.
7
+ let dbDir = '';
8
+ export function initDbDir(projectDir) {
9
+ dbDir = path.join(projectDir, '.janissary', 'db', 'sqlite');
10
+ }
11
+ const VALID_DB_NAME = /^[\w-]+$/;
12
+ export function dbPath(name) {
13
+ if (!VALID_DB_NAME.test(name))
14
+ throw new Error(`Invalid database name: "${name}"`);
15
+ return path.join(dbDir, `${name}.sqlite`);
16
+ }
17
+ /** Whether a database file exists on disk. */
18
+ export function databaseFileExists(name) {
19
+ return !!dbDir && existsSync(dbPath(name));
20
+ }
21
+ /** List database names that have a `.sqlite` file on disk, sorted. */
22
+ export function listDatabaseFiles() {
23
+ if (!dbDir || !existsSync(dbDir))
24
+ return [];
25
+ return readdirSync(dbDir)
26
+ .filter((f) => f.endsWith('.sqlite'))
27
+ .map((f) => f.slice(0, -'.sqlite'.length))
28
+ .toSorted((a, b) => a.localeCompare(b));
29
+ }
30
+ export function removeDatabaseFile(name) {
31
+ rmSync(dbPath(name), { force: true });
32
+ }
33
+ // Open SQLite connections keyed by database name: opened lazily on the first
34
+ // `db` command targeting a database and kept open — across commands and tabs —
35
+ // until closed explicitly (`connection close sqlite:<name>`) or at app exit.
36
+ // Multiple databases can be connected at once.
37
+ const connections = new Map();
38
+ export function getConnection(name) {
39
+ let database = connections.get(name);
40
+ if (!database) {
41
+ mkdirSync(dbDir, { recursive: true });
42
+ database = new DatabaseSync(dbPath(name));
43
+ connections.set(name, database);
44
+ }
45
+ return database;
46
+ }
47
+ export function isConnectionOpen(name) {
48
+ return connections.has(name);
49
+ }
50
+ /** Close one open SQLite connection. Returns whether one was actually open. */
51
+ export function closeConnection(name) {
52
+ const database = connections.get(name);
53
+ if (!database)
54
+ return false;
55
+ try {
56
+ database.close();
57
+ }
58
+ catch { /* ignore */ }
59
+ connections.delete(name);
60
+ return true;
61
+ }
62
+ /** Close every open SQLite connection (called on app exit). */
63
+ export function closeAllConnections() {
64
+ for (const [, database] of connections) {
65
+ try {
66
+ database.close();
67
+ }
68
+ catch { /* ignore */ }
69
+ }
70
+ connections.clear();
71
+ }
72
+ /** Names of databases with an open connection, sorted. */
73
+ export function listOpenConnections() {
74
+ return [...connections.keys()].toSorted((a, b) => a.localeCompare(b));
75
+ }
@@ -0,0 +1,34 @@
1
+ import { messageBus } from './bus.js';
2
+ import { notify } from './notifications.js';
3
+ // Wires the message-bus subscriptions the Controller needs for the lifetime of the process:
4
+ // re-emitting state to clients, persisting agent state and notifying on new transcript entries,
5
+ // process exit, and PTY data/exit relaying. Split out of the Controller constructor purely to keep
6
+ // controller.ts under the file-size guideline; this has no state of its own.
7
+ export function wireControllerEvents(managers, sinks) {
8
+ messageBus.on('state', 'dirty', () => sinks.emitState());
9
+ messageBus.on('transcript', 'entry:appended', (event) => {
10
+ if (event.type !== 'entry:appended')
11
+ return;
12
+ managers.tab.persist(managers.tab.buildAgentState(event.tab, { schedule: managers.schedule.get(event.tab.label) }));
13
+ // A cross-agent `msg`/`broadcast` delivery sets `entry.from`; feed the notifications tab
14
+ // (focus suppression and the per-event toggle are enforced inside `notify`).
15
+ if (event.entry.from)
16
+ notify(managers, 'incoming-message', event.tabLabel, event.entry.from);
17
+ });
18
+ messageBus.on('app', 'exit', () => sinks.exit?.());
19
+ messageBus.on('pty', ['data', 'exit'], (event) => {
20
+ if (event.type === 'data') {
21
+ sinks.sendPty(event.id, event.data);
22
+ return;
23
+ }
24
+ if (event.type !== 'exit')
25
+ return;
26
+ const harnessIndex = managers.tab.tabs.findIndex((tab) => tab.harness?.ptyId === event.id);
27
+ if (harnessIndex !== -1) {
28
+ sinks.sendPtyExit(event.id, event.exitCode);
29
+ managers.tab.closeTab(harnessIndex);
30
+ return;
31
+ }
32
+ sinks.sendPtyExit(event.id, event.exitCode);
33
+ });
34
+ }
@@ -0,0 +1,36 @@
1
+ export function fileTreeToggle(managers, index, path) {
2
+ const label = managers.tab.tabs[index]?.label;
3
+ if (label)
4
+ managers.fileTree.toggle(label, path);
5
+ }
6
+ export function fileTreeCollapseAll(managers, index) {
7
+ const label = managers.tab.tabs[index]?.label;
8
+ if (label)
9
+ managers.fileTree.collapseAll(label);
10
+ }
11
+ export function fileTreeReroot(managers, index, relPath) {
12
+ const label = managers.tab.tabs[index]?.label;
13
+ if (label)
14
+ managers.fileTree.reroot(label, relPath);
15
+ }
16
+ export function moveFileTreeItem(managers, index, fromRelPath, toRelPath) {
17
+ const label = managers.tab.tabs[index]?.label;
18
+ if (label)
19
+ managers.fileTree.move(label, fromRelPath, toRelPath);
20
+ }
21
+ export function deleteFileTreeItem(managers, index, relPath) {
22
+ const label = managers.tab.tabs[index]?.label;
23
+ if (label)
24
+ managers.fileTree.delete(label, relPath);
25
+ }
26
+ export function undoFileTreeItem(managers, index, overwrite) {
27
+ const label = managers.tab.tabs[index]?.label;
28
+ return label ? managers.fileTree.undo(label, overwrite) : {};
29
+ }
30
+ export function redoFileTreeItem(managers, index, overwrite) {
31
+ const label = managers.tab.tabs[index]?.label;
32
+ return label ? managers.fileTree.redo(label, overwrite) : {};
33
+ }
34
+ export function openFileNavigatorFor(managers, label) {
35
+ managers.fileTree.openOrRetarget(label);
36
+ }
@@ -0,0 +1,201 @@
1
+ import { HarnessManager } from './harness/manager.js';
2
+ import { SshManager } from './ssh-manager.js';
3
+ import { DatabaseManager } from './database/manager.js';
4
+ import { AcpManager } from './acp/manager.js';
5
+ import { ShellManager } from './shell-manager.js';
6
+ import { WorkspaceManager } from './workspace-manager.js';
7
+ import { completeCommandLine } from './completion/index.js';
8
+ import { PseudoterminalManager } from './pseudoterminal-manager.js';
9
+ import { TranscriptStore } from './transcript/store.js';
10
+ import { ScheduleManager } from './schedule/manager.js';
11
+ import { ProfileManager } from './profile/manager.js';
12
+ import { ConnectionManager } from './connection/manager.js';
13
+ import { OpenFileManager } from './open-file-manager.js';
14
+ import { FileTreeManager } from './file-tree-manager.js';
15
+ import * as fileTreeRpc from './controller-file-tree.js';
16
+ import { wireControllerEvents } from './controller-events.js';
17
+ import { EditorWatchManager } from './editor/watch-manager.js';
18
+ import { saveFile } from './editor/save.js';
19
+ import { syncEditorBuffer } from './editor/sync.js';
20
+ import { syncPageSnapshot } from './page/sync.js';
21
+ import { CaptureManager } from './capture/manager.js';
22
+ import { AgentCommunicationManager } from './agent/communication-manager.js';
23
+ import { messageBus } from './bus.js';
24
+ import { BrowserManager } from './browser/tab.js';
25
+ import { CommandManager } from './command-manager.js';
26
+ import { runSuggestion } from './monitor/window.js';
27
+ import { MonitorManager } from './monitor/manager.js';
28
+ import { listPersonas } from './personas.js';
29
+ import { TabManager } from './tab/manager.js';
30
+ export class Controller {
31
+ sinks;
32
+ projectDir;
33
+ managers = {};
34
+ get rootDir() { return this.projectDir ?? process.cwd(); }
35
+ constructor(sinks, projectDir) {
36
+ this.sinks = sinks;
37
+ this.projectDir = projectDir;
38
+ this.managers.database = new DatabaseManager();
39
+ this.managers.tab = new TabManager(this.managers, projectDir);
40
+ this.managers.workspace = new WorkspaceManager(projectDir);
41
+ this.managers.browser = new BrowserManager(this.managers);
42
+ this.managers.acp = new AcpManager(this.managers);
43
+ this.managers.openFile = new OpenFileManager(this.managers);
44
+ this.managers.fileTree = new FileTreeManager(this.managers);
45
+ this.managers.editorWatch = new EditorWatchManager(this.managers);
46
+ this.managers.pty = new PseudoterminalManager(this.managers);
47
+ this.managers.schedule = new ScheduleManager(this.managers);
48
+ this.managers.shell = new ShellManager(this.managers);
49
+ this.managers.harness = new HarnessManager(this.managers);
50
+ this.managers.ssh = new SshManager(this.managers);
51
+ this.managers.profile = new ProfileManager(this.managers);
52
+ this.managers.connection = new ConnectionManager(this.managers);
53
+ this.managers.communication = new AgentCommunicationManager(this.managers);
54
+ this.managers.command = new CommandManager(this.managers);
55
+ this.managers.capture = new CaptureManager(this.managers);
56
+ this.managers.monitor = new MonitorManager(this.managers);
57
+ wireControllerEvents(this.managers, this.sinks);
58
+ this.managers.schedule.start();
59
+ }
60
+ // Restore tabs from persisted agent state (for `--relaunch`). Called before any client connects.
61
+ rehydrate() {
62
+ this.managers.tab.rehydrate((name) => TranscriptStore.load(name), (s) => { if (s.schedule)
63
+ this.managers.schedule.set(s.name, s.schedule); });
64
+ }
65
+ view() {
66
+ return this.managers.tab.view((l) => this.managers.connection.connectionsFor(l), (l) => this.managers.acp.label(l), (l) => this.managers.schedule.view(l));
67
+ }
68
+ routeView() {
69
+ return this.managers.command.routeView();
70
+ }
71
+ chooseRoute(index) {
72
+ this.managers.command.chooseRoute(index);
73
+ }
74
+ dispatch(text) {
75
+ this.managers.command.dispatch(text);
76
+ }
77
+ // The absolute path behind an `/open/<id>` ref, or undefined when not registered (drives the route).
78
+ openFilePath(id) {
79
+ return this.managers.tab.openFilePath(id);
80
+ }
81
+ // Write an editor tab's buffer back to disk (the `saveFile` RPC). Throws on error; the RPC
82
+ // layer relays the message to the client.
83
+ saveFile(url, content) {
84
+ saveFile(this.managers, url, content);
85
+ }
86
+ // Cache an editor tab's in-progress buffer as transient draft state (the `editorSync` RPC).
87
+ // In-memory only; never written to disk.
88
+ syncEditorBuffer(url, content) {
89
+ syncEditorBuffer(this.managers, url, content);
90
+ }
91
+ // Cache a page tab's currently visible text as transient snapshot state (the `pageSync` RPC).
92
+ // In-memory only; never written to disk or sent to any client.
93
+ syncPageSnapshot(url, text) {
94
+ syncPageSnapshot(this.managers, url, text);
95
+ }
96
+ // --- monitor reporting tabs ------------------------------------------------
97
+ runSuggestion(id) {
98
+ runSuggestion(this.managers, id);
99
+ }
100
+ rateSuggestion(id, up) {
101
+ this.managers.monitor.rate(id, up);
102
+ }
103
+ resetMonitorContext(name) {
104
+ this.managers.monitor.resetContext(name);
105
+ }
106
+ monitorContextSnapshot(name) {
107
+ this.managers.monitor.snapshotContext(name);
108
+ }
109
+ // --- inline terminal cards (PTY) -----------------------------------------
110
+ ptyInput(id, data) { this.managers.pty.input(id, data); }
111
+ ptyResize(id, cols, rows) { this.managers.pty.resizeOne(id, cols, rows); }
112
+ ptyKill(id) { this.managers.pty.kill(id); }
113
+ resize(cols, rows) { this.managers.pty.resize(cols, rows); }
114
+ // --- tab management ------------------------------------------------------
115
+ setActiveTab(index) {
116
+ this.managers.tab.setActiveTab(index);
117
+ }
118
+ moveTab(dir) {
119
+ this.managers.tab.moveTab(dir);
120
+ }
121
+ reorderTab(dir) {
122
+ this.managers.tab.reorderTab(dir);
123
+ }
124
+ closeTab(index) {
125
+ this.managers.tab.closeTab(index);
126
+ }
127
+ renameTab(index, title) {
128
+ this.managers.tab.renameTab(index, title);
129
+ }
130
+ editQueuedCommand(index, text) {
131
+ this.managers.tab.editQueued(this.managers.tab.cur().label, index, text);
132
+ }
133
+ deleteQueuedCommand(index) {
134
+ this.managers.tab.deleteQueued(this.managers.tab.cur().label, index);
135
+ }
136
+ toggleCollapse() {
137
+ this.managers.tab.toggleCollapse();
138
+ }
139
+ // --- file tree tabs (see controller-file-tree.ts) ------------------------
140
+ fileTreeToggle(index, path) {
141
+ fileTreeRpc.fileTreeToggle(this.managers, index, path);
142
+ }
143
+ fileTreeCollapseAll(index) {
144
+ fileTreeRpc.fileTreeCollapseAll(this.managers, index);
145
+ }
146
+ fileTreeReroot(index, relPath) {
147
+ fileTreeRpc.fileTreeReroot(this.managers, index, relPath);
148
+ }
149
+ moveFileTreeItem(index, fromRelPath, toRelPath) {
150
+ fileTreeRpc.moveFileTreeItem(this.managers, index, fromRelPath, toRelPath);
151
+ }
152
+ deleteFileTreeItem(index, relPath) {
153
+ fileTreeRpc.deleteFileTreeItem(this.managers, index, relPath);
154
+ }
155
+ undoFileTreeItem(index, overwrite) {
156
+ return fileTreeRpc.undoFileTreeItem(this.managers, index, overwrite);
157
+ }
158
+ redoFileTreeItem(index, overwrite) {
159
+ return fileTreeRpc.redoFileTreeItem(this.managers, index, overwrite);
160
+ }
161
+ // Dock/undock any dockable tab (file tree or notifications). The mechanism is view-agnostic —
162
+ // `TabManager.setDock` operates on any tab index — so both kinds share this one handler.
163
+ setDock(index, dock) {
164
+ this.managers.tab.setDock(index, dock);
165
+ }
166
+ // Open (or retarget an existing) file navigator at the named tab's cwd — the 📁 metadata-row
167
+ // button (see controller-file-tree.ts).
168
+ openFileNavigatorFor(label) {
169
+ fileTreeRpc.openFileNavigatorFor(this.managers, label);
170
+ }
171
+ // Tab-completion for the command line (reuses the shared `completeCommandLine`): filesystem
172
+ // paths against the active tab's cwd, `msg`/`broadcast` agent names, `connection close` targets,
173
+ // and `browser` subcommands / window ids.
174
+ complete(text, cursor) {
175
+ const tab = this.managers.tab.cur();
176
+ const cwd = this.managers.tab.cwdOf(tab.label) ?? process.cwd();
177
+ const agents = this.managers.tab.allLabels();
178
+ // Monitor targets: every other action tab, plus `group:<n>` for each existing group.
179
+ const actionTabs = this.managers.tab.tabs.filter((t) => t.view !== 'monitor');
180
+ const groups = [...new Set(actionTabs.map((t) => t.group))].toSorted((a, b) => a - b).map((g) => `group:${g}`);
181
+ const targets = [...actionTabs.map((t) => t.label).filter((l) => l !== tab.label), ...groups];
182
+ return completeCommandLine(text, cursor, cwd, agents, this.managers.connection.completionConnections(tab.label), { personas: listPersonas(), targets });
183
+ }
184
+ // Canonical connection strings for `connection close` completion (shell/acp/browser/sqlite).
185
+ completionConnections(label) {
186
+ return this.managers.connection.completionConnections(label);
187
+ }
188
+ shutdown() {
189
+ this.managers.fileTree.dispose();
190
+ this.managers.editorWatch.dispose();
191
+ this.managers.monitor.closeAll();
192
+ messageBus.clear();
193
+ this.managers.schedule.stop();
194
+ this.managers.shell.closeAll();
195
+ this.managers.acp.closeAll();
196
+ this.managers.pty.closeAll();
197
+ this.managers.browser.closeAll();
198
+ this.managers.database.closeAll();
199
+ this.managers.workspace.removeAll();
200
+ }
201
+ }