@phnx-labs/agents-cli 1.20.91 → 1.20.93
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/CHANGELOG.md +276 -0
- package/README.md +1 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +72 -6
- package/dist/commands/activity.js +198 -49
- package/dist/commands/beta.js +1 -0
- package/dist/commands/doctor.js +4 -2
- package/dist/commands/events.js +91 -1
- package/dist/commands/exec.d.ts +14 -0
- package/dist/commands/exec.js +144 -14
- package/dist/commands/projects.d.ts +22 -0
- package/dist/commands/projects.js +539 -0
- package/dist/commands/secrets.d.ts +17 -0
- package/dist/commands/secrets.js +198 -7
- package/dist/commands/send.d.ts +14 -12
- package/dist/commands/send.js +105 -35
- package/dist/commands/sessions-picker.d.ts +15 -0
- package/dist/commands/sessions-picker.js +37 -12
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +9 -1
- package/dist/commands/sessions.d.ts +10 -5
- package/dist/commands/sessions.js +65 -27
- package/dist/commands/sync.js +9 -3
- package/dist/commands/view.js +4 -0
- package/dist/index.js +18 -1
- package/dist/lib/activity.d.ts +77 -12
- package/dist/lib/activity.js +424 -74
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/channels/send.d.ts +83 -0
- package/dist/lib/channels/send.js +112 -0
- package/dist/lib/devices/registry.d.ts +14 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/events-ingest.d.ts +46 -0
- package/dist/lib/events-ingest.js +182 -0
- package/dist/lib/events.d.ts +15 -3
- package/dist/lib/events.js +55 -3
- package/dist/lib/feed-post.js +8 -2
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/linear-project-counts.d.ts +62 -0
- package/dist/lib/linear-project-counts.js +122 -0
- package/dist/lib/linear-projects.d.ts +50 -0
- package/dist/lib/linear-projects.js +114 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +14 -4
- package/dist/lib/menubar/install-menubar.js +20 -6
- package/dist/lib/menubar/notify-desktop.d.ts +17 -2
- package/dist/lib/menubar/notify-desktop.js +8 -2
- package/dist/lib/project-key.d.ts +44 -0
- package/dist/lib/project-key.js +79 -0
- package/dist/lib/project-probe.d.ts +75 -0
- package/dist/lib/project-probe.js +160 -0
- package/dist/lib/project-resources.d.ts +8 -0
- package/dist/lib/project-resources.js +31 -3
- package/dist/lib/project-root.js +16 -0
- package/dist/lib/project-status.d.ts +100 -0
- package/dist/lib/project-status.js +182 -0
- package/dist/lib/projects.d.ts +144 -0
- package/dist/lib/projects.js +313 -0
- package/dist/lib/remote-agents-json.d.ts +9 -0
- package/dist/lib/remote-agents-json.js +11 -5
- package/dist/lib/routine-notify.d.ts +11 -0
- package/dist/lib/routine-notify.js +22 -0
- package/dist/lib/run-notify.js +3 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +53 -10
- package/dist/lib/secrets/list-filter.d.ts +20 -5
- package/dist/lib/secrets/list-filter.js +22 -6
- package/dist/lib/secrets/usage-db.d.ts +106 -0
- package/dist/lib/secrets/usage-db.js +236 -0
- package/dist/lib/session/bash-command.d.ts +53 -0
- package/dist/lib/session/bash-command.js +364 -0
- package/dist/lib/session/digest.d.ts +6 -0
- package/dist/lib/session/digest.js +19 -0
- package/dist/lib/session/relative-time.d.ts +23 -0
- package/dist/lib/session/relative-time.js +60 -8
- package/dist/lib/session/remote-active.d.ts +5 -1
- package/dist/lib/session/remote-active.js +4 -1
- package/dist/lib/session/remote-list.js +5 -2
- package/dist/lib/session/render.d.ts +2 -9
- package/dist/lib/session/render.js +25 -56
- package/dist/lib/sqlite.js +28 -1
- package/dist/lib/ssh-exec.d.ts +6 -0
- package/dist/lib/ssh-exec.js +10 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +14 -0
- package/dist/lib/state.js +19 -0
- package/dist/lib/terminal/backends/index.d.ts +10 -2
- package/dist/lib/terminal/backends/index.js +14 -2
- package/dist/lib/terminal/backends/terminal-app.d.ts +13 -0
- package/dist/lib/terminal/backends/terminal-app.js +73 -0
- package/dist/lib/terminal/index.d.ts +2 -1
- package/dist/lib/terminal/index.js +2 -1
- package/dist/lib/terminal/preferred.d.ts +89 -0
- package/dist/lib/terminal/preferred.js +87 -0
- package/dist/lib/terminal/run-surface.d.ts +82 -0
- package/dist/lib/terminal/run-surface.js +146 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/types.d.ts +6 -5
- package/dist/lib/versions.d.ts +6 -0
- package/dist/lib/versions.js +6 -4
- package/package.json +2 -1
|
@@ -13,6 +13,7 @@ import { cleanSessionPrompt, extractSessionTopic } from './prompt.js';
|
|
|
13
13
|
import { renderMarkdown } from '../markdown.js';
|
|
14
14
|
import { redactSecrets } from '../redact.js';
|
|
15
15
|
import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from './digest.js';
|
|
16
|
+
import { classifyBashCommand, unwrapCommand, bucketKey } from './bash-command.js';
|
|
16
17
|
import { extractArtifacts, extractHooks, extractLinks, extractSkills } from './highlights.js';
|
|
17
18
|
import { extractTodoProgressFromEvents } from './state.js';
|
|
18
19
|
// ── Path helpers ──────────────────────────────────────────────────────────────
|
|
@@ -82,28 +83,6 @@ export function linkUrl(url, label) {
|
|
|
82
83
|
return supportsHyperlinks() ? osc8(url, label) : label;
|
|
83
84
|
}
|
|
84
85
|
// ── Command grouping ──────────────────────────────────────────────────────────
|
|
85
|
-
/**
|
|
86
|
-
* Unwrap wrapper prefixes to find the actual executable.
|
|
87
|
-
*/
|
|
88
|
-
export function unwrapCommand(cmd) {
|
|
89
|
-
const ssh = cmd.match(/^ssh\s+\S+\s+"(.+)"\s*(?:\|.*)?$/);
|
|
90
|
-
if (ssh)
|
|
91
|
-
return unwrapCommand(ssh[1]);
|
|
92
|
-
const lead = cmd.match(/^(?:sudo|env\s+\S+=\S+|time)\s+(.+)/);
|
|
93
|
-
if (lead)
|
|
94
|
-
return unwrapCommand(lead[1]);
|
|
95
|
-
// Strip shell-style leading env assignments: `PATH=/x CMD ...`, `FOO=bar BAR=baz CMD ...`
|
|
96
|
-
const shellEnv = cmd.match(/^(?:[A-Z_][A-Z0-9_]*=\S+\s+)+(\S.*)$/);
|
|
97
|
-
if (shellEnv)
|
|
98
|
-
return unwrapCommand(shellEnv[1]);
|
|
99
|
-
const cd = cmd.match(/^cd\s+\S+\s*&&\s*(.+)/);
|
|
100
|
-
if (cd)
|
|
101
|
-
return unwrapCommand(cd[1]);
|
|
102
|
-
const npx = cmd.match(/^npx\s+(.+)/);
|
|
103
|
-
if (npx)
|
|
104
|
-
return unwrapCommand(npx[1]);
|
|
105
|
-
return cmd;
|
|
106
|
-
}
|
|
107
86
|
/**
|
|
108
87
|
* Normalize a command so trivial flag/pipe variations collapse to the same key.
|
|
109
88
|
*/
|
|
@@ -120,45 +99,35 @@ export function normalizeForDedup(cmd) {
|
|
|
120
99
|
}
|
|
121
100
|
return s.trim();
|
|
122
101
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
export function bucketKey(cmd) {
|
|
143
|
-
const unwrapped = unwrapCommand(cmd);
|
|
144
|
-
const tokens = unwrapped.trim().split(/\s+/);
|
|
145
|
-
const first = tokens[0] ?? 'other';
|
|
146
|
-
const isRemote = cmd.trim().startsWith('ssh ') || cmd.trim().startsWith('scp ');
|
|
147
|
-
if (TWO_LEVEL_TOKENS.has(first) && tokens[1]) {
|
|
148
|
-
const key = `${first} ${tokens[1]}`;
|
|
149
|
-
return isRemote ? `ssh\u2192${key}` : key;
|
|
150
|
-
}
|
|
151
|
-
return isRemote ? `ssh\u2192${first}` : first;
|
|
152
|
-
}
|
|
102
|
+
// Re-export the shared classifier/bucketing for callers in this module's surface.
|
|
103
|
+
// `bucketKey` is the single source of truth in bash-command.ts (correct subcommand
|
|
104
|
+
// scan + `ssh\u2192` remote prefix); render must not keep a divergent copy.
|
|
105
|
+
export { unwrapCommand, bucketKey };
|
|
106
|
+
const CATEGORY_NAMES = {
|
|
107
|
+
vcs: 'VCS',
|
|
108
|
+
'build-test': 'Build/test',
|
|
109
|
+
install: 'Install',
|
|
110
|
+
remote: 'Remote',
|
|
111
|
+
http: 'HTTP',
|
|
112
|
+
media: 'Media',
|
|
113
|
+
upscaling: 'Upscaling',
|
|
114
|
+
metadata: 'Metadata',
|
|
115
|
+
probe: 'Probes',
|
|
116
|
+
search: 'Search',
|
|
117
|
+
shell: 'Shell',
|
|
118
|
+
wait: 'Wait',
|
|
119
|
+
other: 'Other',
|
|
120
|
+
};
|
|
153
121
|
function categoryOf(cmd) {
|
|
154
122
|
const rawFirst = cmd.trim().split(/\s+/)[0]?.toLowerCase() ?? '';
|
|
155
123
|
// Remote wrappers: classify as Remote regardless of inner command.
|
|
156
124
|
if (['ssh', 'scp', 'rsync'].includes(rawFirst)) {
|
|
157
|
-
return
|
|
125
|
+
return { name: 'Remote', signal: 'mid' };
|
|
158
126
|
}
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
127
|
+
const info = classifyBashCommand(cmd);
|
|
128
|
+
if (info.category === 'other')
|
|
129
|
+
return null;
|
|
130
|
+
return { name: CATEGORY_NAMES[info.category], signal: info.signal };
|
|
162
131
|
}
|
|
163
132
|
/**
|
|
164
133
|
* Collapse consecutive same-normalized commands within a 60-second window
|
package/dist/lib/sqlite.js
CHANGED
|
@@ -14,11 +14,38 @@
|
|
|
14
14
|
import { createRequire } from 'module';
|
|
15
15
|
const isBun = typeof globalThis.Bun !== 'undefined';
|
|
16
16
|
const require = createRequire(import.meta.url);
|
|
17
|
+
// node:sqlite emits a process-level ExperimentalWarning the first time it loads.
|
|
18
|
+
// The packaged CLI launches Node with --no-warnings=ExperimentalWarning, but a
|
|
19
|
+
// direct `node dist/...` run (and vitest's subprocesses) does not, so the warning
|
|
20
|
+
// would leak onto stderr and break any command whose --json output is asserted to
|
|
21
|
+
// be clean. Suppress only that single warning for the duration of the load; every
|
|
22
|
+
// other warning passes through untouched.
|
|
23
|
+
function loadNodeSqlite() {
|
|
24
|
+
const original = process.emitWarning;
|
|
25
|
+
const filtered = ((warning, ...rest) => {
|
|
26
|
+
const name = warning instanceof Error
|
|
27
|
+
? warning.name
|
|
28
|
+
: typeof rest[0] === 'string'
|
|
29
|
+
? rest[0]
|
|
30
|
+
: rest[0]?.type;
|
|
31
|
+
const message = warning instanceof Error ? warning.message : warning;
|
|
32
|
+
if (name === 'ExperimentalWarning' && /SQLite/i.test(String(message ?? '')))
|
|
33
|
+
return;
|
|
34
|
+
original.call(process, warning, ...rest);
|
|
35
|
+
});
|
|
36
|
+
process.emitWarning = filtered;
|
|
37
|
+
try {
|
|
38
|
+
return require('node:sqlite');
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
process.emitWarning = original;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
17
44
|
// Keep Node on createRequire() so Vitest doesn't try to prebundle the built-in
|
|
18
45
|
// sqlite module as a userland package during test collection.
|
|
19
46
|
const sqliteMod = isBun
|
|
20
47
|
? await import('bun:sqlite')
|
|
21
|
-
:
|
|
48
|
+
: loadNodeSqlite();
|
|
22
49
|
// bun:sqlite exports `Database`; node:sqlite exports `DatabaseSync`.
|
|
23
50
|
const NativeDatabase = sqliteMod.Database
|
|
24
51
|
?? sqliteMod.DatabaseSync;
|
package/dist/lib/ssh-exec.d.ts
CHANGED
|
@@ -73,6 +73,12 @@ export declare function sshExec(target: string, remoteCmd: string, opts?: SshExe
|
|
|
73
73
|
/**
|
|
74
74
|
* Async variant of {@link sshExec}. Same hardened argv composition, but uses
|
|
75
75
|
* child_process.spawn so fleet fan-outs can probe multiple hosts concurrently.
|
|
76
|
+
*
|
|
77
|
+
* A timeout-bearing call uses a fresh ssh connection (`multiplex: false`) even
|
|
78
|
+
* when the caller requests multiplexing: a control-master outlives the local
|
|
79
|
+
* client, so killing the local ssh process on timeout would leave the remote
|
|
80
|
+
* command running. With a direct connection, terminating the local child tears
|
|
81
|
+
* down the remote side (RUSH-2114).
|
|
76
82
|
*/
|
|
77
83
|
export declare function sshExecAsync(target: string, remoteCmd: string, opts?: SshExecOptions): Promise<SshExecResult>;
|
|
78
84
|
export interface SshExecRawResult {
|
package/dist/lib/ssh-exec.js
CHANGED
|
@@ -128,10 +128,19 @@ export function sshExec(target, remoteCmd, opts = {}) {
|
|
|
128
128
|
/**
|
|
129
129
|
* Async variant of {@link sshExec}. Same hardened argv composition, but uses
|
|
130
130
|
* child_process.spawn so fleet fan-outs can probe multiple hosts concurrently.
|
|
131
|
+
*
|
|
132
|
+
* A timeout-bearing call uses a fresh ssh connection (`multiplex: false`) even
|
|
133
|
+
* when the caller requests multiplexing: a control-master outlives the local
|
|
134
|
+
* client, so killing the local ssh process on timeout would leave the remote
|
|
135
|
+
* command running. With a direct connection, terminating the local child tears
|
|
136
|
+
* down the remote side (RUSH-2114).
|
|
131
137
|
*/
|
|
132
138
|
export function sshExecAsync(target, remoteCmd, opts = {}) {
|
|
133
139
|
assertValidSshTarget(target);
|
|
134
|
-
|
|
140
|
+
// Control-master connections defeat local timeouts — the master keeps the
|
|
141
|
+
// remote command alive after we kill the client. Force a fresh connection
|
|
142
|
+
// whenever the caller asked for a timeout so the timeout actually stops work.
|
|
143
|
+
const mux = opts.multiplex === false || opts.timeoutMs ? [] : controlOpts();
|
|
135
144
|
const args = [...sshConnectOpts(mux, opts.hostKeyOpts), ...(opts.extraSshArgs ?? []), target, remoteCmd];
|
|
136
145
|
return new Promise((resolve) => {
|
|
137
146
|
const child = spawn('ssh', args, {
|
|
@@ -44,6 +44,7 @@ export declare const loadExport: ModuleLoader;
|
|
|
44
44
|
export declare const loadPackages: ModuleLoader;
|
|
45
45
|
export declare const loadRoutines: ModuleLoader;
|
|
46
46
|
export declare const loadMonitors: ModuleLoader;
|
|
47
|
+
export declare const loadProjects: ModuleLoader;
|
|
47
48
|
export declare const loadRun: ModuleLoader;
|
|
48
49
|
export declare const loadFork: ModuleLoader;
|
|
49
50
|
export declare const loadDefaults: ModuleLoader;
|
|
@@ -22,6 +22,7 @@ export const loadExport = async () => (await import('../../commands/export.js'))
|
|
|
22
22
|
export const loadPackages = async () => (await import('../../commands/packages.js')).registerPackagesCommands;
|
|
23
23
|
export const loadRoutines = async () => (await import('../../commands/routines.js')).registerRoutinesCommands;
|
|
24
24
|
export const loadMonitors = async () => (await import('../../commands/monitors.js')).registerMonitorsCommands;
|
|
25
|
+
export const loadProjects = async () => (await import('../../commands/projects.js')).registerProjectsCommands;
|
|
25
26
|
export const loadRun = async () => (await import('../../commands/exec.js')).registerRunCommand;
|
|
26
27
|
export const loadFork = async () => (await import('../../commands/fork.js')).registerForkCommand;
|
|
27
28
|
export const loadDefaults = async () => (await import('../../commands/defaults.js')).registerDefaultsCommands;
|
|
@@ -134,6 +135,7 @@ export const COMMAND_LOADERS = {
|
|
|
134
135
|
install: [loadPackages],
|
|
135
136
|
routines: [loadRoutines],
|
|
136
137
|
monitors: [loadMonitors],
|
|
138
|
+
projects: [loadProjects],
|
|
137
139
|
run: [loadRun],
|
|
138
140
|
fork: [loadFork],
|
|
139
141
|
defaults: [loadDefaults],
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -98,6 +98,18 @@ export declare function getUserSubagentsDir(): string;
|
|
|
98
98
|
export declare function getSystemWorkflowsDir(): string;
|
|
99
99
|
export declare function getUserWorkflowsDir(): string;
|
|
100
100
|
export declare function getUserSecretsDir(): string;
|
|
101
|
+
/**
|
|
102
|
+
* Path to the secrets usage read-model database (~/.agents/secrets/secrets.db).
|
|
103
|
+
* Read at CALL time so a test can redirect it to a temp file via
|
|
104
|
+
* AGENTS_SECRETS_DB without racing the module-load capture of USER_SECRETS_DIR —
|
|
105
|
+
* mirrors the AGENTS_EVENTS_PATH / AGENTS_DEVICES_DIR escape hatches. Holds only
|
|
106
|
+
* value-free usage telemetry (which bundle was created/imported/exported/viewed/
|
|
107
|
+
* accessed/unlocked, when, by whom), never a secret value. It is a derived index
|
|
108
|
+
* fed FROM the emitSecretAudit chokepoint alongside the append-only
|
|
109
|
+
* ~/.agents/events.jsonl audit log — the same way sessions.db indexes session
|
|
110
|
+
* metadata off the real session flow — not a second write path.
|
|
111
|
+
*/
|
|
112
|
+
export declare function getSecretsDbPath(): string;
|
|
101
113
|
export declare function getUserPromptcutsPath(): string;
|
|
102
114
|
/** Canonical home anchor (HOME env override or os.homedir()). */
|
|
103
115
|
export declare function getHomeDir(): string;
|
|
@@ -109,6 +121,8 @@ export declare function getCacheDir(): string;
|
|
|
109
121
|
export declare function getPackagesDir(): string;
|
|
110
122
|
/** Path to routine YAML definitions (~/.agents/routines/). */
|
|
111
123
|
export declare function getRoutinesDir(): string;
|
|
124
|
+
/** Path to named project definitions (~/.agents/projects/). */
|
|
125
|
+
export declare function getProjectsDir(): string;
|
|
112
126
|
/**
|
|
113
127
|
* Path to webhook handler YAML definitions (~/.agents/webhooks/). Handlers are
|
|
114
128
|
* one-off triggers for agents/workflows/commands/routines, layered the same way
|
package/dist/lib/state.js
CHANGED
|
@@ -88,6 +88,9 @@ const WEBHOOKS_DIR = path.join(USER_AGENTS_DIR, 'webhooks');
|
|
|
88
88
|
// monitor is a routine whose trigger is a watched source instead of a clock.
|
|
89
89
|
const MONITORS_DIR = path.join(USER_AGENTS_DIR, 'monitors');
|
|
90
90
|
const TEAMS_DIR = path.join(USER_AGENTS_DIR, 'teams');
|
|
91
|
+
// Named project definitions (the layer above the --project convention). Sibling
|
|
92
|
+
// of ROUTINES_DIR/TEAMS_DIR: hand-editable YAML, synced across machines by push/pull.
|
|
93
|
+
const PROJECTS_DIR = path.join(USER_AGENTS_DIR, 'projects');
|
|
91
94
|
// History bucket (durable).
|
|
92
95
|
const SESSIONS_DIR = path.join(HISTORY_DIR, 'sessions');
|
|
93
96
|
const SESSIONS_DB_PATH = path.join(SESSIONS_DIR, 'sessions.db');
|
|
@@ -296,6 +299,20 @@ export function getUserSubagentsDir() { return USER_SUBAGENTS_DIR; }
|
|
|
296
299
|
export function getSystemWorkflowsDir() { return SYSTEM_WORKFLOWS_DIR; }
|
|
297
300
|
export function getUserWorkflowsDir() { return USER_WORKFLOWS_DIR; }
|
|
298
301
|
export function getUserSecretsDir() { return USER_SECRETS_DIR; }
|
|
302
|
+
/**
|
|
303
|
+
* Path to the secrets usage read-model database (~/.agents/secrets/secrets.db).
|
|
304
|
+
* Read at CALL time so a test can redirect it to a temp file via
|
|
305
|
+
* AGENTS_SECRETS_DB without racing the module-load capture of USER_SECRETS_DIR —
|
|
306
|
+
* mirrors the AGENTS_EVENTS_PATH / AGENTS_DEVICES_DIR escape hatches. Holds only
|
|
307
|
+
* value-free usage telemetry (which bundle was created/imported/exported/viewed/
|
|
308
|
+
* accessed/unlocked, when, by whom), never a secret value. It is a derived index
|
|
309
|
+
* fed FROM the emitSecretAudit chokepoint alongside the append-only
|
|
310
|
+
* ~/.agents/events.jsonl audit log — the same way sessions.db indexes session
|
|
311
|
+
* metadata off the real session flow — not a second write path.
|
|
312
|
+
*/
|
|
313
|
+
export function getSecretsDbPath() {
|
|
314
|
+
return process.env.AGENTS_SECRETS_DB ?? path.join(USER_SECRETS_DIR, 'secrets.db');
|
|
315
|
+
}
|
|
299
316
|
export function getUserPromptcutsPath() { return USER_PROMPTCUTS_FILE; }
|
|
300
317
|
// ─── User operational path getters ────────────────────────────────────────────
|
|
301
318
|
//
|
|
@@ -311,6 +328,8 @@ export function getCacheDir() { return CACHE_DIR; }
|
|
|
311
328
|
export function getPackagesDir() { return PACKAGES_DIR; }
|
|
312
329
|
/** Path to routine YAML definitions (~/.agents/routines/). */
|
|
313
330
|
export function getRoutinesDir() { return process.env.AGENTS_ROUTINES_DIR ?? ROUTINES_DIR; }
|
|
331
|
+
/** Path to named project definitions (~/.agents/projects/). */
|
|
332
|
+
export function getProjectsDir() { return process.env.AGENTS_PROJECTS_DIR ?? PROJECTS_DIR; }
|
|
314
333
|
/**
|
|
315
334
|
* Path to webhook handler YAML definitions (~/.agents/webhooks/). Handlers are
|
|
316
335
|
* one-off triggers for agents/workflows/commands/routines, layered the same way
|
|
@@ -6,7 +6,15 @@ import { itermBackend } from './iterm.js';
|
|
|
6
6
|
import { ghosttyBackend } from './ghostty.js';
|
|
7
7
|
import { tmuxBackend } from './tmux.js';
|
|
8
8
|
import { vscodiumAgentBackend } from './vscodium-agent.js';
|
|
9
|
-
|
|
9
|
+
import { terminalAppBackend } from './terminal-app.js';
|
|
10
|
+
/**
|
|
11
|
+
* All known interactive backends, keyed by id.
|
|
12
|
+
*
|
|
13
|
+
* Insertion order is the preference order `availableBackends` returns, and
|
|
14
|
+
* `terminal` (Terminal.app) sits LAST deliberately: it is the every-Mac floor, so
|
|
15
|
+
* a caller that falls back to the first available backend still prefers a real
|
|
16
|
+
* terminal the user chose to install.
|
|
17
|
+
*/
|
|
10
18
|
export declare const BACKENDS: Record<Backend, TerminalBackend>;
|
|
11
19
|
/**
|
|
12
20
|
* The backend for the terminal the CLI is currently running in, or null if we
|
|
@@ -16,4 +24,4 @@ export declare const BACKENDS: Record<Backend, TerminalBackend>;
|
|
|
16
24
|
export declare function detectCurrentBackend(ctx: EngineContext): Backend | null;
|
|
17
25
|
/** Backends that can actually be driven in this context. */
|
|
18
26
|
export declare function availableBackends(ctx: EngineContext): TerminalBackend[];
|
|
19
|
-
export { itermBackend, ghosttyBackend, tmuxBackend, vscodiumAgentBackend };
|
|
27
|
+
export { itermBackend, ghosttyBackend, tmuxBackend, vscodiumAgentBackend, terminalAppBackend };
|
|
@@ -2,12 +2,21 @@ import { itermBackend } from './iterm.js';
|
|
|
2
2
|
import { ghosttyBackend } from './ghostty.js';
|
|
3
3
|
import { tmuxBackend } from './tmux.js';
|
|
4
4
|
import { vscodiumAgentBackend } from './vscodium-agent.js';
|
|
5
|
-
|
|
5
|
+
import { terminalAppBackend } from './terminal-app.js';
|
|
6
|
+
/**
|
|
7
|
+
* All known interactive backends, keyed by id.
|
|
8
|
+
*
|
|
9
|
+
* Insertion order is the preference order `availableBackends` returns, and
|
|
10
|
+
* `terminal` (Terminal.app) sits LAST deliberately: it is the every-Mac floor, so
|
|
11
|
+
* a caller that falls back to the first available backend still prefers a real
|
|
12
|
+
* terminal the user chose to install.
|
|
13
|
+
*/
|
|
6
14
|
export const BACKENDS = {
|
|
7
15
|
iterm: itermBackend,
|
|
8
16
|
ghostty: ghosttyBackend,
|
|
9
17
|
tmux: tmuxBackend,
|
|
10
18
|
'vscodium-agent': vscodiumAgentBackend,
|
|
19
|
+
terminal: terminalAppBackend,
|
|
11
20
|
};
|
|
12
21
|
/**
|
|
13
22
|
* The backend for the terminal the CLI is currently running in, or null if we
|
|
@@ -22,10 +31,13 @@ export function detectCurrentBackend(ctx) {
|
|
|
22
31
|
return 'iterm';
|
|
23
32
|
if (term.includes('ghostty'))
|
|
24
33
|
return 'ghostty';
|
|
34
|
+
// Terminal.app sets TERM_PROGRAM=Apple_Terminal.
|
|
35
|
+
if (term.includes('apple_terminal'))
|
|
36
|
+
return 'terminal';
|
|
25
37
|
return null;
|
|
26
38
|
}
|
|
27
39
|
/** Backends that can actually be driven in this context. */
|
|
28
40
|
export function availableBackends(ctx) {
|
|
29
41
|
return Object.values(BACKENDS).filter((b) => b.isAvailable(ctx));
|
|
30
42
|
}
|
|
31
|
-
export { itermBackend, ghosttyBackend, tmuxBackend, vscodiumAgentBackend };
|
|
43
|
+
export { itermBackend, ghosttyBackend, tmuxBackend, vscodiumAgentBackend, terminalAppBackend };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TerminalBackend } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* AppleScript that opens a Terminal.app tab running the command.
|
|
4
|
+
*
|
|
5
|
+
* `do script` with no `in` target opens a NEW window; with a front window
|
|
6
|
+
* present, `tell application "System Events" to keystroke "t"` would be the only
|
|
7
|
+
* way to force a tab, and that needs an Accessibility grant. So: a window when
|
|
8
|
+
* none exists, otherwise `do script … in front window`, which Terminal renders
|
|
9
|
+
* as a new tab of that window when its "New tabs" setting is on and a new
|
|
10
|
+
* window otherwise. Either way the command runs in a fresh, visible surface.
|
|
11
|
+
*/
|
|
12
|
+
export declare function terminalAppTabScript(cwd: string, command: string[]): string;
|
|
13
|
+
export declare const terminalAppBackend: TerminalBackend;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal.app backend — drives macOS Terminal via AppleScript (`osascript`).
|
|
3
|
+
*
|
|
4
|
+
* This is the floor of the backend list: every Mac has Terminal.app, so a GUI
|
|
5
|
+
* caller that can name no better terminal still gets a surface. It is also what
|
|
6
|
+
* the menu bar's "New Session" always used before the engine took that path
|
|
7
|
+
* over, so registering it keeps a Terminal-only Mac working exactly as it did.
|
|
8
|
+
*
|
|
9
|
+
* Terminal.app has no scriptable split — `do script` only ever makes a tab (or a
|
|
10
|
+
* window). A split request therefore opens a TAB, which is stated here and in
|
|
11
|
+
* `buildSplit` rather than silently pretending the pane happened.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import { appleScriptStr } from '../quote.js';
|
|
15
|
+
import { loginExec, iLoginShell } from '../shell.js';
|
|
16
|
+
const TERMINAL_APP = '/System/Applications/Utilities/Terminal.app';
|
|
17
|
+
const TERMINAL_APP_LEGACY = '/Applications/Utilities/Terminal.app';
|
|
18
|
+
function appExists(p) {
|
|
19
|
+
try {
|
|
20
|
+
return fs.existsSync(p);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* AppleScript that opens a Terminal.app tab running the command.
|
|
28
|
+
*
|
|
29
|
+
* `do script` with no `in` target opens a NEW window; with a front window
|
|
30
|
+
* present, `tell application "System Events" to keystroke "t"` would be the only
|
|
31
|
+
* way to force a tab, and that needs an Accessibility grant. So: a window when
|
|
32
|
+
* none exists, otherwise `do script … in front window`, which Terminal renders
|
|
33
|
+
* as a new tab of that window when its "New tabs" setting is on and a new
|
|
34
|
+
* window otherwise. Either way the command runs in a fresh, visible surface.
|
|
35
|
+
*/
|
|
36
|
+
export function terminalAppTabScript(cwd, command) {
|
|
37
|
+
const cmd = appleScriptStr(iLoginShell(loginExec(cwd, command)));
|
|
38
|
+
return [
|
|
39
|
+
'tell application "Terminal"',
|
|
40
|
+
' activate',
|
|
41
|
+
' if (count of windows) is 0 then',
|
|
42
|
+
` do script ${cmd}`,
|
|
43
|
+
' else',
|
|
44
|
+
` do script ${cmd} in front window`,
|
|
45
|
+
' end if',
|
|
46
|
+
'end tell',
|
|
47
|
+
].join('\n');
|
|
48
|
+
}
|
|
49
|
+
export const terminalAppBackend = {
|
|
50
|
+
id: 'terminal',
|
|
51
|
+
label: 'Terminal',
|
|
52
|
+
/**
|
|
53
|
+
* Terminal.app is scriptable only from a session that can reach the GUI login
|
|
54
|
+
* — an `osascript` fired over plain SSH gets "Application isn't running" — so
|
|
55
|
+
* an SSH-hosted context reports it unavailable rather than launching into a
|
|
56
|
+
* window nobody can see. (A launchd GUI agent like the menu bar helper has no
|
|
57
|
+
* SSH_* vars and is available.)
|
|
58
|
+
*/
|
|
59
|
+
isAvailable(ctx) {
|
|
60
|
+
if (ctx.platform !== 'darwin')
|
|
61
|
+
return false;
|
|
62
|
+
if (ctx.env.SSH_CONNECTION || ctx.env.SSH_TTY)
|
|
63
|
+
return false;
|
|
64
|
+
return appExists(TERMINAL_APP) || appExists(TERMINAL_APP_LEGACY);
|
|
65
|
+
},
|
|
66
|
+
buildTab(cwd, command) {
|
|
67
|
+
return { argv: ['osascript', '-e', terminalAppTabScript(cwd, command)] };
|
|
68
|
+
},
|
|
69
|
+
/** Terminal.app cannot split by script — a split request opens a tab instead. */
|
|
70
|
+
buildSplit(cwd, command, _direction) {
|
|
71
|
+
return { argv: ['osascript', '-e', terminalAppTabScript(cwd, command)] };
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export type { Backend, SplitDirection, Layout, EngineContext, LaunchSpec, LaunchRequest, LaunchResult, TerminalBackend, } from './types.js';
|
|
10
10
|
export { currentContext } from './types.js';
|
|
11
|
-
export { BACKENDS, detectCurrentBackend, availableBackends, itermBackend, ghosttyBackend, tmuxBackend, vscodiumAgentBackend } from './backends/index.js';
|
|
11
|
+
export { BACKENDS, detectCurrentBackend, availableBackends, itermBackend, ghosttyBackend, tmuxBackend, vscodiumAgentBackend, terminalAppBackend } from './backends/index.js';
|
|
12
|
+
export { SESSION_HOST_BACKENDS, backendFromSessions, resolveLaunchBackend, describeBackendChoice, type SessionHostSample, type BackendSource, type LaunchBackendChoice, } from './preferred.js';
|
|
12
13
|
export { makeVscodiumAgentBackend, spawnUri, EDITOR_VARIANTS, type EditorVariant } from './backends/vscodium-agent.js';
|
|
13
14
|
export { planLayouts, type Packing } from './policy.js';
|
|
14
15
|
export { specForRequest, buildRequests, openSurface, openSurfaces, type OpenOptions, type OpenManyOptions, type BuildRequestsOptions, type SurfaceItem, } from './engine.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { currentContext } from './types.js';
|
|
2
|
-
export { BACKENDS, detectCurrentBackend, availableBackends, itermBackend, ghosttyBackend, tmuxBackend, vscodiumAgentBackend } from './backends/index.js';
|
|
2
|
+
export { BACKENDS, detectCurrentBackend, availableBackends, itermBackend, ghosttyBackend, tmuxBackend, vscodiumAgentBackend, terminalAppBackend } from './backends/index.js';
|
|
3
|
+
export { SESSION_HOST_BACKENDS, backendFromSessions, resolveLaunchBackend, describeBackendChoice, } from './preferred.js';
|
|
3
4
|
export { makeVscodiumAgentBackend, spawnUri, EDITOR_VARIANTS } from './backends/vscodium-agent.js';
|
|
4
5
|
export { planLayouts } from './policy.js';
|
|
5
6
|
export { specForRequest, buildRequests, openSurface, openSurfaces, } from './engine.js';
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which terminal to launch a NEW surface into, for callers that aren't in one.
|
|
3
|
+
*
|
|
4
|
+
* `detectCurrentBackend` answers this from `$TMUX` / `$TERM_PROGRAM`, which is
|
|
5
|
+
* correct for a command the user typed in a terminal — and useless to a GUI
|
|
6
|
+
* caller. The menu-bar helper is launched by launchd with no terminal in its
|
|
7
|
+
* ancestry, so it used to hardcode AppleScript against Terminal.app and opened
|
|
8
|
+
* every "New Session" there no matter what the user actually works in.
|
|
9
|
+
*
|
|
10
|
+
* The signal it was missing is already on disk: `agents sessions --active`
|
|
11
|
+
* attributes every live session to its host app (`ActiveSession.host`, resolved
|
|
12
|
+
* by walking the process table in session/active.ts). So the terminal the user
|
|
13
|
+
* demonstrably runs agents in is the terminal a new session should open in —
|
|
14
|
+
* that's what this module resolves, and it is the reason the menu bar now opens
|
|
15
|
+
* Ghostty for a Ghostty user and iTerm for an iTerm user.
|
|
16
|
+
*
|
|
17
|
+
* Resolution order (each step is skipped when it names nothing DRIVABLE here):
|
|
18
|
+
* 1. the terminal this process is running in -> 'current-terminal'
|
|
19
|
+
* 2. the host app of the most recent live session -> 'active-session'
|
|
20
|
+
* 3. the first available backend (Terminal.app is the every-Mac floor) -> 'available'
|
|
21
|
+
*/
|
|
22
|
+
import type { Backend, EngineContext } from './types.js';
|
|
23
|
+
/**
|
|
24
|
+
* `ActiveSession.host` -> the backend that can open a new surface in it.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately partial. A host is listed ONLY when the engine can really drive
|
|
27
|
+
* it, because a wrong mapping opens the wrong app and looks like success:
|
|
28
|
+
* - `code` / `cursor` / `windsurf` are absent even though the engine has an
|
|
29
|
+
* editor backend — the registered `vscodium-agent` is bound to the VSCodium
|
|
30
|
+
* variant (backends/vscodium-agent.ts: `EDITOR_VARIANTS[0]`), so mapping
|
|
31
|
+
* Cursor to it would open VSCodium instead of Cursor.
|
|
32
|
+
* - `warp` / `kitty` / `wezterm` / `alacritty` / `hyper` / `screen` are absent
|
|
33
|
+
* because no backend drives them yet.
|
|
34
|
+
* An unmapped host isn't an error — resolution simply moves to the next session,
|
|
35
|
+
* and ultimately to the available-backend floor.
|
|
36
|
+
*/
|
|
37
|
+
export declare const SESSION_HOST_BACKENDS: Readonly<Record<string, Backend>>;
|
|
38
|
+
/** The subset of an ActiveSession this resolver reads. */
|
|
39
|
+
export interface SessionHostSample {
|
|
40
|
+
host?: string;
|
|
41
|
+
/**
|
|
42
|
+
* For a tmux-hosted session: the app its attached tmux client is in
|
|
43
|
+
* (`ActiveSession.viewingIn.app`). This is load-bearing, not a nicety —
|
|
44
|
+
* `agents run` wraps interactive runs in tmux, so a session the user started
|
|
45
|
+
* in Ghostty is attributed `host: 'tmux'` and the terminal it is actually
|
|
46
|
+
* displayed in is only knowable through the attached client. Takes precedence
|
|
47
|
+
* over `host`, which for these sessions names the multiplexer, not a terminal.
|
|
48
|
+
*/
|
|
49
|
+
viewingApp?: string;
|
|
50
|
+
lastActivityMs?: number;
|
|
51
|
+
startedAtMs?: number;
|
|
52
|
+
}
|
|
53
|
+
/** Where the chosen backend came from — reported so a caller can say why. */
|
|
54
|
+
export type BackendSource = 'forced' | 'current-terminal' | 'active-session' | 'available';
|
|
55
|
+
export interface LaunchBackendChoice {
|
|
56
|
+
backend: Backend;
|
|
57
|
+
source: BackendSource;
|
|
58
|
+
/** The `ActiveSession.host` that selected it, when source is 'active-session'. */
|
|
59
|
+
host?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Injection seam so the PRECEDENCE is testable without the machine's installed
|
|
63
|
+
* apps. `isAvailable` for iterm/ghostty/terminal probes `/Applications`, so a
|
|
64
|
+
* test that just passes `platform: 'darwin'` silently depends on the host having
|
|
65
|
+
* those apps — it passes on a dev Mac and fails on a Linux CI runner. Same shape
|
|
66
|
+
* as `ViewingInDeps` in session/viewing-in.ts.
|
|
67
|
+
*/
|
|
68
|
+
export interface BackendResolveDeps {
|
|
69
|
+
/** Defaults to the real backend availability probe. */
|
|
70
|
+
isAvailable?: (backend: Backend, ctx: EngineContext) => boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The backend for the terminal the user's most recent live session runs in, or
|
|
74
|
+
* null when no live session names a host this engine can drive. Pure — the
|
|
75
|
+
* caller supplies the sessions, so the precedence is unit-testable without a
|
|
76
|
+
* process table.
|
|
77
|
+
*/
|
|
78
|
+
export declare function backendFromSessions(sessions: SessionHostSample[], ctx: EngineContext, deps?: BackendResolveDeps): {
|
|
79
|
+
backend: Backend;
|
|
80
|
+
host: string;
|
|
81
|
+
} | null;
|
|
82
|
+
/**
|
|
83
|
+
* Resolve the terminal to open a new surface in. Pure. Returns null when this
|
|
84
|
+
* machine has no drivable terminal at all (Linux with no tmux, an SSH session),
|
|
85
|
+
* which callers must handle — never a guess that silently goes nowhere.
|
|
86
|
+
*/
|
|
87
|
+
export declare function resolveLaunchBackend(ctx: EngineContext, sessions?: SessionHostSample[], deps?: BackendResolveDeps): LaunchBackendChoice | null;
|
|
88
|
+
/** One line explaining a choice, for the `agents run --terminal` preamble. */
|
|
89
|
+
export declare function describeBackendChoice(choice: LaunchBackendChoice): string;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { BACKENDS, detectCurrentBackend } from './backends/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* `ActiveSession.host` -> the backend that can open a new surface in it.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately partial. A host is listed ONLY when the engine can really drive
|
|
6
|
+
* it, because a wrong mapping opens the wrong app and looks like success:
|
|
7
|
+
* - `code` / `cursor` / `windsurf` are absent even though the engine has an
|
|
8
|
+
* editor backend — the registered `vscodium-agent` is bound to the VSCodium
|
|
9
|
+
* variant (backends/vscodium-agent.ts: `EDITOR_VARIANTS[0]`), so mapping
|
|
10
|
+
* Cursor to it would open VSCodium instead of Cursor.
|
|
11
|
+
* - `warp` / `kitty` / `wezterm` / `alacritty` / `hyper` / `screen` are absent
|
|
12
|
+
* because no backend drives them yet.
|
|
13
|
+
* An unmapped host isn't an error — resolution simply moves to the next session,
|
|
14
|
+
* and ultimately to the available-backend floor.
|
|
15
|
+
*/
|
|
16
|
+
export const SESSION_HOST_BACKENDS = Object.freeze({
|
|
17
|
+
iterm: 'iterm',
|
|
18
|
+
ghostty: 'ghostty',
|
|
19
|
+
terminal: 'terminal',
|
|
20
|
+
tmux: 'tmux',
|
|
21
|
+
codium: 'vscodium-agent',
|
|
22
|
+
});
|
|
23
|
+
/** Most recent first. Falls back to start time, then to the given order. */
|
|
24
|
+
function byRecency(a, b) {
|
|
25
|
+
const at = a.lastActivityMs ?? a.startedAtMs ?? 0;
|
|
26
|
+
const bt = b.lastActivityMs ?? b.startedAtMs ?? 0;
|
|
27
|
+
return bt - at;
|
|
28
|
+
}
|
|
29
|
+
const realIsAvailable = (backend, ctx) => BACKENDS[backend].isAvailable(ctx);
|
|
30
|
+
/**
|
|
31
|
+
* The backend for the terminal the user's most recent live session runs in, or
|
|
32
|
+
* null when no live session names a host this engine can drive. Pure — the
|
|
33
|
+
* caller supplies the sessions, so the precedence is unit-testable without a
|
|
34
|
+
* process table.
|
|
35
|
+
*/
|
|
36
|
+
export function backendFromSessions(sessions, ctx, deps = {}) {
|
|
37
|
+
const isAvailable = deps.isAvailable ?? realIsAvailable;
|
|
38
|
+
for (const s of [...sessions].sort(byRecency)) {
|
|
39
|
+
// The app an attached tmux client is in beats the multiplexer name.
|
|
40
|
+
const host = s.viewingApp ?? s.host;
|
|
41
|
+
if (!host)
|
|
42
|
+
continue;
|
|
43
|
+
// hasOwn, not a bare index: the host is data, and `SESSION_HOST_BACKENDS['constructor']`
|
|
44
|
+
// would otherwise hand back a prototype member that BACKENDS cannot key on.
|
|
45
|
+
if (!Object.hasOwn(SESSION_HOST_BACKENDS, host))
|
|
46
|
+
continue;
|
|
47
|
+
const backend = SESSION_HOST_BACKENDS[host];
|
|
48
|
+
if (!backend)
|
|
49
|
+
continue;
|
|
50
|
+
if (!isAvailable(backend, ctx))
|
|
51
|
+
continue;
|
|
52
|
+
return { backend, host };
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the terminal to open a new surface in. Pure. Returns null when this
|
|
58
|
+
* machine has no drivable terminal at all (Linux with no tmux, an SSH session),
|
|
59
|
+
* which callers must handle — never a guess that silently goes nowhere.
|
|
60
|
+
*/
|
|
61
|
+
export function resolveLaunchBackend(ctx, sessions = [], deps = {}) {
|
|
62
|
+
const isAvailable = deps.isAvailable ?? realIsAvailable;
|
|
63
|
+
const current = detectCurrentBackend(ctx);
|
|
64
|
+
if (current && isAvailable(current, ctx)) {
|
|
65
|
+
return { backend: current, source: 'current-terminal' };
|
|
66
|
+
}
|
|
67
|
+
const fromSession = backendFromSessions(sessions, ctx, deps);
|
|
68
|
+
if (fromSession) {
|
|
69
|
+
return { backend: fromSession.backend, source: 'active-session', host: fromSession.host };
|
|
70
|
+
}
|
|
71
|
+
const first = Object.keys(BACKENDS).find((b) => isAvailable(b, ctx));
|
|
72
|
+
return first ? { backend: first, source: 'available' } : null;
|
|
73
|
+
}
|
|
74
|
+
/** One line explaining a choice, for the `agents run --terminal` preamble. */
|
|
75
|
+
export function describeBackendChoice(choice) {
|
|
76
|
+
const label = BACKENDS[choice.backend].label;
|
|
77
|
+
switch (choice.source) {
|
|
78
|
+
case 'forced':
|
|
79
|
+
return `${label} (you asked for it)`;
|
|
80
|
+
case 'current-terminal':
|
|
81
|
+
return `${label} (the terminal you're in)`;
|
|
82
|
+
case 'active-session':
|
|
83
|
+
return `${label} (where your ${choice.host} sessions run)`;
|
|
84
|
+
case 'available':
|
|
85
|
+
return `${label} (no running session named a terminal)`;
|
|
86
|
+
}
|
|
87
|
+
}
|