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.
- package/LICENSE +75 -0
- package/README.md +28 -0
- package/agent-names.json +52 -0
- package/bin/janus.mjs +27 -0
- package/chrome-extension/_metadata/generated_indexed_rulesets/_ruleset1 +0 -0
- package/chrome-extension/content-script.js +62 -0
- package/chrome-extension/manifest.json +25 -0
- package/chrome-extension/rules.json +16 -0
- package/dist/acp/index.js +93 -0
- package/dist/acp/loop.js +75 -0
- package/dist/acp/manager.js +126 -0
- package/dist/acp/rate-limit.js +8 -0
- package/dist/acp/runner.js +18 -0
- package/dist/acp/tools.js +27 -0
- package/dist/acp-loop.js +75 -0
- package/dist/acp-manager.js +109 -0
- package/dist/acp-runner.js +18 -0
- package/dist/acp.js +91 -0
- package/dist/agent/commands.js +29 -0
- package/dist/agent/communication-manager.js +71 -0
- package/dist/agent/state.js +58 -0
- package/dist/agent-commands.js +29 -0
- package/dist/agent-communication-manager.js +71 -0
- package/dist/agent-state.js +58 -0
- package/dist/app-themes.js +13 -0
- package/dist/browser/command.js +104 -0
- package/dist/browser/index.js +124 -0
- package/dist/browser/tab-helpers.js +51 -0
- package/dist/browser/tab.js +99 -0
- package/dist/browser-command.js +104 -0
- package/dist/browser-tab-helpers.js +51 -0
- package/dist/browser-tab.js +99 -0
- package/dist/browser.js +124 -0
- package/dist/buffer.js +42 -0
- package/dist/bus.js +52 -0
- package/dist/capture/manager.js +48 -0
- package/dist/capture/router.js +18 -0
- package/dist/capture-manager.js +48 -0
- package/dist/capture-router.js +20 -0
- package/dist/chrome-extension-loader.js +62 -0
- package/dist/cli-args.js +80 -0
- package/dist/command-manager.js +126 -0
- package/dist/command-router.js +14 -0
- package/dist/commands/acp-reset.js +13 -0
- package/dist/commands/acp.js +5 -0
- package/dist/commands/agent.js +5 -0
- package/dist/commands/broadcast.js +22 -0
- package/dist/commands/browser.js +5 -0
- package/dist/commands/clear.js +5 -0
- package/dist/commands/close.js +42 -0
- package/dist/commands/connection.js +5 -0
- package/dist/commands/db.js +5 -0
- package/dist/commands/edit.js +18 -0
- package/dist/commands/files.js +13 -0
- package/dist/commands/hist.js +7 -0
- package/dist/commands/index.js +62 -0
- package/dist/commands/monitor.js +61 -0
- package/dist/commands/msg.js +17 -0
- package/dist/commands/next.js +7 -0
- package/dist/commands/notifications.js +16 -0
- package/dist/commands/notify.js +20 -0
- package/dist/commands/open.js +41 -0
- package/dist/commands/profile.js +5 -0
- package/dist/commands/queue.js +37 -0
- package/dist/commands/quit.js +6 -0
- package/dist/commands/rename.js +12 -0
- package/dist/commands/resolve-target.js +12 -0
- package/dist/commands/schedule.js +74 -0
- package/dist/commands/search.js +26 -0
- package/dist/commands/send.js +51 -0
- package/dist/commands/state.js +9 -0
- package/dist/commands/syntax.js +36 -0
- package/dist/commands/tasks.js +7 -0
- package/dist/commands/theme.js +41 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands.js +70 -0
- package/dist/completion/browser.js +24 -0
- package/dist/completion/fs.js +26 -0
- package/dist/completion/handlers.js +81 -0
- package/dist/completion/helpers.js +43 -0
- package/dist/completion/index.js +43 -0
- package/dist/completion-browser.js +24 -0
- package/dist/completion-fs.js +26 -0
- package/dist/completion-handlers.js +69 -0
- package/dist/completion-helpers.js +43 -0
- package/dist/completion.js +42 -0
- package/dist/config.js +66 -0
- package/dist/connection/close.js +41 -0
- package/dist/connection/manager.js +93 -0
- package/dist/connection/parsing.js +28 -0
- package/dist/connection-close.js +41 -0
- package/dist/connection-manager.js +93 -0
- package/dist/connection-parsing.js +28 -0
- package/dist/connections.js +75 -0
- package/dist/controller-events.js +34 -0
- package/dist/controller-file-tree.js +36 -0
- package/dist/controller.js +201 -0
- package/dist/database/index.js +83 -0
- package/dist/database/manager.js +67 -0
- package/dist/database/parsing.js +54 -0
- package/dist/database/query.js +41 -0
- package/dist/database-manager.js +67 -0
- package/dist/database-parsing.js +54 -0
- package/dist/database-query.js +41 -0
- package/dist/database.js +83 -0
- package/dist/datetime.js +15 -0
- package/dist/editor/save.js +27 -0
- package/dist/editor/sync.js +10 -0
- package/dist/editor/watch-manager.js +84 -0
- package/dist/editor-save.js +24 -0
- package/dist/editor-watch-manager.js +84 -0
- package/dist/file-tree-args.js +36 -0
- package/dist/file-tree-git-refresh.js +33 -0
- package/dist/file-tree-manager.js +252 -0
- package/dist/file-tree-moves.js +26 -0
- package/dist/file-tree-open.js +56 -0
- package/dist/file-tree-rebuild.js +22 -0
- package/dist/file-tree-watch.js +27 -0
- package/dist/file-tree.js +82 -0
- package/dist/git-status.js +64 -0
- package/dist/github-token.js +19 -0
- package/dist/global-history.js +48 -0
- package/dist/harness/auto-approve.js +79 -0
- package/dist/harness/busy-status.js +91 -0
- package/dist/harness/capture-file.js +28 -0
- package/dist/harness/index.js +87 -0
- package/dist/harness/manager.js +182 -0
- package/dist/harness/models.js +8 -0
- package/dist/harness/recorder.js +89 -0
- package/dist/harness/recording-file.js +25 -0
- package/dist/harness/screen.js +79 -0
- package/dist/harness-capture-file.js +28 -0
- package/dist/harness-manager.js +112 -0
- package/dist/harness-models.js +8 -0
- package/dist/harness-screen.js +72 -0
- package/dist/harness.js +54 -0
- package/dist/index.js +163 -0
- package/dist/instance-lock.js +34 -0
- package/dist/interactive.js +33 -0
- package/dist/main.js +203 -0
- package/dist/managers.js +1 -0
- package/dist/message-handler.js +134 -0
- package/dist/messaging.js +47 -0
- package/dist/monitor/acp.js +24 -0
- package/dist/monitor/ask.js +31 -0
- package/dist/monitor/context.js +27 -0
- package/dist/monitor/editor-feed.js +49 -0
- package/dist/monitor/feed-diff.js +22 -0
- package/dist/monitor/feeds.js +19 -0
- package/dist/monitor/framing.js +29 -0
- package/dist/monitor/harness-feed.js +21 -0
- package/dist/monitor/info.js +29 -0
- package/dist/monitor/manager.js +221 -0
- package/dist/monitor/page-feed.js +21 -0
- package/dist/monitor/parsing.js +82 -0
- package/dist/monitor/reply.js +11 -0
- package/dist/monitor/session.js +36 -0
- package/dist/monitor/stop.js +33 -0
- package/dist/monitor/suggestion.js +12 -0
- package/dist/monitor/targets.js +69 -0
- package/dist/monitor/window.js +109 -0
- package/dist/monitor-acp.js +22 -0
- package/dist/monitor-info.js +18 -0
- package/dist/monitor-manager.js +233 -0
- package/dist/monitor-parsing.js +68 -0
- package/dist/monitor-session.js +19 -0
- package/dist/monitor-targets.js +37 -0
- package/dist/monitor-window.js +81 -0
- package/dist/notifications-tab.js +32 -0
- package/dist/notifications.js +87 -0
- package/dist/open-file-manager.js +100 -0
- package/dist/openers/editor.js +41 -0
- package/dist/openers/external-viewer.js +11 -0
- package/dist/openers/image.js +27 -0
- package/dist/openers/index.js +16 -0
- package/dist/openers/markdown.js +20 -0
- package/dist/openers/os-open.js +24 -0
- package/dist/openers/page.js +51 -0
- package/dist/openers/size.js +13 -0
- package/dist/openers/types.js +1 -0
- package/dist/page/sync.js +10 -0
- package/dist/paths.js +45 -0
- package/dist/persona-parsing.js +49 -0
- package/dist/personas.js +44 -0
- package/dist/profile/agent-opener.js +154 -0
- package/dist/profile/files.js +17 -0
- package/dist/profile/manager.js +72 -0
- package/dist/profile/monitors.js +23 -0
- package/dist/profile/notifications.js +11 -0
- package/dist/profile-agent-opener.js +136 -0
- package/dist/profile-manager.js +72 -0
- package/dist/profiles.js +143 -0
- package/dist/protocol.js +1 -0
- package/dist/pseudoterminal-manager.js +106 -0
- package/dist/pty.js +41 -0
- package/dist/recognizers/acp.js +38 -0
- package/dist/recognizers/analyze.js +49 -0
- package/dist/recognizers/bash.js +51 -0
- package/dist/recognizers/db.js +26 -0
- package/dist/recognizers/index.js +1 -0
- package/dist/recognizers/lexicon.js +11 -0
- package/dist/recognizers/types.js +5 -0
- package/dist/resolve.js +39 -0
- package/dist/route-choice.js +11 -0
- package/dist/sandbox/index.js +217 -0
- package/dist/sandbox/paths.js +178 -0
- package/dist/sandbox/profile.js +161 -0
- package/dist/sandbox-paths.js +159 -0
- package/dist/sandbox-profile.js +161 -0
- package/dist/sandbox.js +217 -0
- package/dist/schedule/display.js +19 -0
- package/dist/schedule/helpers.js +76 -0
- package/dist/schedule/index.js +76 -0
- package/dist/schedule/manager.js +101 -0
- package/dist/schedule/parsing.js +63 -0
- package/dist/schedule/time.js +34 -0
- package/dist/schedule-display.js +19 -0
- package/dist/schedule-helpers.js +76 -0
- package/dist/schedule-manager.js +98 -0
- package/dist/schedule-parsing.js +63 -0
- package/dist/schedule-time.js +34 -0
- package/dist/schedule.js +76 -0
- package/dist/search-matches.js +40 -0
- package/dist/security.js +44 -0
- package/dist/shell-manager.js +139 -0
- package/dist/shell.js +59 -0
- package/dist/ssh-manager.js +42 -0
- package/dist/ssh.js +39 -0
- package/dist/startup-errors.js +21 -0
- package/dist/state-event.js +14 -0
- package/dist/state-format.js +38 -0
- package/dist/syntax-themes.js +14 -0
- package/dist/tab/agent-state.js +19 -0
- package/dist/tab/cleanup.js +33 -0
- package/dist/tab/colors.js +43 -0
- package/dist/tab/creators.js +95 -0
- package/dist/tab/dock.js +35 -0
- package/dist/tab/focus-history.js +41 -0
- package/dist/tab/formatting-handlers.js +77 -0
- package/dist/tab/formatting.js +21 -0
- package/dist/tab/history.js +14 -0
- package/dist/tab/index.js +65 -0
- package/dist/tab/manager.js +329 -0
- package/dist/tab/openers.js +35 -0
- package/dist/tab/queue.js +33 -0
- package/dist/tab/rehydrate.js +15 -0
- package/dist/tab/reorder.js +15 -0
- package/dist/tab/transcript.js +15 -0
- package/dist/tab/utils.js +55 -0
- package/dist/tab/view.js +42 -0
- package/dist/tab-cleanup.js +27 -0
- package/dist/tab-colors.js +43 -0
- package/dist/tab-creators.js +85 -0
- package/dist/tab-formatting-handlers.js +77 -0
- package/dist/tab-formatting.js +21 -0
- package/dist/tab-manager.js +418 -0
- package/dist/tab-utils.js +55 -0
- package/dist/tab.js +57 -0
- package/dist/tasks.js +32 -0
- package/dist/transcript/logger.js +36 -0
- package/dist/transcript/store.js +66 -0
- package/dist/types.js +6 -0
- package/dist/user-agent.js +65 -0
- package/dist/word-wrapping.js +42 -0
- package/dist/workspace-manager.js +40 -0
- package/dist/workspace.js +120 -0
- package/help.md +97 -0
- 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
|
+
}
|
package/dist/profiles.js
ADDED
|
@@ -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
|
+
}
|
package/dist/protocol.js
ADDED
|
@@ -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
|
+
};
|