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,72 @@
1
+ import { makeTab, distinctColor } from '../tab/index.js';
2
+ import { parseProfileCommand, loadProfileEntries, listProfiles, profileExists } from '../profiles.js';
3
+ import { parseAgentCommand, resolveAgentName } from '../commands.js';
4
+ import { openProfileEntries } from './agent-opener.js';
5
+ import { sandboxNotice } from '../sandbox/index.js';
6
+ export class ProfileManager {
7
+ managers;
8
+ constructor(managers) {
9
+ this.managers = managers;
10
+ }
11
+ run(command, label) {
12
+ const parsed = parseProfileCommand(command);
13
+ const out = (text) => this.managers.tab.append(label, { input: command, output: text });
14
+ if ('error' in parsed) {
15
+ out(parsed.error);
16
+ return;
17
+ }
18
+ if (parsed.action === 'list') {
19
+ const names = listProfiles();
20
+ out(names.length > 0 ? names.join('\n') : 'No profiles.');
21
+ return;
22
+ }
23
+ if (!profileExists(parsed.name)) {
24
+ out(`No profile named "${parsed.name}".`);
25
+ return;
26
+ }
27
+ const entries = loadProfileEntries(parsed.name);
28
+ if (entries.length === 0) {
29
+ out(`Profile "${parsed.name}" has no agents.`);
30
+ return;
31
+ }
32
+ openProfileEntries(entries, this.managers, parsed.name, label, out);
33
+ }
34
+ newAgent(command) {
35
+ const parsed = parseAgentCommand(command);
36
+ const existing = this.managers.tab.allLabels();
37
+ const creator = this.managers.tab.cur();
38
+ const resolved = parsed.name || resolveAgentName(`agent ${parsed.name}`, existing);
39
+ const out = (text) => this.managers.tab.append(creator.label, { input: command, output: text });
40
+ if (resolved === null) {
41
+ out('All agent names are in use.');
42
+ return;
43
+ }
44
+ if (existing.some((l) => l.toLowerCase() === resolved.toLowerCase())) {
45
+ out(`Agent "${resolved}" is already active.`);
46
+ return;
47
+ }
48
+ let workspaceDir;
49
+ if (parsed.workspace) {
50
+ const result = this.managers.workspace.create(resolved);
51
+ if ('error' in result) {
52
+ out(result.error);
53
+ return;
54
+ }
55
+ workspaceDir = result.dir;
56
+ }
57
+ const dotColor = distinctColor(this.managers.tab.tabs.map((t) => t.dotColor));
58
+ const group = creator?.group ?? 1;
59
+ const groupColor = creator?.groupColor ?? dotColor;
60
+ const tab = makeTab(resolved, dotColor, this.managers.tab.tabs.length + 1, [], [], workspaceDir, group, groupColor);
61
+ tab.toolStepsExpanded = false;
62
+ tab.offline = parsed.offline;
63
+ this.managers.tab.insertTabInGroup(tab);
64
+ this.managers.tab.setCwd(resolved, workspaceDir ?? process.cwd());
65
+ this.managers.tab.setActiveTab(this.managers.tab.findIndex(resolved));
66
+ this.managers.tab.persist(this.managers.tab.buildAgentState(tab));
67
+ const notice = workspaceDir ? sandboxNotice() : undefined;
68
+ out(`Agent "${resolved}" ready.${workspaceDir ? ` (workspace: ${this.managers.tab.shorten(workspaceDir)})` : ''}`);
69
+ if (notice)
70
+ out(notice);
71
+ }
72
+ }
@@ -0,0 +1,23 @@
1
+ import { parseMonitorCommand } from '../monitor/parsing.js';
2
+ import { formatTargets } from '../monitor/targets.js';
3
+ import { loadProfileMonitors } from '../profiles.js';
4
+ // Start each profile-level monitor (from the profile's `_monitors.json`) once its tabs are open,
5
+ // owned by the launch's issuing tab. Idempotent across relaunch: any existing same-owner monitor
6
+ // for the persona is stopped first, so re-launching a profile refreshes rather than errors. Each
7
+ // outcome (started, or the parse/start error) is pushed to `notes` for the launch report.
8
+ export function startProfileMonitors(profileName, managers, issuingLabel, notes) {
9
+ for (const monitor of loadProfileMonitors(profileName)) {
10
+ const parsed = parseMonitorCommand(`monitor ${monitor.persona} ${monitor.targets.join(' ')}`.trim());
11
+ if ('error' in parsed) {
12
+ notes.push(`Monitor "${monitor.persona}": ${parsed.error}`);
13
+ continue;
14
+ }
15
+ if ('ask' in parsed)
16
+ continue;
17
+ managers.monitor.stop(issuingLabel, parsed.persona);
18
+ const error = managers.monitor.start(issuingLabel, parsed.persona, parsed.targets);
19
+ notes.push(error
20
+ ? `Monitor "${monitor.persona}": ${error}`
21
+ : `Monitoring ${formatTargets(parsed.targets)} (persona: ${parsed.persona}).`);
22
+ }
23
+ }
@@ -0,0 +1,11 @@
1
+ import { loadProfileNotifications } from '../profiles.js';
2
+ import { openNotificationsTab } from '../notifications-tab.js';
3
+ // Open (or dock) the singleton notifications tab for each profile-level notifications entry (from
4
+ // the profile's `_notifications.json`) once every entry is open, mirroring `openProfileFiles`. The
5
+ // notifications tab is a singleton, so multiple entries just re-dock the same feed.
6
+ export function openProfileNotifications(profileName, managers, notes) {
7
+ for (const entry of loadProfileNotifications(profileName)) {
8
+ openNotificationsTab(managers, entry.dock);
9
+ notes.push(`Opened notifications${entry.dock ? ` (docked ${entry.dock})` : ''}.`);
10
+ }
11
+ }
@@ -0,0 +1,136 @@
1
+ import { distinctColor, makeTab } from './tab.js';
2
+ import { HARNESS_COMMANDS } from './harness.js';
3
+ import { isKnownModel } from './harness-models.js';
4
+ import { parseScheduleCommand } from './schedule.js';
5
+ function isHarnessEntry(e) {
6
+ return 'harness' in e;
7
+ }
8
+ function labelOf(e) {
9
+ return isHarnessEntry(e) ? e.label : e.name;
10
+ }
11
+ // Authored `schedule` strings (schedule-command grammar minus `in <tab>`) plus `run` one-shots,
12
+ // parsed into the ScheduleEntry[] the harness tab's schedule is set to. A string that errors or
13
+ // carries an `in <tab>` clause is reported and skipped; a duplicate name keeps the first.
14
+ function buildHarnessSchedule(entry, report) {
15
+ const now = new Date();
16
+ const entries = [];
17
+ const ids = new Set();
18
+ const scheduleLines = entry.schedule ?? [];
19
+ for (const raw of scheduleLines) {
20
+ const parsed = parseScheduleCommand(raw, now);
21
+ if ('error' in parsed) {
22
+ report(`Schedule "${raw}" for "${entry.label}": ${parsed.error}`);
23
+ continue;
24
+ }
25
+ if (parsed.action !== 'add') {
26
+ report(`Schedule "${raw}" for "${entry.label}" is not a new schedule.`);
27
+ continue;
28
+ }
29
+ if (parsed.target !== undefined) {
30
+ report(`Schedule "${raw}" for "${entry.label}" cannot target another tab.`);
31
+ continue;
32
+ }
33
+ if (ids.has(parsed.name)) {
34
+ report(`Duplicate schedule name "${parsed.name}" for "${entry.label}"; kept the first.`);
35
+ continue;
36
+ }
37
+ ids.add(parsed.name);
38
+ entries.push({ ...parsed.entry, id: parsed.name });
39
+ }
40
+ const runLines = entry.run ?? [];
41
+ for (const [i, command] of runLines.entries()) {
42
+ entries.push({ id: `run-${i + 1}`, command, spec: 'once', nextRun: Date.now(), recurring: false });
43
+ }
44
+ return entries;
45
+ }
46
+ function openAgentEntry(state, managers, group, groupColor, dotColor) {
47
+ const log = state.log ?? [];
48
+ const tab = makeTab(state.name, dotColor, managers.tab.tabs.length + 1, state.cmdHistory ?? [], log, state.workspaceDir, group, groupColor);
49
+ tab.toolStepsExpanded = false;
50
+ managers.tab.tabs = [...managers.tab.tabs, tab];
51
+ if (state.cwd)
52
+ managers.tab.setCwd(state.name, state.cwd);
53
+ if (state.context)
54
+ managers.tab.setContext(state.name, state.context);
55
+ if (state.schedule)
56
+ managers.schedule.set(state.name, state.schedule);
57
+ managers.tab.persist(managers.tab.buildAgentState(tab, { schedule: state.schedule }));
58
+ }
59
+ // Validate and open a harness entry. Returns an error to report and skip on, or undefined once
60
+ // the tab (and its schedule) is set up.
61
+ function openHarnessEntry(entry, managers, group, groupColor, issuingCwd, notes) {
62
+ if (HARNESS_COMMANDS[entry.harness] === undefined)
63
+ return `unknown harness "${entry.harness}"`;
64
+ if (entry.model && !isKnownModel(entry.harness, entry.model)) {
65
+ return `Unknown model "${entry.model}" for harness "${entry.harness}" — add it to harness-models.json.`;
66
+ }
67
+ const withCwd = { ...entry, cwd: entry.cwd ?? issuingCwd };
68
+ const error = managers.harness.openFromProfile(withCwd, entry.label, group, groupColor);
69
+ if (error)
70
+ return error;
71
+ const schedule = buildHarnessSchedule(entry, (message) => { notes.push(message); });
72
+ if (schedule.length > 0)
73
+ managers.schedule.set(entry.label, schedule);
74
+ return undefined;
75
+ }
76
+ // Relaunch semantics: close every open tab matching an entry's label first, then the caller opens
77
+ // all entries fresh, so label collisions between a closing tab and an opening one cannot arise.
78
+ // The issuing tab is never closed; if it's named by an entry, that entry is skipped instead.
79
+ function closeMatchingTabs(entries, managers, issuingLabel, skipped, notes) {
80
+ const toOpen = [];
81
+ for (const entry of entries) {
82
+ const label = labelOf(entry);
83
+ if (label.toLowerCase() === issuingLabel.toLowerCase()) {
84
+ skipped.push(`${label} (cannot relaunch the issuing tab)`);
85
+ continue;
86
+ }
87
+ const index = managers.tab.findIndex(label);
88
+ if (index !== -1) {
89
+ managers.tab.closeTab(index);
90
+ notes.push(`Relaunched "${label}".`);
91
+ }
92
+ toOpen.push(entry);
93
+ }
94
+ return toOpen;
95
+ }
96
+ export function openProfileEntries(entries, managers, name, issuingLabel, out) {
97
+ const authoredGroup = entries
98
+ .map((e) => (isHarnessEntry(e) ? undefined : e.group))
99
+ .find((g) => typeof g === 'number');
100
+ const group = authoredGroup ?? Math.max(0, ...managers.tab.tabs.map((t) => t.group)) + 1;
101
+ const skipped = [];
102
+ const notes = [];
103
+ const toOpen = closeMatchingTabs(entries, managers, issuingLabel, skipped, notes);
104
+ const used = new Set(managers.tab.tabs.map((t) => t.dotColor));
105
+ const opened = [];
106
+ let groupColor;
107
+ const firstNew = managers.tab.tabs.length;
108
+ const issuingCwd = managers.tab.cwdOf(issuingLabel) ?? process.cwd();
109
+ for (const entry of toOpen) {
110
+ const label = labelOf(entry);
111
+ const dotColor = distinctColor(used, entry.dotColor);
112
+ used.add(dotColor);
113
+ groupColor ??= dotColor;
114
+ if (isHarnessEntry(entry)) {
115
+ const error = openHarnessEntry(entry, managers, group, groupColor, issuingCwd, notes);
116
+ if (error) {
117
+ skipped.push(`${label} (${error})`);
118
+ continue;
119
+ }
120
+ }
121
+ else {
122
+ openAgentEntry(entry, managers, group, groupColor, dotColor);
123
+ }
124
+ opened.push(label);
125
+ }
126
+ if (opened.length > 0)
127
+ managers.tab.setActiveTab(firstNew);
128
+ const parts = [];
129
+ if (opened.length > 0)
130
+ parts.push(`Launched profile "${name}": ${opened.join(', ')}.`);
131
+ if (notes.length > 0)
132
+ parts.push(notes.join(' '));
133
+ if (skipped.length > 0)
134
+ parts.push(`Skipped: ${skipped.join('; ')}.`);
135
+ out(parts.length > 0 ? parts.join(' ') : `Profile "${name}" has no agents to open.`);
136
+ }
@@ -0,0 +1,72 @@
1
+ import { makeTab, distinctColor } from './tab.js';
2
+ import { parseProfileCommand, loadProfileEntries, listProfiles, profileExists } from './profiles.js';
3
+ import { parseAgentCommand, resolveAgentName } from './commands.js';
4
+ import { openProfileEntries } from './profile-agent-opener.js';
5
+ import { sandboxNotice } from './sandbox.js';
6
+ export class ProfileManager {
7
+ managers;
8
+ constructor(managers) {
9
+ this.managers = managers;
10
+ }
11
+ run(command, label) {
12
+ const parsed = parseProfileCommand(command);
13
+ const out = (text) => this.managers.tab.append(label, { input: command, output: text });
14
+ if ('error' in parsed) {
15
+ out(parsed.error);
16
+ return;
17
+ }
18
+ if (parsed.action === 'list') {
19
+ const names = listProfiles();
20
+ out(names.length > 0 ? names.join('\n') : 'No profiles.');
21
+ return;
22
+ }
23
+ if (!profileExists(parsed.name)) {
24
+ out(`No profile named "${parsed.name}".`);
25
+ return;
26
+ }
27
+ const entries = loadProfileEntries(parsed.name);
28
+ if (entries.length === 0) {
29
+ out(`Profile "${parsed.name}" has no agents.`);
30
+ return;
31
+ }
32
+ openProfileEntries(entries, this.managers, parsed.name, label, out);
33
+ }
34
+ newAgent(command) {
35
+ const parsed = parseAgentCommand(command);
36
+ const existing = this.managers.tab.allLabels();
37
+ const creator = this.managers.tab.cur();
38
+ const resolved = parsed.name || resolveAgentName(`agent ${parsed.name}`, existing);
39
+ const out = (text) => this.managers.tab.append(creator.label, { input: command, output: text });
40
+ if (resolved === null) {
41
+ out('All agent names are in use.');
42
+ return;
43
+ }
44
+ if (existing.some((l) => l.toLowerCase() === resolved.toLowerCase())) {
45
+ out(`Agent "${resolved}" is already active.`);
46
+ return;
47
+ }
48
+ let workspaceDir;
49
+ if (parsed.workspace) {
50
+ const result = this.managers.workspace.create(resolved);
51
+ if ('error' in result) {
52
+ out(result.error);
53
+ return;
54
+ }
55
+ workspaceDir = result.dir;
56
+ }
57
+ const dotColor = distinctColor(this.managers.tab.tabs.map((t) => t.dotColor));
58
+ const group = creator?.group ?? 1;
59
+ const groupColor = creator?.groupColor ?? dotColor;
60
+ const tab = makeTab(resolved, dotColor, this.managers.tab.tabs.length + 1, [], [], workspaceDir, group, groupColor);
61
+ tab.toolStepsExpanded = false;
62
+ tab.offline = parsed.offline;
63
+ this.managers.tab.insertTabInGroup(tab);
64
+ this.managers.tab.setCwd(resolved, workspaceDir ?? process.cwd());
65
+ this.managers.tab.setActiveTab(this.managers.tab.findIndex(resolved));
66
+ this.managers.tab.persist(this.managers.tab.buildAgentState(tab));
67
+ const notice = workspaceDir ? sandboxNotice() : undefined;
68
+ out(`Agent "${resolved}" ready.${workspaceDir ? ` (workspace: ${this.managers.tab.shorten(workspaceDir)})` : ''}`);
69
+ if (notice)
70
+ out(notice);
71
+ }
72
+ }
@@ -0,0 +1,143 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ // A profile is a named, reusable set of agents for a particular use case (writing code,
4
+ // surfing the web, authoring a book, …). Each profile is a directory under the profiles
5
+ // directory holding one `<agentname>.json` file per agent, in the agent-state schema.
6
+ // Profiles live in a top-level, committable `profiles/` dir (not `.janissary/`, which is
7
+ // gitignored and whose `state/` is cleared each launch).
8
+ let profileDir = '';
9
+ export function initProfileDir(projectDir) {
10
+ profileDir = path.join(projectDir, 'profiles');
11
+ }
12
+ export function profilePath(name) {
13
+ return path.join(profileDir, name);
14
+ }
15
+ export function profileExists(name) {
16
+ return profileDir !== '' && existsSync(profilePath(name));
17
+ }
18
+ export function listProfiles() {
19
+ if (!existsSync(profileDir))
20
+ return [];
21
+ try {
22
+ return readdirSync(profileDir, { withFileTypes: true })
23
+ .filter((d) => d.isDirectory())
24
+ .map((d) => d.name)
25
+ .toSorted((a, b) => a.localeCompare(b));
26
+ }
27
+ catch {
28
+ return [];
29
+ }
30
+ }
31
+ // Load every entry file in a profile — agent-state files, or harness entry files (discriminated
32
+ // by a `harness` key). The filename (minus `.json`) is the authoritative name, so it always
33
+ // overrides any `name` field inside the file. Entries are ordered by their `number` field
34
+ // (mirroring `--relaunch` tab restoration) so a profile controls the order its tabs open; files
35
+ // without a number keep their (alphabetical) readdir order.
36
+ export function loadProfileEntries(name) {
37
+ const directory = profilePath(name);
38
+ if (!existsSync(directory))
39
+ return [];
40
+ try {
41
+ return readdirSync(directory)
42
+ // `_`-prefixed files are reserved profile-level config (e.g. `_monitors.json`), never entries.
43
+ .filter((f) => f.endsWith('.json') && !f.startsWith('_'))
44
+ .map((f) => {
45
+ try {
46
+ const parsed = JSON.parse(readFileSync(path.join(directory, f), 'utf8'));
47
+ const label = f.replace(/\.json$/, '');
48
+ return 'harness' in parsed ? { ...parsed, label } : { ...parsed, name: label };
49
+ }
50
+ catch {
51
+ // skip invalid entry files
52
+ }
53
+ })
54
+ .filter((s) => s !== undefined)
55
+ .toSorted((a, b) => (a.number ?? Infinity) - (b.number ?? Infinity));
56
+ }
57
+ catch {
58
+ return [];
59
+ }
60
+ }
61
+ function isProfileMonitor(value) {
62
+ if (typeof value !== 'object' || value === null)
63
+ return false;
64
+ const monitor = value;
65
+ return typeof monitor.persona === 'string'
66
+ && Array.isArray(monitor.targets)
67
+ && monitor.targets.every((target) => typeof target === 'string');
68
+ }
69
+ // Profile-level monitors live in a reserved `_monitors.json` file — a JSON array of
70
+ // `{ persona, targets }` — kept out of the entry set by the leading underscore (it is not an
71
+ // agent or harness tab). Returns [] when the file is absent, unparseable, or not an array;
72
+ // malformed elements are dropped.
73
+ export function loadProfileMonitors(name) {
74
+ const file = path.join(profilePath(name), '_monitors.json');
75
+ if (!existsSync(file))
76
+ return [];
77
+ try {
78
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
79
+ return Array.isArray(parsed) ? parsed.filter(isProfileMonitor) : [];
80
+ }
81
+ catch {
82
+ return [];
83
+ }
84
+ }
85
+ function isProfileFilesEntry(value) {
86
+ if (typeof value !== 'object' || value === null)
87
+ return false;
88
+ const entry = value;
89
+ return (entry.dock === undefined || (typeof entry.dock === 'string' && ['left', 'right'].includes(entry.dock)))
90
+ && (entry.in === undefined || typeof entry.in === 'string');
91
+ }
92
+ // Profile-level file-tree tabs live in a reserved `_files.json` file — a JSON array of
93
+ // `{ dock?, in? }` — kept out of the entry set by the leading underscore. Returns [] when the file
94
+ // is absent, unparseable, or not an array; malformed elements are dropped.
95
+ export function loadProfileFiles(name) {
96
+ const file = path.join(profilePath(name), '_files.json');
97
+ if (!existsSync(file))
98
+ return [];
99
+ try {
100
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
101
+ return Array.isArray(parsed) ? parsed.filter(isProfileFilesEntry) : [];
102
+ }
103
+ catch {
104
+ return [];
105
+ }
106
+ }
107
+ function isProfileNotificationsEntry(value) {
108
+ if (typeof value !== 'object' || value === null)
109
+ return false;
110
+ const entry = value;
111
+ return entry.dock === undefined || (typeof entry.dock === 'string' && ['left', 'right'].includes(entry.dock));
112
+ }
113
+ // Profile-level notifications tabs live in a reserved `_notifications.json` file — a JSON array of
114
+ // `{ dock? }` — kept out of the entry set by the leading underscore. Returns [] when the file is
115
+ // absent, unparseable, or not an array; malformed elements are dropped.
116
+ export function loadProfileNotifications(name) {
117
+ const file = path.join(profilePath(name), '_notifications.json');
118
+ if (!existsSync(file))
119
+ return [];
120
+ try {
121
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
122
+ return Array.isArray(parsed) ? parsed.filter(isProfileNotificationsEntry) : [];
123
+ }
124
+ catch {
125
+ return [];
126
+ }
127
+ }
128
+ export const PROFILE_USAGE = 'Usage: profile launch <name> | profile list';
129
+ export function parseProfileCommand(command) {
130
+ const rest = command.replace(/^profile\b\s*/i, '').trim();
131
+ if (!rest)
132
+ return { error: PROFILE_USAGE };
133
+ const tokens = rest.split(/\s+/);
134
+ const head = tokens[0].toLowerCase();
135
+ if (head === 'list')
136
+ return { action: 'list' };
137
+ if (head === 'launch') {
138
+ if (!tokens[1])
139
+ return { error: 'Usage: profile launch <name>' };
140
+ return { action: 'launch', name: tokens[1] };
141
+ }
142
+ return { error: PROFILE_USAGE };
143
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,106 @@
1
+ import { spawnPty } from './pty.js';
2
+ import { messageBus } from './bus.js';
3
+ import { getGithubToken } from './github-token.js';
4
+ // Owns the live PTY sessions (keyed by their id) backing harness tabs, full-tab interactive command
5
+ // takeovers, and inline terminal cards, plus the dimensions new PTYs are spawned at. The controller
6
+ // owns the tabs these PTYs belong to; this module owns the sessions and their I/O, handing
7
+ // tab-affecting events back through the host.
8
+ export class PseudoterminalManager {
9
+ managers;
10
+ ptys = new Map();
11
+ cols = 80;
12
+ rows = 24;
13
+ constructor(managers) {
14
+ this.managers = managers;
15
+ }
16
+ // Spawn a PTY running `command` (with `program` as its display label) in `cwd`, register it under
17
+ // the owning tab's `label`, and return its id. Output and exit route back through the host.
18
+ // `workspaceDir`/`offline`, when the owning tab is workspaced, confine the process via Seatbelt.
19
+ spawn(label, program, command, cwd, workspaceDir, offline) {
20
+ const session = spawnPty(program, command, cwd, {
21
+ onData: (id, data) => messageBus.emit('pty', { type: 'data', id, data }),
22
+ onExit: (id, exitCode) => this.handleExit(id, exitCode),
23
+ }, this.cols, this.rows, { workspaceDir, offline, githubToken: workspaceDir ? getGithubToken() : undefined });
24
+ this.ptys.set(session.id, { session, tabLabel: label });
25
+ return session.id;
26
+ }
27
+ // Forward client keystrokes to a PTY.
28
+ input(id, data) { this.ptys.get(id)?.session.write(data); }
29
+ // Resize a single PTY (a client viewport change) and mirror the new size onto the bus so
30
+ // server-side observers (the harness screen reader) track the real PTY's dimensions.
31
+ resizeOne(id, cols, rows) {
32
+ const entry = this.ptys.get(id);
33
+ if (!entry)
34
+ return;
35
+ entry.session.resize(cols, rows);
36
+ messageBus.emit('pty', { type: 'resize', id, cols, rows });
37
+ }
38
+ // Kill a single PTY; its exit then flows through `handleExit`.
39
+ kill(id) { this.ptys.get(id)?.session.kill(); }
40
+ // Set the dimensions new PTYs spawn at (the client's terminal size).
41
+ resize(cols, rows) { this.cols = cols; this.rows = rows; }
42
+ // The dimensions new PTYs spawn at, for observers that mirror a PTY's screen.
43
+ spawnDimensions() { return { cols: this.cols, rows: this.rows }; }
44
+ // The program names of a tab's live PTYs, for the connections panel and completion (`terminal:<program>`).
45
+ terminalsFor(label) {
46
+ const programs = [];
47
+ for (const [, entry] of this.ptys)
48
+ if (entry.tabLabel === label)
49
+ programs.push(entry.session.program);
50
+ return programs;
51
+ }
52
+ // Create an inline terminal card: spawn a PTY running `command` in the tab's cwd and attach its
53
+ // id to `tab.activePty` so the client renders a terminal widget in the transcript. A workspaced
54
+ // tab's own `workspaceDir`/`offline` carry over to inline PTYs (e.g. `shell vim` inside it).
55
+ openInlinePty(label, command, program) {
56
+ const cwd = this.managers.tab.cwdOf(label) ?? process.cwd();
57
+ const tab = this.managers.tab.tabs.find((t) => t.label === label);
58
+ const id = this.spawn(label, program, command, cwd, tab?.workspaceDir, tab?.offline);
59
+ for (const t of this.managers.tab.tabs) {
60
+ if (t.label === label) {
61
+ t.activePty = id;
62
+ break;
63
+ }
64
+ }
65
+ messageBus.emit('state', { type: 'dirty' });
66
+ }
67
+ // Kill and forget every PTY belonging to a tab (on tab close).
68
+ closeTab(label) {
69
+ for (const [id, entry] of this.ptys)
70
+ if (entry.tabLabel === label) {
71
+ entry.session.kill();
72
+ this.ptys.delete(id);
73
+ }
74
+ }
75
+ // Kill every PTY (app shutdown).
76
+ closeAll() {
77
+ for (const [, entry] of this.ptys)
78
+ entry.session.kill();
79
+ this.ptys.clear();
80
+ }
81
+ // Drop the exited PTY from the registry, clear `activePty` on full-tab takeovers, update inline
82
+ // terminal card entries, then let the controller handle harness updates and client notification.
83
+ handleExit(id, exitCode) {
84
+ const hadEntry = this.ptys.delete(id);
85
+ // Clear activePty for full-tab interactive PTY takeovers.
86
+ for (const tab of this.managers.tab.tabs) {
87
+ if (tab.activePty === id)
88
+ tab.activePty = undefined;
89
+ }
90
+ // Update inline terminal card status in the log.
91
+ if (hadEntry) {
92
+ for (const tab of this.managers.tab.tabs) {
93
+ const index = tab.log.findIndex((e) => e.terminal?.ptyId === id);
94
+ if (index !== -1) {
95
+ const log = [...tab.log];
96
+ log[index] = { ...log[index], terminal: { ...log[index].terminal, status: 'exited', exitCode } };
97
+ tab.log = log;
98
+ this.managers.tab.persist(this.managers.tab.buildAgentState(tab));
99
+ messageBus.emit('transcript', { type: 'entry:appended', tabLabel: tab.label, entry: log[index], tab });
100
+ }
101
+ }
102
+ }
103
+ messageBus.emit('pty', { type: 'exit', id, exitCode });
104
+ messageBus.emit('state', { type: 'dirty' });
105
+ }
106
+ }
package/dist/pty.js ADDED
@@ -0,0 +1,41 @@
1
+ import * as pty from 'node-pty';
2
+ import { sandboxSpawn } from './sandbox/index.js';
3
+ let counter = 0;
4
+ /**
5
+ * Spawn `cmd` in a pseudo-terminal. `program` is the display label for the card. Unlike the old
6
+ * output is delivered to a callback (the server forwards it to the client's xterm)
7
+ * rather than written to the real stdout, so many can run concurrently across tabs.
8
+ * `sandbox`, when given a `workspaceDir`, confines the process to that workspace (see sandbox.ts);
9
+ * omitted or workspaceDir-less, the command runs exactly as before.
10
+ */
11
+ export function spawnPty(program, command, cwd, handlers, cols = 80, rows = 24, sandbox) {
12
+ const id = `pty${++counter}`;
13
+ const shell = process.env.SHELL || 'bash';
14
+ const { command: file, args, env } = sandboxSpawn({ ...sandbox, selfBinaryHint: program }, shell, ['-lc', command]);
15
+ const proc = pty.spawn(file, args, {
16
+ name: 'xterm-256color',
17
+ cols: Math.max(1, cols),
18
+ rows: Math.max(1, rows),
19
+ cwd: cwd || process.cwd(),
20
+ env,
21
+ });
22
+ proc.onData((d) => handlers.onData(id, d));
23
+ proc.onExit(({ exitCode }) => handlers.onExit(id, exitCode));
24
+ return {
25
+ id,
26
+ program,
27
+ write: (data) => proc.write(data),
28
+ resize: (c, r) => {
29
+ try {
30
+ proc.resize(Math.max(1, c), Math.max(1, r));
31
+ }
32
+ catch { /* process may have exited */ }
33
+ },
34
+ kill: () => {
35
+ try {
36
+ proc.kill();
37
+ }
38
+ catch { /* already gone */ }
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,38 @@
1
+ import { FUNCTION_WORDS } from './lexicon.js';
2
+ // Words that typically open a question or an instruction to an agent.
3
+ const PROMPT_WORDS = new Set([
4
+ 'what', 'how', 'why', 'when', 'who', 'where', 'which', 'whose', 'can', 'could', 'should',
5
+ 'would', 'will', 'is', 'are', 'am', 'do', 'does', 'did', 'please', 'explain', 'describe',
6
+ 'write', 'summarize', 'summarise', 'list', 'tell', 'give', 'show', 'find', 'create',
7
+ 'generate', 'make', 'help', 'translate', 'convert', 'fix', 'refactor', 'implement', 'add',
8
+ 'remove', 'compare', 'suggest', 'review', 'draft', 'rewrite', 'analyze', 'analyse',
9
+ ]);
10
+ // Shell metacharacters / SQL-ish punctuation that argue against this being plain prose.
11
+ const SYMBOLIC = /[|&;<>`$\\]|\$\(/;
12
+ // Recognize a natural-language prompt for the agent (acp). This is the prose catch-all: a
13
+ // trailing `?`, an opening question/instruction word, or simply several mostly-alphabetic
14
+ // words all read as a prompt.
15
+ export const acpRecognizer = {
16
+ route: 'acp',
17
+ recognize: (command) => {
18
+ const trimmed = command.trim();
19
+ const words = trimmed.split(/\s+/);
20
+ const first = words[0]?.toLowerCase() ?? '';
21
+ const letters = (trimmed.match(/[A-Za-z]/g) ?? []).length;
22
+ const alphaRatio = trimmed.length > 0 ? letters / trimmed.length : 0;
23
+ let score = 0;
24
+ score = /\?\s*$/.test(trimmed) ? 0.8 : score;
25
+ if (PROMPT_WORDS.has(first))
26
+ score = Math.max(score, 0.6);
27
+ if (words.length >= 3 && !SYMBOLIC.test(trimmed) && alphaRatio > 0.7) {
28
+ score = Math.max(score, 0.5);
29
+ }
30
+ // A question/instruction opener followed by grammatical words reads as a real sentence
31
+ // ("which file is the longest"), not a command — strong enough to dispatch on its own.
32
+ const functionInRest = words.slice(1).filter((w) => FUNCTION_WORDS.has(w.toLowerCase())).length;
33
+ if (PROMPT_WORDS.has(first) && functionInRest >= 1)
34
+ score += 0.2;
35
+ score = Math.min(score, 0.95);
36
+ return { match: score >= 0.4, reliability: score };
37
+ },
38
+ };