@phnx-labs/agents-cli 1.20.73 → 1.20.76
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 +989 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +31 -1
- package/dist/commands/exec.js +31 -3
- package/dist/commands/export.d.ts +11 -0
- package/dist/commands/export.js +215 -0
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +87 -9
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +15 -2
- package/dist/commands/secrets.js +44 -21
- package/dist/commands/sessions-browser.js +1 -1
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.js +16 -1
- package/dist/commands/sessions.d.ts +36 -13
- package/dist/commands/sessions.js +210 -40
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/setup.js +6 -1
- package/dist/commands/ssh.js +71 -35
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +51 -11
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +99 -31
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +14 -4
- package/dist/lib/activity.js +17 -5
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +56 -0
- package/dist/lib/agents.js +133 -7
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.d.ts +16 -0
- package/dist/lib/config-transfer.js +60 -0
- package/dist/lib/daemon.js +18 -2
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +21 -0
- package/dist/lib/devices/fleet.js +26 -0
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +66 -0
- package/dist/lib/exec.js +195 -6
- package/dist/lib/export.d.ts +72 -0
- package/dist/lib/export.js +269 -0
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +2 -0
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +13 -5
- package/dist/lib/hosts/run-target.js +26 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +37 -2
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +25 -0
- package/dist/lib/import.js +107 -2
- package/dist/lib/isolation-boundary-report.d.ts +10 -0
- package/dist/lib/isolation-boundary-report.js +35 -0
- package/dist/lib/menubar/install-menubar.d.ts +20 -0
- package/dist/lib/menubar/install-menubar.js +45 -1
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +7 -2
- package/dist/lib/overdue.js +18 -11
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/project-resources.js +20 -5
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routines.d.ts +26 -0
- package/dist/lib/routines.js +19 -2
- package/dist/lib/runner.js +104 -7
- package/dist/lib/secrets/agent.d.ts +66 -1
- package/dist/lib/secrets/agent.js +154 -90
- package/dist/lib/secrets/bundles.d.ts +34 -0
- package/dist/lib/secrets/bundles.js +179 -19
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +21 -3
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +114 -7
- package/dist/lib/session/active.js +323 -52
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +48 -0
- package/dist/lib/session/db.js +238 -14
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +465 -2
- package/dist/lib/session/discover.js +1648 -468
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +130 -30
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +22 -10
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.js +6 -2
- package/dist/lib/shims.d.ts +27 -1
- package/dist/lib/shims.js +88 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +15 -36
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +48 -2
- package/dist/lib/uninstall.js +1 -45
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +22 -2
- package/dist/lib/versions.js +98 -10
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
package/dist/commands/exec.js
CHANGED
|
@@ -398,6 +398,11 @@ export function registerRunCommand(program) {
|
|
|
398
398
|
// `--on` and `--computer` are hidden aliases of `--host` — same behavior.
|
|
399
399
|
runCmd.addOption(new Option('--on <name>', 'Alias of --host.').hideHelp());
|
|
400
400
|
runCmd.addOption(new Option('--computer <name>', 'Alias of --host.').hideHelp());
|
|
401
|
+
// Internal: the `--host` dispatch forwards this so the REMOTE run prints its
|
|
402
|
+
// resolved session id as a one-line stdout sentinel (hosts/session-marker.ts),
|
|
403
|
+
// letting the launcher relate the remote-created session back to itself for
|
|
404
|
+
// every agent — not just Claude, whose id it forces up front.
|
|
405
|
+
runCmd.addOption(new Option('--emit-session-id', 'internal: print the resolved session id for a --host launcher to capture').hideHelp());
|
|
401
406
|
// Required for the documented `agents run <agent> [prompt] -- <native flags>`
|
|
402
407
|
// passthrough: commander >=13 rejects excess operands by default, so any
|
|
403
408
|
// post-`--` token died with "too many arguments" before the action ran. The
|
|
@@ -1043,6 +1048,7 @@ export function registerRunCommand(program) {
|
|
|
1043
1048
|
name: options.name,
|
|
1044
1049
|
});
|
|
1045
1050
|
}
|
|
1051
|
+
const isRaw = options.raw || options.tmux === false || options.disableTmux === true;
|
|
1046
1052
|
const exitCode = await runInteractiveOnHost(host, {
|
|
1047
1053
|
agent: runAgent,
|
|
1048
1054
|
version: resumeId ? undefined : runVersion,
|
|
@@ -1064,10 +1070,28 @@ export function registerRunCommand(program) {
|
|
|
1064
1070
|
name: options.name,
|
|
1065
1071
|
resume: resumeId,
|
|
1066
1072
|
passthroughArgs,
|
|
1067
|
-
raw:
|
|
1073
|
+
raw: isRaw,
|
|
1068
1074
|
forceInteractive: options.interactive,
|
|
1069
1075
|
copyCreds: hostCopyCreds,
|
|
1070
1076
|
});
|
|
1077
|
+
// A network drop kills the local ssh client (exit 255) but the remote
|
|
1078
|
+
// agent survives in its detached tmux session. With a known session id
|
|
1079
|
+
// (Claude, or a resumed run) and a tmux-hosted run, re-attach the live
|
|
1080
|
+
// pane automatically instead of exiting — the user never has to notice
|
|
1081
|
+
// the drop and `agents sessions focus` by hand. `raw` runs aren't tmux
|
|
1082
|
+
// wrapped, so there is nothing to reconnect to. Non-Claude id capture is
|
|
1083
|
+
// tracked in RUSH-2007.
|
|
1084
|
+
const reconnectId = hostSessionId ?? resumeId;
|
|
1085
|
+
if (reconnectId && !isRaw) {
|
|
1086
|
+
const { reconnectInteractiveSession, SSH_CONN_FAILURE } = await import('../lib/hosts/reconnect.js');
|
|
1087
|
+
if (exitCode === SSH_CONN_FAILURE) {
|
|
1088
|
+
process.exit(await reconnectInteractiveSession({
|
|
1089
|
+
host,
|
|
1090
|
+
sessionId: reconnectId,
|
|
1091
|
+
initialExit: exitCode,
|
|
1092
|
+
}));
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1071
1095
|
process.exit(exitCode);
|
|
1072
1096
|
}
|
|
1073
1097
|
// Headless host run: launch detached, tail the remote log, and follow
|
|
@@ -1221,7 +1245,7 @@ export function registerRunCommand(program) {
|
|
|
1221
1245
|
await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
|
|
1222
1246
|
process.exit(resumeExit);
|
|
1223
1247
|
}
|
|
1224
|
-
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported, isHeadlessSecretsContext }, { splitBundleRef,
|
|
1248
|
+
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive, inferredInteractiveWithoutTty }, { ALL_AGENT_IDS, ACCOUNT_INSPECTION_AGENT_IDS, agentLabel, supportsAccountInspection }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported, isHeadlessSecretsContext }, { splitBundleRef, resolveHostSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents, ensureSubagentDispatchTool }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports }, { shareRuntimeEnv },] = await Promise.all([
|
|
1225
1249
|
import('../lib/exec.js'),
|
|
1226
1250
|
import('../lib/agents.js'),
|
|
1227
1251
|
import('../lib/profiles.js'),
|
|
@@ -1851,7 +1875,7 @@ export function registerRunCommand(program) {
|
|
|
1851
1875
|
assertRemoteBundleFlagsUnsupported(bundleName, host, { keys: secretsKeysSubset, allowExpired: options.allowExpired }, { keysFlag: '--secrets-keys', allowExpiredFlag: '--allow-expired' });
|
|
1852
1876
|
// Remote bundle (`bundle@host`): resolve over SSH and inject
|
|
1853
1877
|
// ephemerally — values never touch this machine's keychain or disk.
|
|
1854
|
-
const target = await
|
|
1878
|
+
const target = await resolveHostSshTarget(host);
|
|
1855
1879
|
const bundleEnv = await remoteResolveEnv(target, bundleName, { osLookupName: host });
|
|
1856
1880
|
console.log(chalk.gray(`[secrets] Resolved ${bundleName}@${host}: ${Object.keys(bundleEnv).length} keys (remote, ephemeral)`));
|
|
1857
1881
|
secretsEnv = { ...secretsEnv, ...bundleEnv };
|
|
@@ -1923,6 +1947,10 @@ export function registerRunCommand(program) {
|
|
|
1923
1947
|
toolsRestrict: workflowToolsRestrict,
|
|
1924
1948
|
mcpConfigPath: workflowMcpConfigPath,
|
|
1925
1949
|
passthroughArgs,
|
|
1950
|
+
// Set only on the REMOTE side of a `--host` dispatch (the launcher
|
|
1951
|
+
// forwards `--emit-session-id`): print the resolved session id as a
|
|
1952
|
+
// stdout sentinel so the launcher captures the id this run coined.
|
|
1953
|
+
emitSessionId: options.emitSessionId === true,
|
|
1926
1954
|
};
|
|
1927
1955
|
if (options.interactive && options.headless) {
|
|
1928
1956
|
console.error(chalk.red('--interactive and --headless are mutually exclusive. Pass one, or neither (mode is inferred from prompt presence).'));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents export <agent>[@<version>]` — copy an isolated install's config out to the
|
|
3
|
+
* user's real `~/.<agent>`.
|
|
4
|
+
*
|
|
5
|
+
* The exit door for `--isolated`. Default is additive: your files are never modified,
|
|
6
|
+
* and anything that collides is written beside yours so you can diff and take what you
|
|
7
|
+
* want. A receipt records what came from the export, so "which of these are mine?" has
|
|
8
|
+
* an answer and the whole thing can be undone.
|
|
9
|
+
*/
|
|
10
|
+
import type { Command } from 'commander';
|
|
11
|
+
export declare function registerExportCommand(program: Command): void;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
|
+
import { confirm, select } from '@inquirer/prompts';
|
|
4
|
+
import { AGENTS, agentLabel, formatAgentError, resolveAgentName } from '../lib/agents.js';
|
|
5
|
+
import { listInstalledVersions, isVersionIsolated } from '../lib/versions.js';
|
|
6
|
+
import { planExport, executeExport, CONFLICT_SUFFIX } from '../lib/export.js';
|
|
7
|
+
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
8
|
+
function isolatedVersions(agent) {
|
|
9
|
+
return listInstalledVersions(agent).filter((v) => isVersionIsolated(agent, v));
|
|
10
|
+
}
|
|
11
|
+
/** Explain a blocker in terms of what the user should do instead. */
|
|
12
|
+
function reportBlocker(plan) {
|
|
13
|
+
const label = agentLabel(plan.agent);
|
|
14
|
+
const b = plan.blocker;
|
|
15
|
+
switch (b.kind) {
|
|
16
|
+
case 'not-installed':
|
|
17
|
+
console.log(chalk.red(`${label}@${plan.version} is not installed.`));
|
|
18
|
+
console.log(chalk.gray(` Installed: ${listInstalledVersions(plan.agent).join(', ') || 'none'}`));
|
|
19
|
+
break;
|
|
20
|
+
case 'not-isolated':
|
|
21
|
+
console.log(chalk.yellow(`${label}@${plan.version} is not an isolated install.`));
|
|
22
|
+
console.log(chalk.gray(` A normal install's config already IS ${plan.dest} (adoption symlinks it),`));
|
|
23
|
+
console.log(chalk.gray(' so there is nothing to export. To un-adopt it, use: agents uninstall'));
|
|
24
|
+
break;
|
|
25
|
+
case 'no-config':
|
|
26
|
+
console.log(chalk.yellow(`${label}@${plan.version} has no config to export yet (${b.source} is empty).`));
|
|
27
|
+
console.log(chalk.gray(` Run it once first: agents run ${plan.agent}@${plan.version}`));
|
|
28
|
+
break;
|
|
29
|
+
case 'dest-adopted':
|
|
30
|
+
console.log(chalk.red(`${b.realPath} is managed by agents-cli (adopted by ${label}@${b.adoptedVersion}).`));
|
|
31
|
+
console.log(chalk.gray(' Exporting would write into that version\'s home, not your real config.'));
|
|
32
|
+
console.log(chalk.gray(` Release it first: agents uninstall (or agents remove ${plan.agent}@${b.adoptedVersion})`));
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Unified diff between the user's file and the incoming one, via git. */
|
|
37
|
+
function printDiff(existing, incoming) {
|
|
38
|
+
try {
|
|
39
|
+
execFileSync('git', ['diff', '--no-index', '--color', '--', existing, incoming], {
|
|
40
|
+
stdio: ['ignore', 'inherit', 'ignore'],
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// git diff --no-index exits 1 when files differ; output already streamed.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function printPlan(plan, opts) {
|
|
48
|
+
const label = agentLabel(plan.agent);
|
|
49
|
+
console.log(chalk.bold(`Export ${label}@${plan.version} -> ${plan.dest} ${chalk.gray(`[${plan.mode}]`)}\n`));
|
|
50
|
+
console.log(` ${chalk.gray('from')} ${plan.source}`);
|
|
51
|
+
console.log(` ${chalk.gray('to')} ${plan.stagedPath ?? plan.dest}`);
|
|
52
|
+
console.log();
|
|
53
|
+
if (plan.mode === 'replace') {
|
|
54
|
+
console.log(chalk.yellow(` Replacing ${plan.dest} wholesale — ${plan.writes.length} file(s).`));
|
|
55
|
+
if (plan.backupPath)
|
|
56
|
+
console.log(chalk.gray(` Your current config moves to: ${plan.backupPath}`));
|
|
57
|
+
else if (plan.destKind === 'foreign-symlink') {
|
|
58
|
+
console.log(chalk.yellow(` ${plan.dest} is a symlink agents-cli does not own; it will be removed.`));
|
|
59
|
+
console.log(chalk.gray(' Its target is left untouched.'));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else if (plan.mode === 'staged') {
|
|
63
|
+
console.log(chalk.gray(` Writing ${plan.writes.length} file(s) into ${plan.stagedPath}.`));
|
|
64
|
+
console.log(chalk.gray(' Nothing is activated — your config is untouched.'));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
if (plan.writes.length > 0) {
|
|
68
|
+
console.log(chalk.green(` ${plan.writes.length} new file(s) will be added:`));
|
|
69
|
+
for (const w of plan.writes.slice(0, 15))
|
|
70
|
+
console.log(chalk.gray(` + ${w.rel}`));
|
|
71
|
+
if (plan.writes.length > 15)
|
|
72
|
+
console.log(chalk.gray(` + ${plan.writes.length - 15} more`));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.log(chalk.gray(' No new files to add.'));
|
|
76
|
+
}
|
|
77
|
+
if (plan.conflicts.length > 0) {
|
|
78
|
+
console.log();
|
|
79
|
+
console.log(chalk.yellow(` ${plan.conflicts.length} file(s) you already have — YOURS ARE NOT MODIFIED.`));
|
|
80
|
+
console.log(chalk.gray(` The incoming version is written beside each as *${CONFLICT_SUFFIX}:`));
|
|
81
|
+
for (const c of plan.conflicts.slice(0, 15))
|
|
82
|
+
console.log(chalk.gray(` ~ ${c.rel}`));
|
|
83
|
+
if (plan.conflicts.length > 15)
|
|
84
|
+
console.log(chalk.gray(` ~ ${plan.conflicts.length - 15} more`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (opts.diff && plan.conflicts.length > 0) {
|
|
88
|
+
for (const c of plan.conflicts) {
|
|
89
|
+
console.log(chalk.bold(`\n─── ${c.rel} ───`));
|
|
90
|
+
printDiff(c.existing, c.source);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
console.log(chalk.gray(`\n Receipt: ${plan.receiptPath}`));
|
|
94
|
+
}
|
|
95
|
+
export function registerExportCommand(program) {
|
|
96
|
+
program
|
|
97
|
+
.command('export <spec>')
|
|
98
|
+
.description('Copy an isolated install\'s config out to your real ~/.<agent>')
|
|
99
|
+
.option('--replace', 'Replace your config wholesale (yours is backed up) instead of merging')
|
|
100
|
+
.option('--staged', 'Write into ~/.<agent>/.agents-export-<ts>/ and activate nothing')
|
|
101
|
+
.option('--diff', 'Show a unified diff for each file you already have')
|
|
102
|
+
.option('--dry-run', 'Show what would change without writing anything')
|
|
103
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
104
|
+
.addHelpText('after', `
|
|
105
|
+
Modes:
|
|
106
|
+
(default) merge — additive. Your files are NEVER modified. Anything that
|
|
107
|
+
collides is written beside yours as <name>${CONFLICT_SUFFIX}
|
|
108
|
+
so you can diff and take the parts you want.
|
|
109
|
+
--replace — the isolated config becomes ~/.<agent>; yours is moved to
|
|
110
|
+
backups/<agent>/<ts>. For promoting a sandbox wholesale.
|
|
111
|
+
--staged — dump the tree into ~/.<agent>/.agents-export-<ts>/ and
|
|
112
|
+
activate nothing. For inspecting first.
|
|
113
|
+
|
|
114
|
+
Every mode writes a receipt to ~/.<agent>/.agents-cli-export.json listing exactly
|
|
115
|
+
what came from the export — so you can tell your settings from the CLI's, and undo it.
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
agents export codex --dry-run # see the plan
|
|
119
|
+
agents export codex --diff # ...and the deltas on colliding files
|
|
120
|
+
agents export codex # additive; nothing of yours changes
|
|
121
|
+
agents export codex@0.144.6 --replace # promote the sandbox wholesale
|
|
122
|
+
|
|
123
|
+
Note: file CONTENTS are never auto-merged. The TOML parser here does not preserve
|
|
124
|
+
comments, so merging keys would silently delete them. You get both files and a diff.
|
|
125
|
+
`)
|
|
126
|
+
.action(async (spec, options) => {
|
|
127
|
+
if (options.replace && options.staged) {
|
|
128
|
+
console.error(chalk.red('--replace and --staged are mutually exclusive; pass only one.'));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
const mode = options.replace ? 'replace' : options.staged ? 'staged' : 'merge';
|
|
132
|
+
const [rawAgent, rawVersion] = spec.split('@');
|
|
133
|
+
const agent = resolveAgentName(rawAgent);
|
|
134
|
+
if (!agent) {
|
|
135
|
+
console.error(chalk.red(formatAgentError(rawAgent)));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
let version = rawVersion;
|
|
139
|
+
if (!version) {
|
|
140
|
+
const candidates = isolatedVersions(agent);
|
|
141
|
+
if (candidates.length === 0) {
|
|
142
|
+
console.log(chalk.yellow(`No isolated ${agentLabel(agent)} installs to export.`));
|
|
143
|
+
console.log(chalk.gray(` Create one with: agents add ${agent}@<version> --isolated`));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (candidates.length === 1) {
|
|
147
|
+
version = candidates[0];
|
|
148
|
+
}
|
|
149
|
+
else if (isInteractiveTerminal()) {
|
|
150
|
+
version = await select({
|
|
151
|
+
message: `Which isolated ${agentLabel(agent)} version?`,
|
|
152
|
+
choices: candidates.map((v) => ({ name: v, value: v })),
|
|
153
|
+
}).catch((err) => {
|
|
154
|
+
if (isPromptCancelled(err))
|
|
155
|
+
process.exit(130);
|
|
156
|
+
throw err;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
console.error(chalk.red(`${agentLabel(agent)} has several isolated installs; name one.`));
|
|
161
|
+
console.error(chalk.gray(` ${candidates.map((v) => `agents export ${agent}@${v}`).join('\n ')}`));
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const timestamp = Date.now();
|
|
166
|
+
const plan = planExport(agent, version, timestamp, mode);
|
|
167
|
+
if (plan.blocker) {
|
|
168
|
+
reportBlocker(plan);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
printPlan(plan, { diff: options.diff });
|
|
172
|
+
if (options.dryRun) {
|
|
173
|
+
console.log(chalk.gray('\nDry run — nothing was written.'));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Only `--replace` can destroy anything, so it is the only mode that demands
|
|
177
|
+
// confirmation. merge and staged are additive by construction.
|
|
178
|
+
if (mode === 'replace' && !options.yes) {
|
|
179
|
+
if (!isInteractiveTerminal()) {
|
|
180
|
+
console.error(chalk.red('\nRefusing to replace your config without confirmation in a non-interactive shell.'));
|
|
181
|
+
console.error(chalk.gray(' Re-run with --yes once you have checked --dry-run.'));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
const ok = await confirm({ message: `Replace ${plan.dest}?`, default: false }).catch((err) => {
|
|
185
|
+
if (isPromptCancelled(err))
|
|
186
|
+
process.exit(130);
|
|
187
|
+
throw err;
|
|
188
|
+
});
|
|
189
|
+
if (!ok) {
|
|
190
|
+
console.log(chalk.gray('Aborted.'));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const result = executeExport(plan, timestamp);
|
|
195
|
+
if (!result.exported) {
|
|
196
|
+
console.error(chalk.red(`Export failed: ${result.errors.join('; ')}`));
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
console.log();
|
|
200
|
+
if (result.written.length > 0)
|
|
201
|
+
console.log(chalk.green(`Added ${result.written.length} file(s) to ${result.dest}`));
|
|
202
|
+
if (result.conflicts.length > 0) {
|
|
203
|
+
console.log(chalk.yellow(`${result.conflicts.length} of your file(s) were left untouched; incoming copies written as *${CONFLICT_SUFFIX}`));
|
|
204
|
+
console.log(chalk.gray(` Compare them with: agents export ${agent}@${version} --diff --dry-run`));
|
|
205
|
+
}
|
|
206
|
+
if (result.backupPath)
|
|
207
|
+
console.log(chalk.gray(`Previous config kept at: ${result.backupPath}`));
|
|
208
|
+
if (result.stagedPath)
|
|
209
|
+
console.log(chalk.gray(`Staged (nothing activated): ${result.stagedPath}`));
|
|
210
|
+
console.log(chalk.gray(`Receipt: ${result.receiptPath}`));
|
|
211
|
+
if (mode !== 'staged') {
|
|
212
|
+
console.log(chalk.gray(` ${AGENTS[plan.agent].cliCommand} reads this config directly.`));
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
package/dist/commands/feed.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `agents feed` --
|
|
2
|
+
* `agents feed` -- operator inbox + agent status posts.
|
|
3
3
|
*
|
|
4
|
+
* Default (`agents feed`): list open blocks (decisions agents are waiting on).
|
|
4
5
|
* Aggregates block records from the local feed store and, with --host, from
|
|
5
6
|
* reachable remote hosts via SSH passthrough. Each block carries enough
|
|
6
7
|
* identity for `agents message` to route a reply back to the right agent.
|
|
@@ -8,6 +9,11 @@
|
|
|
8
9
|
* Default view groups by **outcome** (ticket / PR / worktree / Unassigned) so
|
|
9
10
|
* an operator sees dozens of deliverables, not ~1,100 agents. Pass `--flat`
|
|
10
11
|
* for the legacy per-agent list.
|
|
12
|
+
*
|
|
13
|
+
* `agents feed post`: agent-callable free-text progress into the activity
|
|
14
|
+
* stream (milestone `status.posted`). Session/agent/host/runtime/pid identity
|
|
15
|
+
* is auto-stamped from env + the pid registry — no domain-specific flags.
|
|
16
|
+
* Humans watch via the feed activity lane / `agents activity`.
|
|
11
17
|
*/
|
|
12
18
|
import type { Command } from 'commander';
|
|
13
19
|
import { type OpenBlock } from '../lib/feed.js';
|
package/dist/commands/feed.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { ensureFeedPublishHook, listAskStats, listBlocks, recordNotified } from '../lib/feed.js';
|
|
3
3
|
import { ensureActivityLogHook, readRecentActivity, formatActivityLine } from '../lib/activity.js';
|
|
4
|
+
import { postFeedStatus } from '../lib/feed-post.js';
|
|
4
5
|
import { enrichBlocksFromSessions, groupBlocksByOutcome, isUnambiguousOutcomeAnswer, openBlocksForOutcome, stampBlockOutcomes, } from '../lib/feed-outcome.js';
|
|
5
6
|
import { classifyBlock, filterBlocksForFeed, suppressionDigest, } from '../lib/ask-classifier.js';
|
|
6
7
|
import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
@@ -214,9 +215,9 @@ export function sessionHintsFromActive(sessions) {
|
|
|
214
215
|
}));
|
|
215
216
|
}
|
|
216
217
|
export function registerFeedCommand(program) {
|
|
217
|
-
program
|
|
218
|
+
const feed = program
|
|
218
219
|
.command('feed')
|
|
219
|
-
.description('
|
|
220
|
+
.description('Open blocks (needs you) + agent status posts (feed post)')
|
|
220
221
|
.option('--json', 'Output as JSON (each block stamped with its outcome + ask class)')
|
|
221
222
|
.option('--flat', 'List one block per agent instead of grouping by outcome')
|
|
222
223
|
.option('--all', 'Include stalls/FYIs that policy would suppress (default: hide them)')
|
|
@@ -225,8 +226,50 @@ export function registerFeedCommand(program) {
|
|
|
225
226
|
.option('--device <target...>', 'Alias for --host; repeatable')
|
|
226
227
|
.option('--dispatch', 'Run stall suppression + default-on-no-answer policy and urgent notifications')
|
|
227
228
|
.option('--pause <id>', 'Pause a runaway/needy local process (SIGSTOP) or cancel a cloud task')
|
|
228
|
-
.option('--kill <id>', 'Kill a runaway/needy local process (SIGTERM) or cancel a cloud task')
|
|
229
|
-
|
|
229
|
+
.option('--kill <id>', 'Kill a runaway/needy local process (SIGTERM) or cancel a cloud task');
|
|
230
|
+
feed
|
|
231
|
+
.command('post')
|
|
232
|
+
.description('Post a status update to the fleet activity stream (for agents)')
|
|
233
|
+
.argument('<text...>', 'What just happened — one short human line')
|
|
234
|
+
.option('--session <id>', 'Session id escape hatch (default: auto from env / pid registry)')
|
|
235
|
+
.option('--json', 'Emit the written event as JSON')
|
|
236
|
+
.addHelpText('after', `
|
|
237
|
+
Examples:
|
|
238
|
+
# Inside an agents-cli run (session identity is already in the env):
|
|
239
|
+
agents feed post "CHANGELOG pushed; watching CI and mac-mini E2E"
|
|
240
|
+
agents feed post "ready for review" --json
|
|
241
|
+
|
|
242
|
+
# Outside a run, pass the session explicitly:
|
|
243
|
+
agents feed post "manual note" --session 00998b0e-2d15-4d2f-a58b-974a886c9b47
|
|
244
|
+
|
|
245
|
+
Identity (session, agent, host, runtime, pid, launchId) is stamped automatically.
|
|
246
|
+
Domain facts (tickets, PRs) are not CLI flags — join them on the session at read time.
|
|
247
|
+
`)
|
|
248
|
+
.action((textParts, opts, cmd) => {
|
|
249
|
+
// Parent `feed` also declares `--json` (for the list view). Commander
|
|
250
|
+
// binds the flag on the parent, so a `feed post … --json` lands on
|
|
251
|
+
// parent.opts().json — not the child. Read both.
|
|
252
|
+
const flags = {
|
|
253
|
+
session: opts?.session ?? cmd?.opts?.()?.session,
|
|
254
|
+
json: Boolean(opts?.json ?? cmd?.opts?.()?.json ?? cmd?.parent?.opts?.()?.json),
|
|
255
|
+
};
|
|
256
|
+
try {
|
|
257
|
+
const { event } = postFeedStatus({
|
|
258
|
+
text: Array.isArray(textParts) ? textParts.join(' ') : String(textParts ?? ''),
|
|
259
|
+
sessionId: flags.session,
|
|
260
|
+
});
|
|
261
|
+
if (flags.json) {
|
|
262
|
+
console.log(JSON.stringify(event, null, 2));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
console.log(formatActivityLine(event, { showHost: true }).trimStart());
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
console.error(chalk.red(err.message));
|
|
269
|
+
process.exitCode = 1;
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
feed.action(async (opts) => {
|
|
230
273
|
if (opts.device?.length)
|
|
231
274
|
opts.host = [...(opts.host ?? []), ...opts.device];
|
|
232
275
|
const self = machineId();
|
package/dist/commands/go.d.ts
CHANGED
|
@@ -17,8 +17,15 @@ import type { Command } from 'commander';
|
|
|
17
17
|
import { type ActiveSession } from '../lib/session/active.js';
|
|
18
18
|
import type { SessionMeta } from '../lib/session/types.js';
|
|
19
19
|
export declare function registerGoCommand(program: Command): void;
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Live jump targets (local + remote), keyed by session id. Cloud is excluded by
|
|
22
|
+
* default (it has no local pid to attach), but `detach` opts in with
|
|
23
|
+
* `includeCloud` so it can resolve a cloud id and refuse it with a clear message
|
|
24
|
+
* instead of a bare "no live session".
|
|
25
|
+
*/
|
|
26
|
+
export declare function gatherLiveTargets(local: boolean, opts?: {
|
|
27
|
+
includeCloud?: boolean;
|
|
28
|
+
}): Promise<{
|
|
22
29
|
self: string;
|
|
23
30
|
activeById: Map<string, ActiveSession>;
|
|
24
31
|
}>;
|
package/dist/commands/go.js
CHANGED
|
@@ -20,6 +20,7 @@ import { promisify } from 'util';
|
|
|
20
20
|
import { getActiveSessions, findSessionFileForKind } from '../lib/session/active.js';
|
|
21
21
|
import { gatherRemoteActive } from '../lib/session/remote-active.js';
|
|
22
22
|
import { discoverSessions } from '../lib/session/discover.js';
|
|
23
|
+
import { deriveShortId } from '../lib/session/short-id.js';
|
|
23
24
|
import { dedupeByMachineSession, mergeLocalFirst, pickSessionInteractive } from './sessions.js';
|
|
24
25
|
import { focusAction } from './focus.js';
|
|
25
26
|
import { machineId } from '../lib/session/sync/config.js';
|
|
@@ -39,8 +40,13 @@ export function registerGoCommand(program) {
|
|
|
39
40
|
await focusAction(id, { local: opts.local, attachOnly: true });
|
|
40
41
|
});
|
|
41
42
|
}
|
|
42
|
-
/**
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Live jump targets (local + remote), keyed by session id. Cloud is excluded by
|
|
45
|
+
* default (it has no local pid to attach), but `detach` opts in with
|
|
46
|
+
* `includeCloud` so it can resolve a cloud id and refuse it with a clear message
|
|
47
|
+
* instead of a bare "no live session".
|
|
48
|
+
*/
|
|
49
|
+
export async function gatherLiveTargets(local, opts = {}) {
|
|
44
50
|
const self = machineId();
|
|
45
51
|
const localActive = await getActiveSessions();
|
|
46
52
|
for (const s of localActive)
|
|
@@ -55,9 +61,13 @@ export async function gatherLiveTargets(local) {
|
|
|
55
61
|
catch { /* remote sweep is best-effort */ }
|
|
56
62
|
}
|
|
57
63
|
const activeById = new Map();
|
|
58
|
-
for (const s of active)
|
|
59
|
-
if (s.
|
|
60
|
-
|
|
64
|
+
for (const s of active) {
|
|
65
|
+
if (!s.sessionId)
|
|
66
|
+
continue;
|
|
67
|
+
if (s.context === 'cloud' && !opts.includeCloud)
|
|
68
|
+
continue;
|
|
69
|
+
activeById.set(s.sessionId, s);
|
|
70
|
+
}
|
|
61
71
|
return { self, activeById };
|
|
62
72
|
}
|
|
63
73
|
/** Interactive pick over the live sessions' rich SessionMeta; returns the chosen live session. */
|
|
@@ -99,7 +109,7 @@ function synthMeta(s, self) {
|
|
|
99
109
|
const filePath = remote ? '' : (findSessionFileForKind(s.kind, s.cwd, s.sessionId) ?? '');
|
|
100
110
|
return {
|
|
101
111
|
id: s.sessionId,
|
|
102
|
-
shortId: s.sessionId
|
|
112
|
+
shortId: deriveShortId(s.sessionId),
|
|
103
113
|
agent: s.kind,
|
|
104
114
|
timestamp: new Date(s.startedAtMs ?? Date.now()).toISOString(),
|
|
105
115
|
filePath,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom harness commands (`agents harness`).
|
|
3
|
+
*
|
|
4
|
+
* A "custom harness" is a named (host CLI + model) combo — e.g. OpenCode pinned
|
|
5
|
+
* to meta/muse-spark-1.1, called `spark`. It runs like a native agent type
|
|
6
|
+
* (`agents run spark`) and syncs across devices via `agents repo push user`.
|
|
7
|
+
*
|
|
8
|
+
* Mechanism: harnesses ARE profiles (same ~/.agents/profiles/*.yml, same run
|
|
9
|
+
* resolution). This command group is the harness-flavored surface over that
|
|
10
|
+
* layer + a discovery `list` spanning custom harnesses, addable presets, and the
|
|
11
|
+
* native harness registry. The `agents profiles` tree stays unchanged.
|
|
12
|
+
*/
|
|
13
|
+
import type { Command } from 'commander';
|
|
14
|
+
export declare function registerHarnessCommands(program: Command): void;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom harness commands (`agents harness`).
|
|
3
|
+
*
|
|
4
|
+
* A "custom harness" is a named (host CLI + model) combo — e.g. OpenCode pinned
|
|
5
|
+
* to meta/muse-spark-1.1, called `spark`. It runs like a native agent type
|
|
6
|
+
* (`agents run spark`) and syncs across devices via `agents repo push user`.
|
|
7
|
+
*
|
|
8
|
+
* Mechanism: harnesses ARE profiles (same ~/.agents/profiles/*.yml, same run
|
|
9
|
+
* resolution). This command group is the harness-flavored surface over that
|
|
10
|
+
* layer + a discovery `list` spanning custom harnesses, addable presets, and the
|
|
11
|
+
* native harness registry. The `agents profiles` tree stays unchanged.
|
|
12
|
+
*/
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { addProfile } from './profiles.js';
|
|
15
|
+
import { listProfiles, readProfile, deleteProfile, profileHostLabel, profileProviderLabel, profileModelLabel, profileAuthLabel, getProfilePath, } from '../lib/profiles.js';
|
|
16
|
+
import { listPresets } from '../lib/profiles-presets.js';
|
|
17
|
+
import { AGENTS, ALL_AGENT_IDS } from '../lib/agents.js';
|
|
18
|
+
/** Short capability summary for a native harness — its supported run modes. */
|
|
19
|
+
function nativeModes(id) {
|
|
20
|
+
const modes = AGENTS[id]?.capabilities?.modes ?? [];
|
|
21
|
+
return modes.length ? modes.join('/') : '-';
|
|
22
|
+
}
|
|
23
|
+
export function registerHarnessCommands(program) {
|
|
24
|
+
const cmd = program
|
|
25
|
+
.command('harness')
|
|
26
|
+
.alias('harnesses')
|
|
27
|
+
.description('Custom harnesses — name a (host CLI + model) combo and run it like a native agent type.')
|
|
28
|
+
.addHelpText('after', `
|
|
29
|
+
A custom harness pins a host CLI (opencode, claude, codex, grok, gemini, ...) to a
|
|
30
|
+
model and gives it a name. 'agents run <name>' then behaves like a native agent
|
|
31
|
+
type, and 'agents repo push user' syncs it to every device.
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
# Meta Muse Spark 1.1 through OpenCode, called 'spark'
|
|
35
|
+
agents harness add spark --host opencode --model meta/muse-spark-1.1
|
|
36
|
+
agents run spark "refactor api/handlers/checkout.py"
|
|
37
|
+
|
|
38
|
+
# Per-run model override still wins
|
|
39
|
+
agents run spark --model opencode/big-pickle "quick pass"
|
|
40
|
+
|
|
41
|
+
# See custom harnesses, addable presets, and native harnesses
|
|
42
|
+
agents harness list
|
|
43
|
+
|
|
44
|
+
# Private OpenAI/Anthropic-compatible endpoint (stores a keychain key)
|
|
45
|
+
agents harness add corp --host claude --model gpt-x --base-url https://gw.corp/v1 --auth-provider corp
|
|
46
|
+
|
|
47
|
+
# Custom harnesses are just profiles — this also works via 'agents profiles'
|
|
48
|
+
`);
|
|
49
|
+
cmd
|
|
50
|
+
.command('add <name>')
|
|
51
|
+
.description('Create a custom harness from a host + model (or apply a built-in preset).')
|
|
52
|
+
.option('--host <agent>', 'Host CLI to run under (opencode, claude, codex, grok, gemini, ...) — pair with --model')
|
|
53
|
+
.option('--model <id>', 'Model id to pin on the host (e.g., meta/muse-spark-1.1) — pair with --host')
|
|
54
|
+
.option('--base-url <url>', 'Custom endpoint base URL (claude/codex hosts)')
|
|
55
|
+
.option('--auth-provider <provider>', 'Attach a keychain-backed API key under this provider (for private endpoints)')
|
|
56
|
+
.option('--preset <preset>', 'Apply a built-in preset instead of --host/--model')
|
|
57
|
+
.option('--version <version>', 'Pin the host CLI version (e.g., 1.16.0)')
|
|
58
|
+
.option('--key-stdin', 'Read API key from stdin instead of prompting (for scripts/CI)')
|
|
59
|
+
.option('--force', 'Overwrite an existing harness with the same name')
|
|
60
|
+
.action(async (name, opts) => {
|
|
61
|
+
try {
|
|
62
|
+
await addProfile(name, opts, 'Harness');
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
console.error(chalk.red(err.message));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
cmd
|
|
70
|
+
.command('list')
|
|
71
|
+
.alias('ls')
|
|
72
|
+
.description('List custom harnesses, addable presets, and native harnesses.')
|
|
73
|
+
.option('--json', 'Emit JSON instead of a table')
|
|
74
|
+
.action((opts) => {
|
|
75
|
+
const custom = listProfiles();
|
|
76
|
+
const presets = listPresets();
|
|
77
|
+
const native = ALL_AGENT_IDS.map((id) => ({ id, name: AGENTS[id].name, modes: nativeModes(id) }));
|
|
78
|
+
if (opts.json) {
|
|
79
|
+
console.log(JSON.stringify({
|
|
80
|
+
custom: custom.map((p) => ({
|
|
81
|
+
name: p.name,
|
|
82
|
+
host: p.host.agent,
|
|
83
|
+
model: profileModelLabel(p),
|
|
84
|
+
provider: profileProviderLabel(p),
|
|
85
|
+
})),
|
|
86
|
+
presets: presets.map((p) => ({ name: p.name, provider: p.provider, description: p.description })),
|
|
87
|
+
native,
|
|
88
|
+
}, null, 2));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
console.log(chalk.bold('Custom harnesses') + chalk.gray(' (yours — agents run <name>)'));
|
|
92
|
+
if (custom.length === 0) {
|
|
93
|
+
console.log(chalk.gray(' none yet — try: agents harness add spark --host opencode --model meta/muse-spark-1.1'));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
for (const p of custom) {
|
|
97
|
+
console.log(` ${chalk.cyan(p.name.padEnd(16))} ${profileHostLabel(p).padEnd(14)} ${chalk.gray(profileModelLabel(p))}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(chalk.bold('Presets') + chalk.gray(' (addable — agents harness add <name>)'));
|
|
102
|
+
for (const p of presets) {
|
|
103
|
+
console.log(` ${chalk.cyan(p.name.padEnd(16))} ${p.provider.padEnd(12)} ${chalk.gray(p.description.slice(0, 70))}`);
|
|
104
|
+
}
|
|
105
|
+
console.log('');
|
|
106
|
+
console.log(chalk.bold('Native harnesses') + chalk.gray(' (built-in — agents run <id>)'));
|
|
107
|
+
for (const n of native) {
|
|
108
|
+
console.log(` ${chalk.cyan(n.id.padEnd(16))} ${n.name.padEnd(14)} ${chalk.gray('modes: ' + n.modes)}`);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
cmd
|
|
112
|
+
.command('view <name>')
|
|
113
|
+
.alias('show')
|
|
114
|
+
.description('Show one custom harness (host, model, provider, auth, path).')
|
|
115
|
+
.action((name) => {
|
|
116
|
+
try {
|
|
117
|
+
const p = readProfile(name);
|
|
118
|
+
console.log(chalk.bold(p.name));
|
|
119
|
+
if (p.description)
|
|
120
|
+
console.log(chalk.gray(p.description));
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(`Host: ${profileHostLabel(p)}`);
|
|
123
|
+
console.log(`Model: ${profileModelLabel(p)}`);
|
|
124
|
+
console.log(`Provider: ${profileProviderLabel(p)}`);
|
|
125
|
+
console.log(`Auth: ${profileAuthLabel(p)}`);
|
|
126
|
+
console.log(chalk.gray(getProfilePath(p.name)));
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
console.error(chalk.red(err.message));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
cmd
|
|
134
|
+
.command('remove <name>')
|
|
135
|
+
.alias('rm')
|
|
136
|
+
.description('Delete a custom harness (keychain token is kept).')
|
|
137
|
+
.action((name) => {
|
|
138
|
+
const existed = deleteProfile(name);
|
|
139
|
+
if (!existed) {
|
|
140
|
+
console.error(chalk.red(`Harness '${name}' not found.`));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
console.log(chalk.green(`Harness '${name}' removed.`));
|
|
144
|
+
});
|
|
145
|
+
}
|