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,22 @@
1
+ import { parseBroadcastCommand } from '../messaging.js';
2
+ export const command = {
3
+ name: 'broadcast',
4
+ match: (command_) => /^broadcast\b/i.test(command_),
5
+ run: (command_, tab, managers) => {
6
+ const parsed = parseBroadcastCommand(command_);
7
+ if ('error' in parsed) {
8
+ managers.tab.append(tab.label, { input: command_, output: parsed.error });
9
+ return;
10
+ }
11
+ const targets = parsed.targets === 'all'
12
+ ? managers.tab.allLabels().filter((l) => l !== tab.label)
13
+ : parsed.targets;
14
+ const missing = [];
15
+ for (const to of targets) {
16
+ if (!managers.communication.send({ from: tab.label, to, kind: parsed.kind, text: parsed.text }))
17
+ missing.push(to);
18
+ }
19
+ if (missing.length > 0)
20
+ managers.tab.append(tab.label, { input: command_, output: `No agent named: ${missing.join(', ')}.` });
21
+ },
22
+ };
@@ -0,0 +1,5 @@
1
+ export const command = {
2
+ name: 'browser',
3
+ match: (command_) => /^browser\b/i.test(command_),
4
+ run: (command, tab, managers) => { managers.browser.runInteractive(command, tab.label); },
5
+ };
@@ -0,0 +1,5 @@
1
+ export const command = {
2
+ name: 'clear',
3
+ match: (command_) => command_.toLowerCase() === 'clear',
4
+ run: (_command, tab, managers) => { managers.tab.clearTranscript(tab.label); },
5
+ };
@@ -0,0 +1,42 @@
1
+ // Parse a `close` command: bare `close` closes the active tab (quitting the app if it is the
2
+ // only tab); `close page <n>` closes the numbered page tab; `close <name>` closes the tab with
3
+ // that label. `exit` is an alias of `close`.
4
+ export function parseClose(command_) {
5
+ const rest = command_.replace(/^(?:close|exit)\b\s*/i, '').trim();
6
+ if (!rest)
7
+ return { target: 'active' };
8
+ const pageMatch = rest.match(/^page\b\s*(\d+)\s*$/i);
9
+ if (pageMatch)
10
+ return { target: 'page', number: Number(pageMatch[1]) };
11
+ return { target: 'tabname', name: rest.trim() };
12
+ }
13
+ export const command = {
14
+ name: 'close',
15
+ match: (command_) => /^(?:close|exit)\b/i.test(command_),
16
+ run: (command_, tab, managers) => {
17
+ const parsed = parseClose(command_);
18
+ if ('error' in parsed) {
19
+ managers.tab.append(tab.label, { input: command_, output: parsed.error });
20
+ return;
21
+ }
22
+ if (parsed.target === 'page') {
23
+ const pageTab = managers.tab.tabs.findIndex((t) => t.page?.number === parsed.number);
24
+ if (pageTab === -1) {
25
+ managers.tab.append(tab.label, { input: command_, output: `No page numbered ${parsed.number}.` });
26
+ return;
27
+ }
28
+ managers.tab.closeTab(pageTab);
29
+ }
30
+ else if (parsed.target === 'tabname') {
31
+ const nameTab = managers.tab.tabs.findIndex((t) => t.label.toLowerCase() === parsed.name.toLowerCase());
32
+ if (nameTab === -1) {
33
+ managers.tab.append(tab.label, { input: command_, output: `No tab named "${parsed.name}".` });
34
+ return;
35
+ }
36
+ managers.tab.closeTab(nameTab);
37
+ }
38
+ else {
39
+ managers.tab.closeTab(tab.index);
40
+ }
41
+ },
42
+ };
@@ -0,0 +1,5 @@
1
+ export const command = {
2
+ name: 'connection',
3
+ match: (command_) => /^connection\b/i.test(command_),
4
+ run: (command, tab, managers) => { managers.connection.run(command, tab.label); },
5
+ };
@@ -0,0 +1,5 @@
1
+ export const command = {
2
+ name: 'db',
3
+ match: (command_) => /^db\b/i.test(command_),
4
+ run: (command_, tab, managers) => { managers.tab.append(tab.label, { input: command_, output: managers.database.runInTab(tab.label, command_) }); },
5
+ };
@@ -0,0 +1,18 @@
1
+ // `edit <file>` — open the in-app plain-text editor on any file, regardless of which opener owns
2
+ // its extension (`open readme.md` keeps the rendered view; `edit readme.md` opens the editor).
3
+ export const command = {
4
+ name: 'edit',
5
+ match: (command_) => /^edit\b/i.test(command_),
6
+ run: (command, tab, managers) => {
7
+ const target = command.replace(/^edit\b\s*/i, '').trim();
8
+ if (!target) {
9
+ managers.tab.append(tab.label, { input: command, output: 'Usage: edit <file>' });
10
+ return;
11
+ }
12
+ const lineMatch = /^(.+):(\d+)$/.exec(target);
13
+ const path = lineMatch ? lineMatch[1] : target;
14
+ const line = lineMatch ? Number(lineMatch[2]) : undefined;
15
+ managers.tab.append(tab.label, { input: command, output: '' });
16
+ managers.openFile.edit(command, path, tab.label, line);
17
+ },
18
+ };
@@ -0,0 +1,13 @@
1
+ // `files [path]` opens a file tree tab rooted at the issuing tab's cwd, or at `path` when given.
2
+ // `files left [path]` / `files right [path]` dock the tree into that sidebar instead of the
3
+ // central tab strip (a directory literally named `left`/`right` is still reachable, e.g. `files
4
+ // ./left`, since the keyword is only recognized as the first word). `files in <label> [on <side>]`
5
+ // / `files on <side> [in <label>]` root the tree at another tab's cwd and/or dock it explicitly.
6
+ export const command = {
7
+ name: 'files',
8
+ match: (command_) => /^files\b/i.test(command_),
9
+ run: (command_, tab, managers) => {
10
+ managers.tab.append(tab.label, { input: command_, output: '' });
11
+ managers.fileTree.open(command_, tab.label);
12
+ },
13
+ };
@@ -0,0 +1,7 @@
1
+ export const command = {
2
+ name: 'hist',
3
+ match: (command_) => command_.toLowerCase() === 'hist',
4
+ // The history picker is interactive (client-side, Ctrl+R); reaching the server non-interactively
5
+ // (e.g. via a scheduled dispatch) is a no-op.
6
+ run: () => { },
7
+ };
@@ -0,0 +1,62 @@
1
+ import { command as agent } from './agent.js';
2
+ import { command as next } from './next.js';
3
+ import { command as message } from './msg.js';
4
+ import { command as broadcast } from './broadcast.js';
5
+ import { command as acp } from './acp.js';
6
+ import { command as acpReset } from './acp-reset.js';
7
+ import { command as database } from './db.js';
8
+ import { command as browser } from './browser.js';
9
+ import { command as connection } from './connection.js';
10
+ import { command as clear } from './clear.js';
11
+ import { command as state } from './state.js';
12
+ import { command as hist } from './hist.js';
13
+ import { command as close } from './close.js';
14
+ import { command as quit } from './quit.js';
15
+ import { command as schedule } from './schedule.js';
16
+ import { command as profile } from './profile.js';
17
+ import { command as open } from './open.js';
18
+ import { command as edit } from './edit.js';
19
+ import { command as send } from './send.js';
20
+ import { command as queue } from './queue.js';
21
+ import { command as rename } from './rename.js';
22
+ import { command as search } from './search.js';
23
+ import { command as files } from './files.js';
24
+ import { command as notifications } from './notifications.js';
25
+ import { command as notify } from './notify.js';
26
+ import { command as syntax } from './syntax.js';
27
+ import { command as theme } from './theme.js';
28
+ import { command as tasks } from './tasks.js';
29
+ import { monitor, unmonitor, monitors } from './monitor.js';
30
+ export const commands = [
31
+ agent,
32
+ next,
33
+ message,
34
+ broadcast,
35
+ acpReset,
36
+ acp,
37
+ database,
38
+ browser,
39
+ connection,
40
+ clear,
41
+ state,
42
+ hist,
43
+ close,
44
+ quit,
45
+ schedule,
46
+ profile,
47
+ open,
48
+ edit,
49
+ send,
50
+ queue,
51
+ rename,
52
+ search,
53
+ files,
54
+ notifications,
55
+ notify,
56
+ syntax,
57
+ theme,
58
+ tasks,
59
+ monitors,
60
+ monitor,
61
+ unmonitor,
62
+ ];
@@ -0,0 +1,61 @@
1
+ import { parseMonitorCommand, parseUnmonitorCommand } from '../monitor/parsing.js';
2
+ // `monitor <persona> [target...]` — start a persona-driven AI monitor. No targets:
3
+ // inline mode, watching the current tab and reporting into its transcript. With targets
4
+ // (tab labels or `group:<n>`): external mode, reporting into the persona's reporting tab.
5
+ // `monitor ask <persona> <question>` — query a running monitor's ACP directly.
6
+ export const monitor = {
7
+ name: 'monitor',
8
+ match: (command_) => /^monitor(\s|$)/i.test(command_),
9
+ run: (command_, tab, managers) => {
10
+ const out = (text) => managers.tab.append(tab.label, { input: command_, output: text });
11
+ const parsed = parseMonitorCommand(command_);
12
+ if ('error' in parsed) {
13
+ out(parsed.error);
14
+ return;
15
+ }
16
+ if ('ask' in parsed) {
17
+ const askError = managers.monitor.ask(tab.label, parsed.persona, parsed.question);
18
+ if (askError)
19
+ out(askError);
20
+ return;
21
+ }
22
+ const error = managers.monitor.start(tab.label, parsed.persona, parsed.targets);
23
+ if (error) {
24
+ out(error);
25
+ return;
26
+ }
27
+ const watched = parsed.targets.length === 0
28
+ ? tab.label
29
+ : parsed.targets.map((t) => (t.kind === 'tab' ? t.label : `group ${t.group}`)).join(', ');
30
+ out(`→ Now monitoring ${watched} (persona: ${parsed.persona})`);
31
+ },
32
+ };
33
+ // `unmonitor <persona> [target]` / `unmonitor --all` — stop monitors started from this tab.
34
+ export const unmonitor = {
35
+ name: 'unmonitor',
36
+ match: (command_) => /^unmonitor(\s|$)/i.test(command_),
37
+ run: (command_, tab, managers) => {
38
+ const out = (text) => managers.tab.append(tab.label, { input: command_, output: text });
39
+ const parsed = parseUnmonitorCommand(command_);
40
+ if ('error' in parsed) {
41
+ out(parsed.error);
42
+ return;
43
+ }
44
+ if ('all' in parsed) {
45
+ const stopped = managers.monitor.stopAll(tab.label);
46
+ out(stopped > 0 ? `→ Stopped ${stopped} monitor${stopped === 1 ? '' : 's'}` : 'No monitors running from this tab.');
47
+ return;
48
+ }
49
+ const stopped = managers.monitor.stop(tab.label, parsed.persona, parsed.target);
50
+ out(stopped ? `→ Stopped ${parsed.persona} monitor` : `No "${parsed.persona}" monitor running from this tab.`);
51
+ },
52
+ };
53
+ // `monitors` — list all active monitors.
54
+ export const monitors = {
55
+ name: 'monitors',
56
+ match: (command_) => /^monitors$/i.test(command_),
57
+ run: (command_, tab, managers) => {
58
+ const lines = managers.monitor.list();
59
+ managers.tab.append(tab.label, { input: command_, output: lines.length > 0 ? lines.join('\n') : 'No active monitors.' });
60
+ },
61
+ };
@@ -0,0 +1,17 @@
1
+ import { parseMsgCommand as parseMessageCommand } from '../messaging.js';
2
+ export const command = {
3
+ name: 'msg',
4
+ match: (command_) => /^msg\b/i.test(command_),
5
+ run: (command_, tab, managers) => {
6
+ const parsed = parseMessageCommand(command_);
7
+ if ('error' in parsed) {
8
+ managers.tab.append(tab.label, { input: command_, output: parsed.error });
9
+ return;
10
+ }
11
+ if (!managers.communication.send({ from: tab.label, to: parsed.to, kind: parsed.kind, text: parsed.text })) {
12
+ managers.tab.append(tab.label, { input: command_, output: `No agent named "${parsed.to}".` });
13
+ return;
14
+ }
15
+ managers.tab.append(tab.label, { input: command_, output: `→ ${parsed.to} (${parsed.kind}): ${parsed.text}` });
16
+ },
17
+ };
@@ -0,0 +1,7 @@
1
+ export const command = {
2
+ name: 'next',
3
+ match: (command_) => command_.toLowerCase() === 'next',
4
+ run: (_command, context, managers) => {
5
+ managers.tab.setActiveTab((managers.tab.activeTab + 1) % managers.tab.tabs.length);
6
+ },
7
+ };
@@ -0,0 +1,16 @@
1
+ import { openNotificationsTab } from '../notifications-tab.js';
2
+ // `notifications` opens (or focuses) the singleton notifications tab. `notifications left` /
3
+ // `notifications right` dock it into that sidebar, mirroring `files [left|right]`. Bare
4
+ // `notifications` on a docked tab undocks it back to center and makes it active. The tab is a
5
+ // singleton — a second invocation reuses the existing one.
6
+ export const command = {
7
+ name: 'notifications',
8
+ match: (command_) => /^notifications\b/i.test(command_),
9
+ run: (command_, tab, managers) => {
10
+ managers.tab.append(tab.label, { input: command_, output: '' });
11
+ const rest = command_.replace(/^notifications\b\s*/i, '');
12
+ const keyword = /^(left|right)\b/i.exec(rest);
13
+ const dock = keyword ? keyword[1].toLowerCase() : undefined;
14
+ openNotificationsTab(managers, dock);
15
+ },
16
+ };
@@ -0,0 +1,20 @@
1
+ import { notify } from '../notifications.js';
2
+ // `notify <message>` pushes a custom line into the notifications feed, attributed to the issuing
3
+ // tab. It is the deliberate counterpart to the four ambient events: an explicit signal that
4
+ // bypasses focus suppression and the per-event toggles (an agent that calls `notify` always means
5
+ // to signal, even from the focused tab). It never opens the tab — if the feed is closed the
6
+ // message is dropped (drop-if-closed). Available from any tab, agents included. `notify` with no
7
+ // message is a usage error and records nothing in the feed.
8
+ export const command = {
9
+ name: 'notify',
10
+ match: (command_) => /^notify\b/i.test(command_),
11
+ run: (command_, tab, managers) => {
12
+ const message = command_.replace(/^notify\b\s*/i, '').trim();
13
+ managers.tab.append(tab.label, { input: command_, output: '' });
14
+ if (!message) {
15
+ managers.tab.append(tab.label, { input: '', output: 'Usage: notify <message>.' });
16
+ return;
17
+ }
18
+ notify(managers, 'manual', tab.label, message);
19
+ },
20
+ };
@@ -0,0 +1,41 @@
1
+ // Parse an `open` command line. The leading `open` keyword is stripped; the optional `external` and
2
+ // `page` keywords (in any order) are consumed; everything remaining is the target. A target with an
3
+ // http/https scheme, or preceded by `page`, is routed to the web opener (`web: true`); otherwise it
4
+ // goes to the file opener. The target is kept verbatim (paths may contain spaces).
5
+ export function parseOpen(command_) {
6
+ let rest = command_.replace(/^open\b\s*/i, '');
7
+ let isExternal = false;
8
+ let isPage = false;
9
+ // Consume optional keywords in any order.
10
+ let changed = true;
11
+ while (changed) {
12
+ changed = false;
13
+ const extMatch = rest.match(/^external\b\s*/i);
14
+ if (extMatch) {
15
+ isExternal = true;
16
+ rest = rest.slice(extMatch[0].length);
17
+ changed = true;
18
+ }
19
+ const pageMatch = rest.match(/^page\b\s*/i);
20
+ if (pageMatch) {
21
+ isPage = true;
22
+ rest = rest.slice(pageMatch[0].length);
23
+ changed = true;
24
+ }
25
+ }
26
+ const target = rest.trim();
27
+ if (!target)
28
+ return { error: 'Usage: open [external] [page] <target>' };
29
+ const web = isPage || /^https?:\/\//i.test(target);
30
+ return { external: isExternal, web, target };
31
+ }
32
+ // Whether an `open` argument is a shell wildcard pattern (expanded to a list of files) rather than a
33
+ // single literal path. Detects the common glob metacharacters: `* ? [ ] { }`.
34
+ export function isGlobPattern(argument) {
35
+ return /[*?[\]{}]/.test(argument);
36
+ }
37
+ export const command = {
38
+ name: 'open',
39
+ match: (command_) => /^open\b/i.test(command_),
40
+ run: (command, tab, managers) => { managers.openFile.run(command, tab.label); },
41
+ };
@@ -0,0 +1,5 @@
1
+ export const command = {
2
+ name: 'profile',
3
+ match: (command_) => /^profile\b/i.test(command_),
4
+ run: (command, tab, managers) => { managers.profile.run(command, tab.label); },
5
+ };
@@ -0,0 +1,37 @@
1
+ import { resolveTarget } from './resolve-target.js';
2
+ /** Parse a `queue <agent> <command...>` command (the leading `queue` is optional). */
3
+ export function parseQueueCommand(input) {
4
+ const body = input.trim().replace(/^queue\b\s*/i, '');
5
+ const parts = body.split(/\s+/).filter(Boolean);
6
+ const label = parts[0];
7
+ const text = parts.slice(1).join(' ');
8
+ if (!label || !text)
9
+ return { error: 'Usage: queue <agent> <command>' };
10
+ return { label, text };
11
+ }
12
+ export const command = {
13
+ name: 'queue',
14
+ match: (command_) => /^queue\b/i.test(command_),
15
+ run: (command_, tab, managers) => {
16
+ // Bare `queue` is the interactive picker (Ctrl+E), handled client-side; reaching the server
17
+ // non-interactively (e.g. via a scheduled dispatch) is a no-op.
18
+ if (/^queue\s*$/i.test(command_.trim()))
19
+ return;
20
+ const append = (text) => managers.tab.append(tab.label, { input: command_, output: text });
21
+ const parsed = parseQueueCommand(command_);
22
+ if ('error' in parsed) {
23
+ append(parsed.error);
24
+ return;
25
+ }
26
+ const target = resolveTarget(parsed.label, managers, append);
27
+ if (!target)
28
+ return;
29
+ if (target.view !== undefined && target.view !== 'agent') {
30
+ append(`Tab "${parsed.label}" has no command queue.`);
31
+ return;
32
+ }
33
+ managers.tab.enqueue(target.label, parsed.text);
34
+ managers.command.drainQueue(target.label);
35
+ append(`→ ${parsed.label} (queued): ${parsed.text}`);
36
+ },
37
+ };
@@ -0,0 +1,6 @@
1
+ import { messageBus } from '../bus.js';
2
+ export const command = {
3
+ name: 'quit',
4
+ match: (command_) => command_.trim().toLowerCase() === 'quit',
5
+ run: () => { messageBus.emit('app', { type: 'exit' }); },
6
+ };
@@ -0,0 +1,12 @@
1
+ export const command = {
2
+ name: 'rename',
3
+ match: (command_) => /^rename\b/i.test(command_),
4
+ run: (command_, tab, managers) => {
5
+ const rest = command_.replace(/^rename\b\s*/i, '').trim();
6
+ managers.tab.renameTab(tab.index, rest);
7
+ const output = rest
8
+ ? `Tab "${tab.label}" now displays as "${rest}" (msg/routing still use "${tab.label}").`
9
+ : `Tab "${tab.label}" alias cleared.`;
10
+ managers.tab.append(tab.label, { input: command_, output });
11
+ },
12
+ };
@@ -0,0 +1,12 @@
1
+ // Resolve a command's target tab by its label or display alias (see `rename`), appending the
2
+ // standard "not found" message and returning undefined if there's no match — shared by
3
+ // `queue` and `send`, which both address a tab this way.
4
+ export function resolveTarget(label, managers, append) {
5
+ const key = label.toLowerCase();
6
+ const target = managers.tab.tabs.find((t) => t.label.toLowerCase() === key || t.title?.toLowerCase() === key);
7
+ if (!target) {
8
+ append(`No tab named "${label}".`);
9
+ return undefined;
10
+ }
11
+ return target;
12
+ }
@@ -0,0 +1,74 @@
1
+ import { parseScheduleCommand, formatSchedule } from '../schedule/index.js';
2
+ // Resolve the tab a schedule operation applies to: the issuing tab by default, or the
3
+ // `in <tab>` target. Agent and harness tabs can hold schedules; image/page/markdown views
4
+ // cannot run commands, so scheduling into them is rejected.
5
+ function resolveTargetTab(target, own, managers) {
6
+ const label = target ?? own;
7
+ const tab = managers.tab.tabs.find((t) => t.label === label);
8
+ if (!tab)
9
+ return { error: `No tab named "${label}".` };
10
+ if (tab.view !== undefined && tab.view !== 'agent' && tab.view !== 'harness') {
11
+ return { error: `Tab "${label}" cannot run scheduled commands.` };
12
+ }
13
+ return tab;
14
+ }
15
+ // Persist a tab's schedule alongside its agent state. Harness tabs have no persisted agent
16
+ // state (they cannot be rehydrated), so their schedules live in memory only.
17
+ function persistSchedule(tab, managers) {
18
+ if (tab.view === 'harness')
19
+ return;
20
+ managers.tab.persist(managers.tab.buildAgentState(tab, { schedule: managers.schedule.get(tab.label) }));
21
+ }
22
+ export const command = {
23
+ name: 'schedule',
24
+ match: (command_) => /^schedule\b/i.test(command_),
25
+ run: (command_, tab, managers) => {
26
+ const parsed = parseScheduleCommand(command_.replace(/^schedule\b\s*/i, ''), new Date());
27
+ const append = (text) => managers.tab.append(tab.label, { input: command_, output: text });
28
+ if ('error' in parsed) {
29
+ append(parsed.error);
30
+ return;
31
+ }
32
+ const target = resolveTargetTab(parsed.target, tab.label, managers);
33
+ if ('error' in target) {
34
+ append(target.error);
35
+ return;
36
+ }
37
+ const suffix = target.label === tab.label ? '' : ` in ${target.label}`;
38
+ const current = managers.schedule.get(target.label) ?? [];
39
+ if (parsed.action === 'list') {
40
+ append(formatSchedule(current));
41
+ return;
42
+ }
43
+ let next;
44
+ let message;
45
+ if (parsed.action === 'add') {
46
+ if (current.some((e) => e.id === parsed.name)) {
47
+ append(`A scheduled command named "${parsed.name}" already exists${suffix}.`);
48
+ return;
49
+ }
50
+ const entry = { ...parsed.entry, id: parsed.name };
51
+ next = [...current, entry];
52
+ message = `Scheduled ${entry.id}${suffix}: ${entry.spec} — ${entry.command}`;
53
+ }
54
+ else if (parsed.action === 'cancel') {
55
+ next = current.filter((e) => e.id !== parsed.id);
56
+ if (next.length === current.length) {
57
+ append(`No scheduled command "${parsed.id}"${suffix}.`);
58
+ return;
59
+ }
60
+ message = `Cancelled ${parsed.id}${suffix}.`;
61
+ }
62
+ else {
63
+ if (current.length === 0) {
64
+ append(`No scheduled commands${suffix}.`);
65
+ return;
66
+ }
67
+ next = [];
68
+ message = `Cleared ${current.length} scheduled command${current.length === 1 ? '' : 's'}${suffix}.`;
69
+ }
70
+ managers.schedule.set(target.label, next);
71
+ persistSchedule(target, managers);
72
+ append(message);
73
+ },
74
+ };
@@ -0,0 +1,26 @@
1
+ import { flattenBuffer } from '../tab/formatting.js';
2
+ import { compilePattern, findMatches } from '../search-matches.js';
3
+ export const SEARCH_USAGE = 'Usage: search transcript <pattern>';
4
+ // Server half of transcript search: the web client handles matching/highlighting itself
5
+ // (it already holds the flattened transcript), so this command only covers the no-match
6
+ // report and non-interactive dispatch (e.g. scripted/`send`-delivered commands).
7
+ export const command = {
8
+ name: 'search',
9
+ match: (command_) => /^search\s+transcript\b/i.test(command_),
10
+ run: (command_, tab, managers) => {
11
+ const pattern = command_.replace(/^search\s+transcript\b\s*/i, '').trim();
12
+ const append = (output) => managers.tab.append(tab.label, { input: command_, output });
13
+ if (!pattern || !compilePattern(pattern)) {
14
+ append(SEARCH_USAGE);
15
+ return;
16
+ }
17
+ const target = managers.tab.tabs.find((t) => t.label === tab.label);
18
+ const lines = flattenBuffer(target?.log ?? []);
19
+ const matches = findMatches(lines, pattern);
20
+ if (matches.length === 0) {
21
+ append('No matches found in the transcript.');
22
+ return;
23
+ }
24
+ append(lines[matches.at(-1)].text);
25
+ },
26
+ };
@@ -0,0 +1,51 @@
1
+ import { resolveTarget } from './resolve-target.js';
2
+ /** Parse a `send <label> <text...>` command (the leading `send` is optional). */
3
+ export function parseSendCommand(input) {
4
+ const body = input.trim().replace(/^send\b\s*/i, '');
5
+ const parts = body.split(/\s+/).filter(Boolean);
6
+ if (parts.length === 0)
7
+ return { error: 'Usage: send <label> <text>' };
8
+ const label = parts[0];
9
+ const text = parts.slice(1).join(' ');
10
+ if (!text)
11
+ return { error: 'No text to send.' };
12
+ return { label, text };
13
+ }
14
+ function deliverTo(target, text, managers) {
15
+ if (target.view === 'harness') {
16
+ if (target.harness?.status !== 'running')
17
+ return `Tab "${target.label}" is not a running harness.`;
18
+ // Split from the text so a long line's trailing \r can't land in the same burst the harness's
19
+ // own input parser treats as a paste (see schedule-manager.ts's `fire`).
20
+ const ptyId = target.harness.ptyId;
21
+ managers.pty.input(ptyId, text);
22
+ setTimeout(() => managers.pty.input(ptyId, '\r'), 50);
23
+ return null;
24
+ }
25
+ if (target.view === undefined || target.view === 'agent') {
26
+ managers.command.dispatchTo(target.label, text);
27
+ return null;
28
+ }
29
+ return `Tab "${target.label}" does not accept input.`;
30
+ }
31
+ export const command = {
32
+ name: 'send',
33
+ match: (command_) => /^send\b/i.test(command_),
34
+ run: (command_, tab, managers) => {
35
+ const append = (text) => managers.tab.append(tab.label, { input: command_, output: text });
36
+ const parsed = parseSendCommand(command_);
37
+ if ('error' in parsed) {
38
+ append(parsed.error);
39
+ return;
40
+ }
41
+ const target = resolveTarget(parsed.label, managers, append);
42
+ if (!target)
43
+ return;
44
+ const error = deliverTo(target, parsed.text, managers);
45
+ if (error) {
46
+ append(error);
47
+ return;
48
+ }
49
+ append(`→ ${parsed.label}: ${parsed.text}`);
50
+ },
51
+ };
@@ -0,0 +1,9 @@
1
+ import { loadAgentState } from '../agent/state.js';
2
+ import { formatState } from '../state-format.js';
3
+ export const command = {
4
+ name: 'state',
5
+ match: (command_) => command_.toLowerCase() === 'state',
6
+ run: (command, tab, managers) => {
7
+ managers.tab.append(tab.label, { input: command, output: formatState(tab.label, loadAgentState(tab.label) ?? null) });
8
+ },
9
+ };
@@ -0,0 +1,36 @@
1
+ import { getConfig, updateConfig } from '../config.js';
2
+ import { SYNTAX_THEMES } from '../syntax-themes.js';
3
+ const USAGE = 'Usage: syntax theme [name]';
4
+ function listThemes() {
5
+ const active = getConfig().syntaxTheme;
6
+ return SYNTAX_THEMES.map((name) => (name === active ? `* ${name}` : ` ${name}`)).join('\n');
7
+ }
8
+ export const command = {
9
+ name: 'syntax',
10
+ match: (command_) => /^syntax\b/i.test(command_),
11
+ run: (command_, tab, managers) => {
12
+ const rest = command_.replace(/^syntax\b\s*/i, '').trim();
13
+ if (!/^theme\b/i.test(rest)) {
14
+ managers.tab.append(tab.label, { input: command_, output: USAGE });
15
+ return;
16
+ }
17
+ const name = rest.replace(/^theme\b\s*/i, '').trim();
18
+ if (!name) {
19
+ managers.tab.append(tab.label, { input: command_, output: `Available themes:\n${listThemes()}` });
20
+ return;
21
+ }
22
+ const canonical = SYNTAX_THEMES.find((theme) => theme.toLowerCase() === name.toLowerCase());
23
+ if (!canonical) {
24
+ managers.tab.append(tab.label, {
25
+ input: command_,
26
+ output: `Unknown theme "${name}". Available themes:\n${listThemes()}`,
27
+ });
28
+ return;
29
+ }
30
+ const persisted = updateConfig({ syntaxTheme: canonical });
31
+ const output = persisted
32
+ ? `Syntax theme set to "${canonical}".`
33
+ : `Syntax theme set to "${canonical}" for this session (config write failed — won't persist).`;
34
+ managers.tab.append(tab.label, { input: command_, output });
35
+ },
36
+ };