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,7 @@
1
+ export const command = {
2
+ name: 'tasks',
3
+ match: (command_) => command_.toLowerCase() === 'tasks',
4
+ // The task picker is interactive (client-side, Ctrl+A); reaching the server non-interactively
5
+ // (e.g. via a scheduled dispatch) is a no-op.
6
+ run: () => { },
7
+ };
@@ -0,0 +1,41 @@
1
+ import { getConfig, updateConfig } from '../config.js';
2
+ import { APP_THEMES } from '../app-themes.js';
3
+ import { SYNTAX_THEMES } from '../syntax-themes.js';
4
+ function listThemes() {
5
+ const active = getConfig().theme;
6
+ return APP_THEMES.map((name) => (name === active ? `* ${name}` : ` ${name}`)).join('\n');
7
+ }
8
+ // `theme sync`: set the syntax theme to the app theme's name, when a syntax theme by that exact
9
+ // name exists (the two name sets barely overlap today — only `nord` matches).
10
+ function syncSyntaxTheme() {
11
+ const appTheme = getConfig().theme;
12
+ const canonical = SYNTAX_THEMES.find((theme) => theme.toLowerCase() === appTheme.toLowerCase());
13
+ if (!canonical)
14
+ return `No syntax theme named "${appTheme}" exists — syntax theme unchanged.`;
15
+ const persisted = updateConfig({ syntaxTheme: canonical });
16
+ return persisted
17
+ ? `Syntax theme set to "${canonical}".`
18
+ : `Syntax theme set to "${canonical}" for this session (config write failed — won't persist).`;
19
+ }
20
+ function setTheme(name) {
21
+ const canonical = APP_THEMES.find((theme) => theme.toLowerCase() === name.toLowerCase());
22
+ if (!canonical)
23
+ return `Unknown theme "${name}". Available themes:\n${listThemes()}`;
24
+ const persisted = updateConfig({ theme: canonical });
25
+ return persisted
26
+ ? `Theme set to "${canonical}".`
27
+ : `Theme set to "${canonical}" for this session (config write failed — won't persist).`;
28
+ }
29
+ export const command = {
30
+ name: 'theme',
31
+ match: (command_) => /^theme\b/i.test(command_),
32
+ run: (command_, tab, managers) => {
33
+ const rest = command_.replace(/^theme\b\s*/i, '').trim();
34
+ if (!rest) {
35
+ managers.tab.append(tab.label, { input: command_, output: `Available themes:\n${listThemes()}` });
36
+ return;
37
+ }
38
+ const output = /^sync$/i.test(rest) ? syncSyntaxTheme() : setTheme(rest);
39
+ managers.tab.append(tab.label, { input: command_, output });
40
+ },
41
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ export { resolveAgentName, parseAgentCommand } from './agent/commands.js';
5
+ export const availableCommands = [
6
+ 'help',
7
+ 'state',
8
+ 'clear',
9
+ 'close',
10
+ 'hist',
11
+ 'quit',
12
+ 'agent',
13
+ 'msg',
14
+ 'broadcast',
15
+ 'acp',
16
+ 'db',
17
+ 'connection',
18
+ 'harness',
19
+ 'ssh',
20
+ 'search',
21
+ 'files',
22
+ 'notifications',
23
+ 'notify',
24
+ 'syntax',
25
+ ];
26
+ let helpOutput = null;
27
+ function buildHelp() {
28
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
+ const helpPath = path.join(__dirname, '..', 'help.md');
30
+ try {
31
+ return readFileSync(helpPath, 'utf8').trim();
32
+ }
33
+ catch {
34
+ return 'Built-in: ' + availableCommands.join(', ') + '. Prefix a command with "shell " to run it in the shell, or / to run a built-in command. Press Ctrl+R or type hist to browse command history.';
35
+ }
36
+ }
37
+ export const getOutput = (command) => {
38
+ const trimmed = command.trim().toLowerCase();
39
+ if (trimmed === 'help') {
40
+ if (!helpOutput)
41
+ helpOutput = buildHelp();
42
+ return helpOutput;
43
+ }
44
+ if (trimmed === 'clear')
45
+ return null;
46
+ if (trimmed === 'state')
47
+ return null;
48
+ if (trimmed === 'hist')
49
+ return null;
50
+ if (['quit', 'exit', 'close'].includes(trimmed))
51
+ return null;
52
+ if (trimmed.startsWith('agent'))
53
+ return null;
54
+ if (trimmed.startsWith('msg'))
55
+ return null;
56
+ if (trimmed.startsWith('broadcast'))
57
+ return null;
58
+ if (trimmed.startsWith('acp'))
59
+ return null;
60
+ if (/^db\b/.test(trimmed))
61
+ return null;
62
+ if (/^connection\b/.test(trimmed))
63
+ return null;
64
+ if (trimmed === 'next')
65
+ return null;
66
+ if (trimmed === '')
67
+ return null;
68
+ return `Unknown command: "${trimmed}". Type "help" for available commands.`;
69
+ };
70
+ export { default as agentNames } from '../agent-names.json' with { type: 'json' };
@@ -0,0 +1,24 @@
1
+ import { completeWord } from './helpers.js';
2
+ import { BROWSER_SUBCOMMANDS } from '../browser/command.js';
3
+ export function completeBrowserCommand(command, argumentIndex, preceding, token, connections, before, after, tokenStart) {
4
+ if (command !== 'browser') {
5
+ return null;
6
+ }
7
+ if (argumentIndex === 1) {
8
+ return completeWord(token, '', BROWSER_SUBCOMMANDS, ' ', before, after, tokenStart);
9
+ }
10
+ const sub = preceding[1]?.toLowerCase();
11
+ const windowIds = connections
12
+ .filter((c) => c.startsWith('browser:'))
13
+ .map((c) => c.slice('browser:'.length));
14
+ if (argumentIndex === 2 && sub === 'use') {
15
+ return completeWord(token, '', windowIds, ' ', before, after, tokenStart);
16
+ }
17
+ if (argumentIndex === 2 && sub === 'window') {
18
+ return completeWord(token, '', ['close'], ' ', before, after, tokenStart);
19
+ }
20
+ if (argumentIndex === 3 && sub === 'window' && preceding[2]?.toLowerCase() === 'close') {
21
+ return completeWord(token, '', windowIds, ' ', before, after, tokenStart);
22
+ }
23
+ return null;
24
+ }
@@ -0,0 +1,26 @@
1
+ import path from 'node:path';
2
+ import { readdirSync } from 'node:fs';
3
+ import { isDir, longestCommonPrefix, splitToken, replaceToken } from './helpers.js';
4
+ export function completeFilePath(token, cwd, before, after, tokenStart, input, cursor) {
5
+ const { dir, base } = splitToken(token, cwd);
6
+ let entries;
7
+ try {
8
+ entries = readdirSync(dir);
9
+ }
10
+ catch {
11
+ return { newInput: input, newCursor: cursor, matches: [] };
12
+ }
13
+ const matches = entries
14
+ .filter((entry) => entry.startsWith(base) && (base.startsWith('.') || !entry.startsWith('.')))
15
+ .toSorted((a, b) => a.localeCompare(b));
16
+ if (matches.length === 0)
17
+ return { newInput: input, newCursor: cursor, matches: [] };
18
+ let completedName = longestCommonPrefix(matches);
19
+ let suffix = '';
20
+ if (matches.length === 1) {
21
+ completedName = matches[0];
22
+ suffix = isDir(path.join(dir, matches[0])) ? '/' : ' ';
23
+ }
24
+ const typedDirPrefix = token.slice(0, token.length - base.length);
25
+ return replaceToken(before, after, tokenStart, typedDirPrefix + completedName + suffix, matches);
26
+ }
@@ -0,0 +1,81 @@
1
+ import { completeWord } from './helpers.js';
2
+ import { modelsFor } from '../harness/models.js';
3
+ export { completeBrowserCommand } from './browser.js';
4
+ export function completeAgentName(command, argumentIndex, token, agents, before, after, tokenStart) {
5
+ if (argumentIndex !== 1 || (command !== 'msg' && command !== 'broadcast')) {
6
+ return null;
7
+ }
8
+ if (command === 'broadcast') {
9
+ const segStart = token.lastIndexOf(',') + 1;
10
+ return completeWord(token.slice(segStart), token.slice(0, segStart), [...agents, 'all'], '', before, after, tokenStart);
11
+ }
12
+ return completeWord(token, '', agents, ' ', before, after, tokenStart);
13
+ }
14
+ export function completeSendTarget(command, argumentIndex, token, labels, before, after, tokenStart) {
15
+ if (argumentIndex !== 1 || !['send', 'queue', 'close', 'exit'].includes(command))
16
+ return null;
17
+ return completeWord(token, '', labels, ' ', before, after, tokenStart);
18
+ }
19
+ // Complete the target of a `schedule … in <tab>` clause against open tab labels. The clause
20
+ // sits right after the timer name (or `list`/`clear`) at argument 3, or after `cancel <name>`
21
+ // at argument 4 — anywhere else the word `in` belongs to the scheduled command itself.
22
+ export function completeScheduleTarget(command, argumentIndex, preceding, token, labels, before, after, tokenStart) {
23
+ if (command !== 'schedule' || preceding.at(-1)?.toLowerCase() !== 'in')
24
+ return null;
25
+ const isClause = argumentIndex === 3 || (argumentIndex === 4 && preceding[1]?.toLowerCase() === 'cancel');
26
+ if (!isClause)
27
+ return null;
28
+ return completeWord(token, '', labels, ' ', before, after, tokenStart);
29
+ }
30
+ // Complete `monitor`/`unmonitor` arguments: the first argument is a persona name (or
31
+ // `ask` / `--all`), later arguments are targets (tab labels or `group:<n>` tokens) —
32
+ // except after `monitor ask`, where the persona comes second.
33
+ export function completeMonitorCommand(command, argumentIndex, preceding, token, monitor, before, after, tokenStart) {
34
+ if (!monitor || (command !== 'monitor' && command !== 'unmonitor'))
35
+ return null;
36
+ if (argumentIndex === 1) {
37
+ const extra = command === 'unmonitor' ? ['--all'] : ['ask'];
38
+ return completeWord(token, '', [...monitor.personas, ...extra], ' ', before, after, tokenStart);
39
+ }
40
+ if (argumentIndex === 2 && command === 'monitor' && preceding[1]?.toLowerCase() === 'ask') {
41
+ return completeWord(token, '', monitor.personas, ' ', before, after, tokenStart);
42
+ }
43
+ if (argumentIndex >= 2 && preceding[1]?.toLowerCase() !== 'ask') {
44
+ return completeWord(token, '', monitor.targets, ' ', before, after, tokenStart);
45
+ }
46
+ return null;
47
+ }
48
+ // Complete `search transcript` — the only subcommand, so argument 1 always offers it.
49
+ export function completeSearchCommand(command, argumentIndex, token, before, after, tokenStart) {
50
+ if (argumentIndex !== 1 || command !== 'search')
51
+ return null;
52
+ return completeWord(token, '', ['transcript'], ' ', before, after, tokenStart);
53
+ }
54
+ // Complete `syntax theme <name>` — argument 1 offers `theme`, argument 2 completes theme names.
55
+ export function completeSyntaxTheme(command, argumentIndex, preceding, token, themes, before, after, tokenStart) {
56
+ if (command !== 'syntax')
57
+ return null;
58
+ if (argumentIndex === 1)
59
+ return completeWord(token, '', ['theme'], ' ', before, after, tokenStart);
60
+ if (argumentIndex === 2 && preceding[1]?.toLowerCase() === 'theme') {
61
+ return completeWord(token, '', themes, ' ', before, after, tokenStart);
62
+ }
63
+ return null;
64
+ }
65
+ // Complete `harness <name> ... --model <partial>` against the harness's known model catalog.
66
+ // The flag can appear anywhere after the harness name, so match on the token immediately
67
+ // preceding the cursor rather than a fixed argument index (mirrors completeScheduleTarget).
68
+ export function completeHarnessModel(command, preceding, token, before, after, tokenStart) {
69
+ if (command !== 'harness' || preceding.at(-1)?.toLowerCase() !== '--model')
70
+ return null;
71
+ const harnessName = preceding[1]?.toLowerCase();
72
+ if (!harnessName)
73
+ return null;
74
+ return completeWord(token, '', modelsFor(harnessName), ' ', before, after, tokenStart);
75
+ }
76
+ export function completeConnectionClose(command, argumentIndex, preceding, token, connections, before, after, tokenStart) {
77
+ if (argumentIndex !== 2 || command !== 'connection' || preceding[1]?.toLowerCase() !== 'close') {
78
+ return null;
79
+ }
80
+ return completeWord(token, '', connections, ' ', before, after, tokenStart);
81
+ }
@@ -0,0 +1,43 @@
1
+ import { statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ export const isDir = (p) => {
5
+ try {
6
+ return statSync(p).isDirectory();
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ };
12
+ export const longestCommonPrefix = (items) => {
13
+ if (items.length === 0)
14
+ return '';
15
+ let prefix = items[0];
16
+ for (const item of items.slice(1)) {
17
+ while (!item.startsWith(prefix))
18
+ prefix = prefix.slice(0, -1);
19
+ if (!prefix)
20
+ break;
21
+ }
22
+ return prefix;
23
+ };
24
+ export const splitToken = (token, cwd) => {
25
+ const expanded = token.startsWith('~') ? homedir() + token.slice(1) : token;
26
+ const slash = expanded.lastIndexOf('/');
27
+ if (slash !== -1) {
28
+ const dirPart = expanded.slice(0, slash + 1) || '/';
29
+ return { dir: path.resolve(cwd, dirPart), base: expanded.slice(slash + 1) };
30
+ }
31
+ return { dir: cwd, base: expanded };
32
+ };
33
+ export const replaceToken = (before, after, tokenStart, newToken, matches) => {
34
+ const newBefore = before.slice(0, tokenStart) + newToken;
35
+ return { newInput: newBefore + after, newCursor: newBefore.length, matches };
36
+ };
37
+ export const completeWord = (partial, keepPrefix, candidates, suffix, before, after, tokenStart) => {
38
+ const matches = candidates.filter((c) => c.startsWith(partial)).toSorted((a, b) => a.localeCompare(b));
39
+ if (matches.length === 0)
40
+ return { newInput: before + after, newCursor: before.length, matches: [] };
41
+ const completed = matches.length === 1 ? matches[0] + suffix : longestCommonPrefix(matches);
42
+ return replaceToken(before, after, tokenStart, keepPrefix + completed, matches);
43
+ };
@@ -0,0 +1,43 @@
1
+ import { completeAgentName, completeSendTarget, completeScheduleTarget, completeConnectionClose, completeBrowserCommand, completeMonitorCommand, completeSearchCommand, completeSyntaxTheme, completeHarnessModel } from './handlers.js';
2
+ import { completeFilePath } from './fs.js';
3
+ import { SYNTAX_THEMES } from '../syntax-themes.js';
4
+ /**
5
+ * Tab-complete the token ending at the cursor.
6
+ *
7
+ * - For the recipient argument of `msg`/`broadcast`, completes against active agent names
8
+ * (`broadcast` also offers `all` and supports a comma-separated list).
9
+ * - For the target argument of `send`, `queue`, `close`/`exit`, and the `in <tab>` clause of
10
+ * `schedule`, completes against all open tab labels.
11
+ * - For the target of `connection close`, completes against open connection strings
12
+ * (e.g. `sqlite:movies`, `shell:bash`, `acp:opencode`, `browser:w1`).
13
+ * - For the `browser` command, completes subcommands and, where a window id is expected
14
+ * (`browser use`, `browser window close`), the current tab's open window ids.
15
+ * - Otherwise completes a filesystem path relative to `cwd`.
16
+ *
17
+ * A single match is filled in fully; multiple matches fill in their longest common prefix
18
+ * and are returned via `matches` so the caller can display the options.
19
+ */
20
+ export function completeCommandLine(input, cursor, cwd, agents = [], connections = [], monitor) {
21
+ const before = input.slice(0, cursor);
22
+ const after = input.slice(cursor);
23
+ const tokenStart = Math.max(before.lastIndexOf(' '), before.lastIndexOf('\t')) + 1;
24
+ const token = before.slice(tokenStart);
25
+ // Determine the command word and which argument position the cursor is in.
26
+ const preceding = before.slice(0, tokenStart).trim().split(/\s+/).filter(Boolean);
27
+ const command = preceding[0]?.replace(/^\//, '').toLowerCase();
28
+ const argumentIndex = preceding.length;
29
+ // Try command-specific handlers.
30
+ const result = completeAgentName(command, argumentIndex, token, agents, before, after, tokenStart) ??
31
+ completeSendTarget(command, argumentIndex, token, agents, before, after, tokenStart) ??
32
+ completeScheduleTarget(command, argumentIndex, preceding, token, agents, before, after, tokenStart) ??
33
+ completeConnectionClose(command, argumentIndex, preceding, token, connections, before, after, tokenStart) ??
34
+ completeBrowserCommand(command, argumentIndex, preceding, token, connections, before, after, tokenStart) ??
35
+ completeMonitorCommand(command, argumentIndex, preceding, token, monitor, before, after, tokenStart) ??
36
+ completeSearchCommand(command, argumentIndex, token, before, after, tokenStart) ??
37
+ completeSyntaxTheme(command, argumentIndex, preceding, token, SYNTAX_THEMES, before, after, tokenStart) ??
38
+ completeHarnessModel(command, preceding, token, before, after, tokenStart);
39
+ if (result !== null) {
40
+ return result;
41
+ }
42
+ return completeFilePath(token, cwd, before, after, tokenStart, input, cursor);
43
+ }
@@ -0,0 +1,24 @@
1
+ import { completeWord } from './completion-helpers.js';
2
+ import { BROWSER_SUBCOMMANDS } from './browser-command.js';
3
+ export function completeBrowserCommand(command, argumentIndex, preceding, token, connections, before, after, tokenStart) {
4
+ if (command !== 'browser') {
5
+ return null;
6
+ }
7
+ if (argumentIndex === 1) {
8
+ return completeWord(token, '', BROWSER_SUBCOMMANDS, ' ', before, after, tokenStart);
9
+ }
10
+ const sub = preceding[1]?.toLowerCase();
11
+ const windowIds = connections
12
+ .filter((c) => c.startsWith('browser:'))
13
+ .map((c) => c.slice('browser:'.length));
14
+ if (argumentIndex === 2 && sub === 'use') {
15
+ return completeWord(token, '', windowIds, ' ', before, after, tokenStart);
16
+ }
17
+ if (argumentIndex === 2 && sub === 'window') {
18
+ return completeWord(token, '', ['close'], ' ', before, after, tokenStart);
19
+ }
20
+ if (argumentIndex === 3 && sub === 'window' && preceding[2]?.toLowerCase() === 'close') {
21
+ return completeWord(token, '', windowIds, ' ', before, after, tokenStart);
22
+ }
23
+ return null;
24
+ }
@@ -0,0 +1,26 @@
1
+ import path from 'node:path';
2
+ import { readdirSync } from 'node:fs';
3
+ import { isDir, longestCommonPrefix, splitToken, replaceToken } from './completion-helpers.js';
4
+ export function completeFilePath(token, cwd, before, after, tokenStart, input, cursor) {
5
+ const { dir, base } = splitToken(token, cwd);
6
+ let entries;
7
+ try {
8
+ entries = readdirSync(dir);
9
+ }
10
+ catch {
11
+ return { newInput: input, newCursor: cursor, matches: [] };
12
+ }
13
+ const matches = entries
14
+ .filter((entry) => entry.startsWith(base) && (base.startsWith('.') || !entry.startsWith('.')))
15
+ .toSorted((a, b) => a.localeCompare(b));
16
+ if (matches.length === 0)
17
+ return { newInput: input, newCursor: cursor, matches: [] };
18
+ let completedName = longestCommonPrefix(matches);
19
+ let suffix = '';
20
+ if (matches.length === 1) {
21
+ completedName = matches[0];
22
+ suffix = isDir(path.join(dir, matches[0])) ? '/' : ' ';
23
+ }
24
+ const typedDirPrefix = token.slice(0, token.length - base.length);
25
+ return replaceToken(before, after, tokenStart, typedDirPrefix + completedName + suffix, matches);
26
+ }
@@ -0,0 +1,69 @@
1
+ import { completeWord } from './completion-helpers.js';
2
+ export { completeBrowserCommand } from './completion-browser.js';
3
+ export function completeAgentName(command, argumentIndex, token, agents, before, after, tokenStart) {
4
+ if (argumentIndex !== 1 || (command !== 'msg' && command !== 'broadcast')) {
5
+ return null;
6
+ }
7
+ if (command === 'broadcast') {
8
+ const segStart = token.lastIndexOf(',') + 1;
9
+ return completeWord(token.slice(segStart), token.slice(0, segStart), [...agents, 'all'], '', before, after, tokenStart);
10
+ }
11
+ return completeWord(token, '', agents, ' ', before, after, tokenStart);
12
+ }
13
+ export function completeSendTarget(command, argumentIndex, token, labels, before, after, tokenStart) {
14
+ if (argumentIndex !== 1 || (command !== 'send' && command !== 'queue'))
15
+ return null;
16
+ return completeWord(token, '', labels, ' ', before, after, tokenStart);
17
+ }
18
+ // Complete the target of a `schedule … in <tab>` clause against open tab labels. The clause
19
+ // sits right after the timer name (or `list`/`clear`) at argument 3, or after `cancel <name>`
20
+ // at argument 4 — anywhere else the word `in` belongs to the scheduled command itself.
21
+ export function completeScheduleTarget(command, argumentIndex, preceding, token, labels, before, after, tokenStart) {
22
+ if (command !== 'schedule' || preceding.at(-1)?.toLowerCase() !== 'in')
23
+ return null;
24
+ const isClause = argumentIndex === 3 || (argumentIndex === 4 && preceding[1]?.toLowerCase() === 'cancel');
25
+ if (!isClause)
26
+ return null;
27
+ return completeWord(token, '', labels, ' ', before, after, tokenStart);
28
+ }
29
+ // Complete `monitor`/`unmonitor` arguments: the first argument is a persona name (or
30
+ // `ask` / `--all`), later arguments are targets (tab labels or `group:<n>` tokens) —
31
+ // except after `monitor ask`, where the persona comes second.
32
+ export function completeMonitorCommand(command, argumentIndex, preceding, token, monitor, before, after, tokenStart) {
33
+ if (!monitor || (command !== 'monitor' && command !== 'unmonitor'))
34
+ return null;
35
+ if (argumentIndex === 1) {
36
+ const extra = command === 'unmonitor' ? ['--all'] : ['ask'];
37
+ return completeWord(token, '', [...monitor.personas, ...extra], ' ', before, after, tokenStart);
38
+ }
39
+ if (argumentIndex === 2 && command === 'monitor' && preceding[1]?.toLowerCase() === 'ask') {
40
+ return completeWord(token, '', monitor.personas, ' ', before, after, tokenStart);
41
+ }
42
+ if (argumentIndex >= 2 && preceding[1]?.toLowerCase() !== 'ask') {
43
+ return completeWord(token, '', monitor.targets, ' ', before, after, tokenStart);
44
+ }
45
+ return null;
46
+ }
47
+ // Complete `search transcript` — the only subcommand, so argument 1 always offers it.
48
+ export function completeSearchCommand(command, argumentIndex, token, before, after, tokenStart) {
49
+ if (argumentIndex !== 1 || command !== 'search')
50
+ return null;
51
+ return completeWord(token, '', ['transcript'], ' ', before, after, tokenStart);
52
+ }
53
+ // Complete `syntax theme <name>` — argument 1 offers `theme`, argument 2 completes theme names.
54
+ export function completeSyntaxTheme(command, argumentIndex, preceding, token, themes, before, after, tokenStart) {
55
+ if (command !== 'syntax')
56
+ return null;
57
+ if (argumentIndex === 1)
58
+ return completeWord(token, '', ['theme'], ' ', before, after, tokenStart);
59
+ if (argumentIndex === 2 && preceding[1]?.toLowerCase() === 'theme') {
60
+ return completeWord(token, '', themes, ' ', before, after, tokenStart);
61
+ }
62
+ return null;
63
+ }
64
+ export function completeConnectionClose(command, argumentIndex, preceding, token, connections, before, after, tokenStart) {
65
+ if (argumentIndex !== 2 || command !== 'connection' || preceding[1]?.toLowerCase() !== 'close') {
66
+ return null;
67
+ }
68
+ return completeWord(token, '', connections, ' ', before, after, tokenStart);
69
+ }
@@ -0,0 +1,43 @@
1
+ import { statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ export const isDir = (p) => {
5
+ try {
6
+ return statSync(p).isDirectory();
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ };
12
+ export const longestCommonPrefix = (items) => {
13
+ if (items.length === 0)
14
+ return '';
15
+ let prefix = items[0];
16
+ for (const item of items.slice(1)) {
17
+ while (!item.startsWith(prefix))
18
+ prefix = prefix.slice(0, -1);
19
+ if (!prefix)
20
+ break;
21
+ }
22
+ return prefix;
23
+ };
24
+ export const splitToken = (token, cwd) => {
25
+ const expanded = token.startsWith('~') ? homedir() + token.slice(1) : token;
26
+ const slash = expanded.lastIndexOf('/');
27
+ if (slash !== -1) {
28
+ const dirPart = expanded.slice(0, slash + 1) || '/';
29
+ return { dir: path.resolve(cwd, dirPart), base: expanded.slice(slash + 1) };
30
+ }
31
+ return { dir: cwd, base: expanded };
32
+ };
33
+ export const replaceToken = (before, after, tokenStart, newToken, matches) => {
34
+ const newBefore = before.slice(0, tokenStart) + newToken;
35
+ return { newInput: newBefore + after, newCursor: newBefore.length, matches };
36
+ };
37
+ export const completeWord = (partial, keepPrefix, candidates, suffix, before, after, tokenStart) => {
38
+ const matches = candidates.filter((c) => c.startsWith(partial)).toSorted((a, b) => a.localeCompare(b));
39
+ if (matches.length === 0)
40
+ return { newInput: before + after, newCursor: before.length, matches: [] };
41
+ const completed = matches.length === 1 ? matches[0] + suffix : longestCommonPrefix(matches);
42
+ return replaceToken(before, after, tokenStart, keepPrefix + completed, matches);
43
+ };
@@ -0,0 +1,42 @@
1
+ import { completeAgentName, completeSendTarget, completeScheduleTarget, completeConnectionClose, completeBrowserCommand, completeMonitorCommand, completeSearchCommand, completeSyntaxTheme } from './completion-handlers.js';
2
+ import { completeFilePath } from './completion-fs.js';
3
+ import { SYNTAX_THEMES } from './syntax-themes.js';
4
+ /**
5
+ * Tab-complete the token ending at the cursor.
6
+ *
7
+ * - For the recipient argument of `msg`/`broadcast`, completes against active agent names
8
+ * (`broadcast` also offers `all` and supports a comma-separated list).
9
+ * - For the target argument of `send` and the `in <tab>` clause of `schedule`, completes
10
+ * against all open tab labels.
11
+ * - For the target of `connection close`, completes against open connection strings
12
+ * (e.g. `sqlite:movies`, `shell:bash`, `acp:opencode`, `browser:w1`).
13
+ * - For the `browser` command, completes subcommands and, where a window id is expected
14
+ * (`browser use`, `browser window close`), the current tab's open window ids.
15
+ * - Otherwise completes a filesystem path relative to `cwd`.
16
+ *
17
+ * A single match is filled in fully; multiple matches fill in their longest common prefix
18
+ * and are returned via `matches` so the caller can display the options.
19
+ */
20
+ export function completeCommandLine(input, cursor, cwd, agents = [], connections = [], monitor) {
21
+ const before = input.slice(0, cursor);
22
+ const after = input.slice(cursor);
23
+ const tokenStart = Math.max(before.lastIndexOf(' '), before.lastIndexOf('\t')) + 1;
24
+ const token = before.slice(tokenStart);
25
+ // Determine the command word and which argument position the cursor is in.
26
+ const preceding = before.slice(0, tokenStart).trim().split(/\s+/).filter(Boolean);
27
+ const command = preceding[0]?.replace(/^\//, '').toLowerCase();
28
+ const argumentIndex = preceding.length;
29
+ // Try command-specific handlers.
30
+ const result = completeAgentName(command, argumentIndex, token, agents, before, after, tokenStart) ??
31
+ completeSendTarget(command, argumentIndex, token, agents, before, after, tokenStart) ??
32
+ completeScheduleTarget(command, argumentIndex, preceding, token, agents, before, after, tokenStart) ??
33
+ completeConnectionClose(command, argumentIndex, preceding, token, connections, before, after, tokenStart) ??
34
+ completeBrowserCommand(command, argumentIndex, preceding, token, connections, before, after, tokenStart) ??
35
+ completeMonitorCommand(command, argumentIndex, preceding, token, monitor, before, after, tokenStart) ??
36
+ completeSearchCommand(command, argumentIndex, token, before, after, tokenStart) ??
37
+ completeSyntaxTheme(command, argumentIndex, preceding, token, SYNTAX_THEMES, before, after, tokenStart);
38
+ if (result !== null) {
39
+ return result;
40
+ }
41
+ return completeFilePath(token, cwd, before, after, tokenStart, input, cursor);
42
+ }
package/dist/config.js ADDED
@@ -0,0 +1,66 @@
1
+ import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { DEFAULT_SYNTAX_THEME } from './syntax-themes.js';
4
+ import { DEFAULT_APP_THEME } from './app-themes.js';
5
+ export const DEFAULT_TRANSCRIPT_MAX_LINES = 25_000;
6
+ export const DEFAULT_TAB_NAME_MAX_LENGTH = 16;
7
+ const DEFAULT_CONFIG = {
8
+ transcriptMaxLines: DEFAULT_TRANSCRIPT_MAX_LINES,
9
+ tabNameMaxLength: DEFAULT_TAB_NAME_MAX_LENGTH,
10
+ sandboxWorkspaces: true,
11
+ syntaxTheme: DEFAULT_SYNTAX_THEME,
12
+ theme: DEFAULT_APP_THEME,
13
+ notifications: {
14
+ events: {
15
+ stateChange: false,
16
+ incomingMessage: false,
17
+ scheduleFire: false,
18
+ agentStart: false,
19
+ rateLimited: false,
20
+ },
21
+ },
22
+ };
23
+ let config = { ...DEFAULT_CONFIG };
24
+ let storedConfigPath = null;
25
+ export function loadConfig(projectDirectory) {
26
+ const configDirectory = path.join(projectDirectory, '.janissary');
27
+ const configPath = path.join(configDirectory, 'config.json');
28
+ storedConfigPath = configPath;
29
+ if (!existsSync(configPath)) {
30
+ mkdirSync(configDirectory, { recursive: true });
31
+ writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, undefined, 2) + '\n');
32
+ config = { ...DEFAULT_CONFIG };
33
+ return config;
34
+ }
35
+ try {
36
+ const parsed = JSON.parse(readFileSync(configPath, 'utf8'));
37
+ config = { ...DEFAULT_CONFIG, ...parsed };
38
+ return config;
39
+ }
40
+ catch {
41
+ process.stderr.write('warning: .janissary/config.json is invalid JSON — using defaults (file left untouched)\n');
42
+ config = { ...DEFAULT_CONFIG };
43
+ return config;
44
+ }
45
+ }
46
+ export function getConfig() {
47
+ return config;
48
+ }
49
+ // Merge `partial` into the in-memory config and write it back to `.janissary/config.json`.
50
+ // Reads and re-stringifies the raw file (rather than the parsed `Config` defaults) so unknown
51
+ // keys a user added by hand survive the round trip. Returns false (without throwing) on failure,
52
+ // so the caller can report it to the transcript instead.
53
+ export function updateConfig(partial) {
54
+ config = { ...config, ...partial };
55
+ if (!storedConfigPath)
56
+ return false;
57
+ try {
58
+ const raw = existsSync(storedConfigPath) ? JSON.parse(readFileSync(storedConfigPath, 'utf8')) : {};
59
+ const merged = { ...raw, ...partial };
60
+ writeFileSync(storedConfigPath, JSON.stringify(merged, undefined, 2) + '\n');
61
+ return true;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }