@phnx-labs/agents-cli 1.19.2 → 1.20.3
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 +140 -0
- package/README.md +72 -12
- package/dist/browser.js +0 -0
- package/dist/commands/browser.js +88 -16
- package/dist/commands/cli.d.ts +14 -0
- package/dist/commands/cli.js +244 -0
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +27 -10
- package/dist/commands/computer.js +18 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/exec.js +38 -18
- package/dist/commands/factory.d.ts +3 -14
- package/dist/commands/factory.js +3 -3
- package/dist/commands/feedback.d.ts +7 -0
- package/dist/commands/feedback.js +89 -0
- package/dist/commands/helper.d.ts +12 -0
- package/dist/commands/helper.js +87 -0
- package/dist/commands/hooks.js +89 -10
- package/dist/commands/mcp.js +166 -10
- package/dist/commands/packages.js +196 -27
- package/dist/commands/permissions.js +21 -6
- package/dist/commands/plugins.js +11 -4
- package/dist/commands/profiles.d.ts +8 -0
- package/dist/commands/profiles.js +118 -5
- package/dist/commands/prune.js +39 -160
- package/dist/commands/pull.js +58 -5
- package/dist/commands/routines.js +107 -14
- package/dist/commands/rules.js +8 -4
- package/dist/commands/secrets-migrate.d.ts +24 -0
- package/dist/commands/secrets-migrate.js +198 -0
- package/dist/commands/secrets-sync.d.ts +11 -0
- package/dist/commands/secrets-sync.js +155 -0
- package/dist/commands/secrets.js +79 -46
- package/dist/commands/sessions.d.ts +28 -0
- package/dist/commands/sessions.js +98 -33
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +37 -28
- package/dist/commands/skills.js +25 -8
- package/dist/commands/subagents.js +69 -49
- package/dist/commands/teams.js +61 -10
- package/dist/commands/utils.d.ts +33 -0
- package/dist/commands/utils.js +139 -0
- package/dist/commands/versions.d.ts +4 -3
- package/dist/commands/versions.js +134 -130
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +175 -19
- package/dist/commands/workflows.js +29 -6
- package/dist/computer.js +0 -0
- package/dist/index.js +38 -6
- package/dist/lib/acp/client.js +6 -1
- package/dist/lib/acp/harnesses.js +8 -0
- package/dist/lib/agents.d.ts +4 -0
- package/dist/lib/agents.js +125 -34
- package/dist/lib/auto-pull-worker.js +18 -1
- package/dist/lib/browser/cdp.d.ts +8 -1
- package/dist/lib/browser/cdp.js +40 -3
- package/dist/lib/browser/chrome.d.ts +13 -0
- package/dist/lib/browser/chrome.js +46 -3
- package/dist/lib/browser/domain-skills.d.ts +51 -0
- package/dist/lib/browser/domain-skills.js +157 -0
- package/dist/lib/browser/drivers/local.js +45 -4
- package/dist/lib/browser/drivers/ssh.js +2 -2
- package/dist/lib/browser/ipc.d.ts +8 -1
- package/dist/lib/browser/ipc.js +37 -28
- package/dist/lib/browser/profiles.d.ts +16 -3
- package/dist/lib/browser/profiles.js +44 -4
- package/dist/lib/browser/service.d.ts +3 -0
- package/dist/lib/browser/service.js +40 -5
- package/dist/lib/browser/types.d.ts +11 -4
- package/dist/lib/cli-resources.d.ts +137 -0
- package/dist/lib/cli-resources.js +477 -0
- package/dist/lib/cloud/factory.d.ts +1 -1
- package/dist/lib/cloud/factory.js +1 -1
- package/dist/lib/cloud/rush.js +5 -5
- package/dist/lib/command-skills.js +0 -2
- package/dist/lib/computer-rpc.d.ts +3 -0
- package/dist/lib/computer-rpc.js +53 -0
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/events.d.ts +16 -2
- package/dist/lib/events.js +33 -2
- package/dist/lib/exec.d.ts +42 -13
- package/dist/lib/exec.js +127 -33
- package/dist/lib/help.js +11 -5
- package/dist/lib/hooks/cache.d.ts +38 -0
- package/dist/lib/hooks/cache.js +242 -0
- package/dist/lib/hooks/profile.d.ts +33 -0
- package/dist/lib/hooks/profile.js +129 -0
- package/dist/lib/hooks.d.ts +0 -10
- package/dist/lib/hooks.js +246 -11
- package/dist/lib/mcp.d.ts +15 -0
- package/dist/lib/mcp.js +46 -0
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/overdue.d.ts +26 -0
- package/dist/lib/overdue.js +101 -0
- package/dist/lib/permissions.d.ts +13 -0
- package/dist/lib/permissions.js +55 -1
- package/dist/lib/plugin-marketplace.js +1 -1
- package/dist/lib/plugins.js +15 -1
- package/dist/lib/profiles-presets.d.ts +26 -0
- package/dist/lib/profiles-presets.js +216 -0
- package/dist/lib/profiles.d.ts +34 -0
- package/dist/lib/profiles.js +112 -1
- package/dist/lib/resources/mcp.js +37 -0
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/rotate.js +10 -4
- package/dist/lib/routines-format.d.ts +47 -0
- package/dist/lib/routines-format.js +194 -0
- package/dist/lib/routines.d.ts +8 -2
- package/dist/lib/routines.js +34 -14
- package/dist/lib/runner.js +83 -15
- package/dist/lib/scheduler.js +8 -1
- 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/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
- package/dist/lib/secrets/bundles.d.ts +34 -17
- package/dist/lib/secrets/bundles.js +210 -36
- package/dist/lib/secrets/index.d.ts +49 -30
- package/dist/lib/secrets/index.js +126 -115
- package/dist/lib/secrets/install-helper.d.ts +45 -0
- package/dist/lib/secrets/install-helper.js +165 -0
- package/dist/lib/secrets/linux.js +4 -4
- package/dist/lib/secrets/sync.d.ts +56 -0
- package/dist/lib/secrets/sync.js +180 -0
- package/dist/lib/session/active.d.ts +8 -0
- package/dist/lib/session/active.js +3 -2
- package/dist/lib/session/db.d.ts +0 -4
- package/dist/lib/session/db.js +0 -26
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +44 -0
- package/dist/lib/session/render.js +4 -4
- package/dist/lib/session/types.d.ts +2 -2
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +5 -2
- package/dist/lib/shims.js +70 -38
- package/dist/lib/state.d.ts +14 -2
- package/dist/lib/state.js +51 -20
- package/dist/lib/teams/agents.d.ts +5 -4
- package/dist/lib/teams/agents.js +48 -22
- package/dist/lib/teams/api.d.ts +2 -1
- package/dist/lib/teams/api.js +4 -3
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +153 -3
- package/dist/lib/teams/summarizer.js +18 -2
- package/dist/lib/teams/worktree.js +14 -3
- package/dist/lib/types.d.ts +63 -4
- package/dist/lib/types.js +8 -3
- package/dist/lib/usage.d.ts +27 -2
- package/dist/lib/usage.js +100 -17
- package/dist/lib/versions.d.ts +45 -3
- package/dist/lib/versions.js +455 -60
- package/package.json +15 -14
- package/scripts/install-helper.js +97 -0
- package/scripts/postinstall.js +16 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
- package/npm-shrinkwrap.json +0 -3162
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** A single install method. Exactly one of the keys (npm/brew/script/binary) is set. */
|
|
2
|
+
export type InstallMethod = {
|
|
3
|
+
npm: string;
|
|
4
|
+
} | {
|
|
5
|
+
brew: string;
|
|
6
|
+
} | {
|
|
7
|
+
script: string;
|
|
8
|
+
} | {
|
|
9
|
+
binary: BinarySpec;
|
|
10
|
+
};
|
|
11
|
+
/** Per-platform binary download spec. Keys are `<os>-<arch>` (e.g. darwin-arm64). */
|
|
12
|
+
export interface BinarySpec {
|
|
13
|
+
[platform: string]: {
|
|
14
|
+
url: string;
|
|
15
|
+
/** Path inside the archive (relative). Required when url is a .tar.gz/.zip. */
|
|
16
|
+
extract?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* How to verify a CLI is installed. Structured so we can dispatch to spawnSync
|
|
21
|
+
* with an argv array — never through a shell.
|
|
22
|
+
*
|
|
23
|
+
* `which` — just check PATH for `cmd`.
|
|
24
|
+
* `version` — spawn `cmd` with `args` and require exit 0.
|
|
25
|
+
*/
|
|
26
|
+
export type CheckSpec = {
|
|
27
|
+
kind: 'which';
|
|
28
|
+
cmd: string;
|
|
29
|
+
} | {
|
|
30
|
+
kind: 'version';
|
|
31
|
+
cmd: string;
|
|
32
|
+
args: string[];
|
|
33
|
+
};
|
|
34
|
+
/** Parsed CLI manifest. */
|
|
35
|
+
export interface CliManifest {
|
|
36
|
+
/** Name as it appears on the command line (e.g. "higgsfield"). */
|
|
37
|
+
name: string;
|
|
38
|
+
/** One-line summary shown in `agents cli list`. */
|
|
39
|
+
description?: string;
|
|
40
|
+
/** Project homepage; used in detail view + post-install messaging. */
|
|
41
|
+
homepage?: string;
|
|
42
|
+
/** Structured check spec; never a raw shell command. */
|
|
43
|
+
check: CheckSpec;
|
|
44
|
+
/** Install methods tried in order; first one whose tool is available is used. */
|
|
45
|
+
install: InstallMethod[];
|
|
46
|
+
/** Message printed after successful install — typically auth instructions. */
|
|
47
|
+
postInstall?: string;
|
|
48
|
+
/** Origin layer this manifest was resolved from. */
|
|
49
|
+
source: string;
|
|
50
|
+
/** Absolute path to the yaml file. */
|
|
51
|
+
path: string;
|
|
52
|
+
}
|
|
53
|
+
/** A validation problem in a CLI manifest. */
|
|
54
|
+
export interface CliManifestError {
|
|
55
|
+
/** Filename that failed to parse. */
|
|
56
|
+
file: string;
|
|
57
|
+
/** Human-readable reason. */
|
|
58
|
+
reason: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Parse a `check:` field into a CheckSpec. Accepts either a structured object
|
|
62
|
+
* (`{ kind: 'which'|'version', cmd, args? }`) or a legacy whitespace-separated
|
|
63
|
+
* string. String form is split on whitespace and each token is validated against
|
|
64
|
+
* SAFE_CHECK_TOKEN — manifests cannot smuggle in shell metacharacters.
|
|
65
|
+
*/
|
|
66
|
+
export declare function parseCheckSpec(raw: unknown, defaultName: string): CheckSpec;
|
|
67
|
+
/**
|
|
68
|
+
* Parse a single CLI manifest from its YAML contents.
|
|
69
|
+
* Returns a manifest on success; throws on schema violations so callers can
|
|
70
|
+
* decide whether to surface or swallow the error per file.
|
|
71
|
+
*/
|
|
72
|
+
export declare function parseCliManifest(contents: string, opts: {
|
|
73
|
+
name: string;
|
|
74
|
+
source: string;
|
|
75
|
+
path: string;
|
|
76
|
+
}): CliManifest;
|
|
77
|
+
/**
|
|
78
|
+
* Discover all CLI manifests resolvable from the current cwd. Returns valid
|
|
79
|
+
* manifests and any parse errors separately so the CLI can show both.
|
|
80
|
+
*/
|
|
81
|
+
export declare function listCliManifests(cwd?: string): {
|
|
82
|
+
manifests: CliManifest[];
|
|
83
|
+
errors: CliManifestError[];
|
|
84
|
+
};
|
|
85
|
+
/** Resolve a single CLI manifest by name. Returns null when not declared. */
|
|
86
|
+
export declare function resolveCliManifest(name: string, cwd?: string): CliManifest | null;
|
|
87
|
+
export declare function hasCommand(cmd: string): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Run the manifest's check. Dispatches on CheckSpec.kind — never invokes a
|
|
90
|
+
* shell, never interpolates strings into a command line.
|
|
91
|
+
*/
|
|
92
|
+
export declare function isCliInstalled(manifest: CliManifest): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Pick the first install method whose required host tool is available.
|
|
95
|
+
* Returns null when none of the declared methods can run on this host.
|
|
96
|
+
*/
|
|
97
|
+
export declare function selectInstallMethod(manifest: CliManifest): InstallMethod | null;
|
|
98
|
+
/** Render a CheckSpec back to a human-readable command string (display only). */
|
|
99
|
+
export declare function describeCheck(check: CheckSpec): string;
|
|
100
|
+
/** Short description of a method for display. */
|
|
101
|
+
export declare function describeMethod(method: InstallMethod): string;
|
|
102
|
+
export interface InstallResult {
|
|
103
|
+
manifest: CliManifest;
|
|
104
|
+
/** Method that was attempted (null if no compatible method existed). */
|
|
105
|
+
method: InstallMethod | null;
|
|
106
|
+
/** True when the post-install `check` passed. */
|
|
107
|
+
installed: boolean;
|
|
108
|
+
/** stdout/stderr captured from the install command, for surfacing on failure. */
|
|
109
|
+
output?: string;
|
|
110
|
+
/** Set when the install runner threw or exited non-zero. */
|
|
111
|
+
error?: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Display-only rendering of how a method would be run, for `--dry-run` and
|
|
115
|
+
* status output. Not used by installCli — execution goes through runInstallMethod
|
|
116
|
+
* which dispatches to spawnSync with argv arrays.
|
|
117
|
+
*/
|
|
118
|
+
export declare function buildInstallCommand(method: InstallMethod): string;
|
|
119
|
+
/**
|
|
120
|
+
* Install a single CLI by running its first compatible method. Streams the
|
|
121
|
+
* underlying command's output to the parent terminal so users see brew/npm
|
|
122
|
+
* progress live. Verifies success by re-running `check`.
|
|
123
|
+
*/
|
|
124
|
+
export declare function installCli(manifest: CliManifest, opts?: {
|
|
125
|
+
dryRun?: boolean;
|
|
126
|
+
}): InstallResult;
|
|
127
|
+
export interface CliStatus {
|
|
128
|
+
manifest: CliManifest;
|
|
129
|
+
installed: boolean;
|
|
130
|
+
}
|
|
131
|
+
/** Convenience: list all manifests + their installed-on-host status. */
|
|
132
|
+
export declare function listCliStatus(cwd?: string): {
|
|
133
|
+
statuses: CliStatus[];
|
|
134
|
+
errors: CliManifestError[];
|
|
135
|
+
};
|
|
136
|
+
/** Names of CLIs that are declared but not currently installed on the host. */
|
|
137
|
+
export declare function getMissingClis(cwd?: string): CliManifest[];
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI tool resources — declarative manifests for command-line binaries the user
|
|
3
|
+
* wants installed on the host (e.g. higgsfield, gh, glab).
|
|
4
|
+
*
|
|
5
|
+
* A CLI resource is a YAML file under <repo>/cli/<name>.yaml. Resolution follows
|
|
6
|
+
* the same project > user > system > extra-repo precedence as other resources,
|
|
7
|
+
* but unlike skills/commands/hooks, CLI resources are NOT copied into per-agent
|
|
8
|
+
* version homes — they install binaries onto the host PATH. The relationship is
|
|
9
|
+
* "Brewfile-style": declare once in ~/.agents/cli/, install on any new machine.
|
|
10
|
+
*
|
|
11
|
+
* Security: every field that becomes a child-process argument is validated
|
|
12
|
+
* against a strict allowlist and dispatched via spawnSync with an argv array.
|
|
13
|
+
* Nothing here ever runs through a shell — manifests can come from project repos
|
|
14
|
+
* or pulled extras, so anything that would let a manifest author smuggle in
|
|
15
|
+
* `;`, `$(...)`, backticks, redirects, or pipe operators is a remote-code-
|
|
16
|
+
* execution sink.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as os from 'os';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import { spawnSync } from 'child_process';
|
|
22
|
+
import * as yaml from 'yaml';
|
|
23
|
+
import { listResources, resolveResource } from './resources.js';
|
|
24
|
+
// ─── Validation primitives ───────────────────────────────────────────────────
|
|
25
|
+
/** Token allowed inside `check:` strings — letters, digits, underscore, dot, slash, dash. */
|
|
26
|
+
const SAFE_CHECK_TOKEN = /^[a-zA-Z0-9_./-]+$/;
|
|
27
|
+
/** npm package name with optional scope and optional version/tag. */
|
|
28
|
+
const NPM_PACKAGE = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(@[a-zA-Z0-9._-]+)?$/;
|
|
29
|
+
/** Homebrew formula name (and optional tap prefix). */
|
|
30
|
+
const BREW_FORMULA = /^([a-z0-9][a-z0-9_.-]*\/[a-z0-9][a-z0-9_.-]*\/)?[a-z0-9][a-z0-9_.+-]*$/;
|
|
31
|
+
/** Path segment inside a tarball — no leading slash, no `..`, no shell metas. */
|
|
32
|
+
const SAFE_PATH_SEGMENT = /^[a-zA-Z0-9_./-]+$/;
|
|
33
|
+
function assertSafeCheckToken(tok) {
|
|
34
|
+
if (!SAFE_CHECK_TOKEN.test(tok)) {
|
|
35
|
+
throw new Error(`check contains unsafe token: ${JSON.stringify(tok)}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function assertNpmPackage(name) {
|
|
39
|
+
if (!NPM_PACKAGE.test(name)) {
|
|
40
|
+
throw new Error(`npm package name is not allowlisted: ${JSON.stringify(name)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function assertBrewFormula(name) {
|
|
44
|
+
if (!BREW_FORMULA.test(name)) {
|
|
45
|
+
throw new Error(`brew formula name is not allowlisted: ${JSON.stringify(name)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function assertHttpsUrl(url) {
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = new URL(url);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error(`url is not parseable: ${JSON.stringify(url)}`);
|
|
55
|
+
}
|
|
56
|
+
if (parsed.protocol !== 'https:') {
|
|
57
|
+
throw new Error(`url must use https:// (got ${parsed.protocol}): ${JSON.stringify(url)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function assertSafePathSegment(seg) {
|
|
61
|
+
if (!SAFE_PATH_SEGMENT.test(seg) || seg.startsWith('/') || seg.split('/').includes('..')) {
|
|
62
|
+
throw new Error(`extract path is not allowlisted: ${JSON.stringify(seg)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ─── Parsing ─────────────────────────────────────────────────────────────────
|
|
66
|
+
/**
|
|
67
|
+
* Parse a `check:` field into a CheckSpec. Accepts either a structured object
|
|
68
|
+
* (`{ kind: 'which'|'version', cmd, args? }`) or a legacy whitespace-separated
|
|
69
|
+
* string. String form is split on whitespace and each token is validated against
|
|
70
|
+
* SAFE_CHECK_TOKEN — manifests cannot smuggle in shell metacharacters.
|
|
71
|
+
*/
|
|
72
|
+
export function parseCheckSpec(raw, defaultName) {
|
|
73
|
+
if (raw == null) {
|
|
74
|
+
assertSafeCheckToken(defaultName);
|
|
75
|
+
return { kind: 'version', cmd: defaultName, args: ['--version'] };
|
|
76
|
+
}
|
|
77
|
+
if (typeof raw === 'string') {
|
|
78
|
+
const tokens = raw.trim().split(/\s+/).filter((t) => t.length > 0);
|
|
79
|
+
if (tokens.length === 0) {
|
|
80
|
+
assertSafeCheckToken(defaultName);
|
|
81
|
+
return { kind: 'version', cmd: defaultName, args: ['--version'] };
|
|
82
|
+
}
|
|
83
|
+
for (const tok of tokens)
|
|
84
|
+
assertSafeCheckToken(tok);
|
|
85
|
+
const [cmd, ...args] = tokens;
|
|
86
|
+
return args.length === 0 ? { kind: 'which', cmd } : { kind: 'version', cmd, args };
|
|
87
|
+
}
|
|
88
|
+
if (typeof raw === 'object') {
|
|
89
|
+
const r = raw;
|
|
90
|
+
const kind = r.kind;
|
|
91
|
+
if (kind !== 'which' && kind !== 'version') {
|
|
92
|
+
throw new Error(`check.kind must be "which" or "version" (got ${JSON.stringify(kind)})`);
|
|
93
|
+
}
|
|
94
|
+
if (typeof r.cmd !== 'string' || !r.cmd.trim()) {
|
|
95
|
+
throw new Error('check.cmd must be a non-empty string');
|
|
96
|
+
}
|
|
97
|
+
const cmd = r.cmd.trim();
|
|
98
|
+
assertSafeCheckToken(cmd);
|
|
99
|
+
if (kind === 'which')
|
|
100
|
+
return { kind: 'which', cmd };
|
|
101
|
+
const args = Array.isArray(r.args) ? r.args : [];
|
|
102
|
+
const safeArgs = [];
|
|
103
|
+
for (const a of args) {
|
|
104
|
+
if (typeof a !== 'string')
|
|
105
|
+
throw new Error('check.args entries must be strings');
|
|
106
|
+
assertSafeCheckToken(a);
|
|
107
|
+
safeArgs.push(a);
|
|
108
|
+
}
|
|
109
|
+
return { kind: 'version', cmd, args: safeArgs };
|
|
110
|
+
}
|
|
111
|
+
throw new Error('check must be a string or an object with { kind, cmd, args? }');
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse a single CLI manifest from its YAML contents.
|
|
115
|
+
* Returns a manifest on success; throws on schema violations so callers can
|
|
116
|
+
* decide whether to surface or swallow the error per file.
|
|
117
|
+
*/
|
|
118
|
+
export function parseCliManifest(contents, opts) {
|
|
119
|
+
const raw = yaml.parse(contents);
|
|
120
|
+
if (!raw || typeof raw !== 'object') {
|
|
121
|
+
throw new Error('manifest must be a YAML object');
|
|
122
|
+
}
|
|
123
|
+
const name = typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : opts.name;
|
|
124
|
+
assertSafeCheckToken(name);
|
|
125
|
+
const description = typeof raw.description === 'string' ? raw.description : undefined;
|
|
126
|
+
const homepage = typeof raw.homepage === 'string' ? raw.homepage : undefined;
|
|
127
|
+
const check = parseCheckSpec(raw.check, name);
|
|
128
|
+
const postInstall = typeof raw.post_install === 'string' ? raw.post_install : undefined;
|
|
129
|
+
if (!Array.isArray(raw.install) || raw.install.length === 0) {
|
|
130
|
+
throw new Error('install must be a non-empty list of methods');
|
|
131
|
+
}
|
|
132
|
+
const install = raw.install.map((entry, i) => {
|
|
133
|
+
if (!entry || typeof entry !== 'object') {
|
|
134
|
+
throw new Error(`install[${i}] must be an object with one of: npm, brew, script, binary`);
|
|
135
|
+
}
|
|
136
|
+
const e = entry;
|
|
137
|
+
const keys = Object.keys(e).filter((k) => e[k] !== undefined && e[k] !== null);
|
|
138
|
+
if (keys.length !== 1) {
|
|
139
|
+
throw new Error(`install[${i}] must declare exactly one method (got: ${keys.join(', ') || 'none'})`);
|
|
140
|
+
}
|
|
141
|
+
const key = keys[0];
|
|
142
|
+
const value = e[key];
|
|
143
|
+
if (key === 'npm') {
|
|
144
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
145
|
+
throw new Error(`install[${i}].npm must be a non-empty string`);
|
|
146
|
+
}
|
|
147
|
+
const v = value.trim();
|
|
148
|
+
assertNpmPackage(v);
|
|
149
|
+
return { npm: v };
|
|
150
|
+
}
|
|
151
|
+
if (key === 'brew') {
|
|
152
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
153
|
+
throw new Error(`install[${i}].brew must be a non-empty string`);
|
|
154
|
+
}
|
|
155
|
+
const v = value.trim();
|
|
156
|
+
assertBrewFormula(v);
|
|
157
|
+
return { brew: v };
|
|
158
|
+
}
|
|
159
|
+
if (key === 'script') {
|
|
160
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
161
|
+
throw new Error(`install[${i}].script must be a non-empty string`);
|
|
162
|
+
}
|
|
163
|
+
const v = value.trim();
|
|
164
|
+
assertHttpsUrl(v);
|
|
165
|
+
return { script: v };
|
|
166
|
+
}
|
|
167
|
+
if (key === 'binary') {
|
|
168
|
+
if (!value || typeof value !== 'object') {
|
|
169
|
+
throw new Error(`install[${i}].binary must be a platform map`);
|
|
170
|
+
}
|
|
171
|
+
const binary = {};
|
|
172
|
+
for (const [platform, spec] of Object.entries(value)) {
|
|
173
|
+
if (!spec || typeof spec !== 'object') {
|
|
174
|
+
throw new Error(`install[${i}].binary.${platform} must be an object with a url`);
|
|
175
|
+
}
|
|
176
|
+
const s = spec;
|
|
177
|
+
if (typeof s.url !== 'string' || !s.url.trim()) {
|
|
178
|
+
throw new Error(`install[${i}].binary.${platform}.url must be a non-empty string`);
|
|
179
|
+
}
|
|
180
|
+
const url = s.url.trim();
|
|
181
|
+
assertHttpsUrl(url);
|
|
182
|
+
let extract;
|
|
183
|
+
if (typeof s.extract === 'string' && s.extract.length > 0) {
|
|
184
|
+
assertSafePathSegment(s.extract);
|
|
185
|
+
extract = s.extract;
|
|
186
|
+
}
|
|
187
|
+
binary[platform] = { url, extract };
|
|
188
|
+
}
|
|
189
|
+
return { binary };
|
|
190
|
+
}
|
|
191
|
+
throw new Error(`install[${i}] has unknown method "${key}" (expected: npm, brew, script, binary)`);
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
name,
|
|
195
|
+
description,
|
|
196
|
+
homepage,
|
|
197
|
+
check,
|
|
198
|
+
install,
|
|
199
|
+
postInstall,
|
|
200
|
+
source: opts.source,
|
|
201
|
+
path: opts.path,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Discover all CLI manifests resolvable from the current cwd. Returns valid
|
|
206
|
+
* manifests and any parse errors separately so the CLI can show both.
|
|
207
|
+
*/
|
|
208
|
+
export function listCliManifests(cwd) {
|
|
209
|
+
const resolved = listResources('cli', cwd);
|
|
210
|
+
const manifests = [];
|
|
211
|
+
const errors = [];
|
|
212
|
+
for (const entry of resolved) {
|
|
213
|
+
if (!entry.path.endsWith('.yaml') && !entry.path.endsWith('.yml'))
|
|
214
|
+
continue;
|
|
215
|
+
try {
|
|
216
|
+
const contents = fs.readFileSync(entry.path, 'utf-8');
|
|
217
|
+
const manifest = parseCliManifest(contents, {
|
|
218
|
+
name: entry.name,
|
|
219
|
+
source: entry.source,
|
|
220
|
+
path: entry.path,
|
|
221
|
+
});
|
|
222
|
+
manifests.push(manifest);
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
errors.push({ file: entry.path, reason: err.message });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return { manifests, errors };
|
|
229
|
+
}
|
|
230
|
+
/** Resolve a single CLI manifest by name. Returns null when not declared. */
|
|
231
|
+
export function resolveCliManifest(name, cwd) {
|
|
232
|
+
const resolved = resolveResource('cli', name, cwd);
|
|
233
|
+
if (!resolved)
|
|
234
|
+
return null;
|
|
235
|
+
if (!resolved.path.endsWith('.yaml') && !resolved.path.endsWith('.yml'))
|
|
236
|
+
return null;
|
|
237
|
+
const contents = fs.readFileSync(resolved.path, 'utf-8');
|
|
238
|
+
return parseCliManifest(contents, {
|
|
239
|
+
name: resolved.name,
|
|
240
|
+
source: resolved.source,
|
|
241
|
+
path: resolved.path,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
// ─── Host detection ──────────────────────────────────────────────────────────
|
|
245
|
+
/**
|
|
246
|
+
* Return true if a command resolves on the current PATH. Uses POSIX `command -v`
|
|
247
|
+
* via spawn argv (no shell); results are cached for the lifetime of the process.
|
|
248
|
+
*/
|
|
249
|
+
const cmdExistsCache = new Map();
|
|
250
|
+
export function hasCommand(cmd) {
|
|
251
|
+
if (cmdExistsCache.has(cmd))
|
|
252
|
+
return cmdExistsCache.get(cmd);
|
|
253
|
+
// `command` is a shell builtin on most POSIX shells; invoking `sh -c 'command -v X'`
|
|
254
|
+
// with X as an *argument* (not interpolated) is the safe path. `cmd` may be passed
|
|
255
|
+
// by callers that haven't validated it, so we route via argv to neutralize metas.
|
|
256
|
+
const result = spawnSync('sh', ['-c', 'command -v "$1" >/dev/null 2>&1', '_', cmd], {
|
|
257
|
+
stdio: 'ignore',
|
|
258
|
+
});
|
|
259
|
+
const ok = result.status === 0;
|
|
260
|
+
cmdExistsCache.set(cmd, ok);
|
|
261
|
+
return ok;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Run the manifest's check. Dispatches on CheckSpec.kind — never invokes a
|
|
265
|
+
* shell, never interpolates strings into a command line.
|
|
266
|
+
*/
|
|
267
|
+
export function isCliInstalled(manifest) {
|
|
268
|
+
const c = manifest.check;
|
|
269
|
+
if (c.kind === 'which') {
|
|
270
|
+
cmdExistsCache.delete(c.cmd);
|
|
271
|
+
return hasCommand(c.cmd);
|
|
272
|
+
}
|
|
273
|
+
const result = spawnSync(c.cmd, c.args, { stdio: 'ignore', timeout: 10_000 });
|
|
274
|
+
return result.status === 0;
|
|
275
|
+
}
|
|
276
|
+
// ─── Method selection ────────────────────────────────────────────────────────
|
|
277
|
+
/**
|
|
278
|
+
* Pick the first install method whose required host tool is available.
|
|
279
|
+
* Returns null when none of the declared methods can run on this host.
|
|
280
|
+
*/
|
|
281
|
+
export function selectInstallMethod(manifest) {
|
|
282
|
+
for (const method of manifest.install) {
|
|
283
|
+
if ('npm' in method && hasCommand('npm'))
|
|
284
|
+
return method;
|
|
285
|
+
if ('brew' in method && hasCommand('brew'))
|
|
286
|
+
return method;
|
|
287
|
+
if ('script' in method && (hasCommand('curl') || hasCommand('wget')))
|
|
288
|
+
return method;
|
|
289
|
+
if ('binary' in method) {
|
|
290
|
+
const key = `${process.platform}-${process.arch}`;
|
|
291
|
+
if (method.binary[key])
|
|
292
|
+
return method;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
/** Render a CheckSpec back to a human-readable command string (display only). */
|
|
298
|
+
export function describeCheck(check) {
|
|
299
|
+
return check.kind === 'which' ? check.cmd : `${check.cmd} ${check.args.join(' ')}`.trim();
|
|
300
|
+
}
|
|
301
|
+
/** Short description of a method for display. */
|
|
302
|
+
export function describeMethod(method) {
|
|
303
|
+
if ('npm' in method)
|
|
304
|
+
return `npm install -g ${method.npm}`;
|
|
305
|
+
if ('brew' in method)
|
|
306
|
+
return `brew install ${method.brew}`;
|
|
307
|
+
if ('script' in method)
|
|
308
|
+
return `curl ${method.script} | sh`;
|
|
309
|
+
const key = `${process.platform}-${process.arch}`;
|
|
310
|
+
const spec = method.binary[key];
|
|
311
|
+
return spec ? `download ${spec.url}` : 'binary download';
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Display-only rendering of how a method would be run, for `--dry-run` and
|
|
315
|
+
* status output. Not used by installCli — execution goes through runInstallMethod
|
|
316
|
+
* which dispatches to spawnSync with argv arrays.
|
|
317
|
+
*/
|
|
318
|
+
export function buildInstallCommand(method) {
|
|
319
|
+
if ('npm' in method)
|
|
320
|
+
return `npm install -g ${method.npm}`;
|
|
321
|
+
if ('brew' in method)
|
|
322
|
+
return `brew install ${method.brew}`;
|
|
323
|
+
if ('script' in method) {
|
|
324
|
+
return hasCommand('curl')
|
|
325
|
+
? `curl -fsSL ${method.script} | sh`
|
|
326
|
+
: `wget -qO- ${method.script} | sh`;
|
|
327
|
+
}
|
|
328
|
+
const key = `${process.platform}-${process.arch}`;
|
|
329
|
+
const spec = method.binary[key];
|
|
330
|
+
if (!spec)
|
|
331
|
+
return 'binary download';
|
|
332
|
+
return spec.extract
|
|
333
|
+
? `curl -fsSL ${spec.url} -o /tmp/agents-cli-bin.tgz && tar -xzf /tmp/agents-cli-bin.tgz -C /usr/local/bin ${spec.extract}`
|
|
334
|
+
: `curl -fsSL ${spec.url} -o /usr/local/bin/agents-cli-downloaded`;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Execute an install method via spawnSync with argv arrays. Each branch
|
|
338
|
+
* re-validates the relevant field — defense in depth, since callers may
|
|
339
|
+
* construct InstallMethod values without going through parseCliManifest
|
|
340
|
+
* (tests, future programmatic use).
|
|
341
|
+
*
|
|
342
|
+
* For `script`, the download is staged to a temp file and then exec'd as
|
|
343
|
+
* `sh <file>` so we never need a shell pipe (`curl | sh`).
|
|
344
|
+
*/
|
|
345
|
+
function runInstallMethod(method) {
|
|
346
|
+
if ('npm' in method) {
|
|
347
|
+
assertNpmPackage(method.npm);
|
|
348
|
+
const r = spawnSync('npm', ['install', '-g', method.npm], { stdio: 'inherit' });
|
|
349
|
+
if (r.status !== 0) {
|
|
350
|
+
throw new Error(`npm install -g ${method.npm} exited with status ${r.status ?? 'unknown'}`);
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if ('brew' in method) {
|
|
355
|
+
assertBrewFormula(method.brew);
|
|
356
|
+
const r = spawnSync('brew', ['install', method.brew], { stdio: 'inherit' });
|
|
357
|
+
if (r.status !== 0) {
|
|
358
|
+
throw new Error(`brew install ${method.brew} exited with status ${r.status ?? 'unknown'}`);
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if ('script' in method) {
|
|
363
|
+
assertHttpsUrl(method.script);
|
|
364
|
+
const tmp = path.join(os.tmpdir(), `agents-cli-install-${process.pid}-${Date.now()}.sh`);
|
|
365
|
+
try {
|
|
366
|
+
let dl;
|
|
367
|
+
if (hasCommand('curl')) {
|
|
368
|
+
dl = spawnSync('curl', ['-fsSL', method.script, '-o', tmp], { stdio: 'inherit' });
|
|
369
|
+
}
|
|
370
|
+
else if (hasCommand('wget')) {
|
|
371
|
+
dl = spawnSync('wget', ['-q', '-O', tmp, method.script], { stdio: 'inherit' });
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
throw new Error('neither curl nor wget is available on PATH');
|
|
375
|
+
}
|
|
376
|
+
if (dl.status !== 0) {
|
|
377
|
+
throw new Error(`download of install script failed (status ${dl.status ?? 'unknown'})`);
|
|
378
|
+
}
|
|
379
|
+
const r = spawnSync('sh', [tmp], { stdio: 'inherit' });
|
|
380
|
+
if (r.status !== 0) {
|
|
381
|
+
throw new Error(`install script exited with status ${r.status ?? 'unknown'}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
try {
|
|
386
|
+
fs.unlinkSync(tmp);
|
|
387
|
+
}
|
|
388
|
+
catch { /* best effort */ }
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if ('binary' in method) {
|
|
393
|
+
const key = `${process.platform}-${process.arch}`;
|
|
394
|
+
const spec = method.binary[key];
|
|
395
|
+
if (!spec)
|
|
396
|
+
throw new Error(`no binary declared for ${key}`);
|
|
397
|
+
assertHttpsUrl(spec.url);
|
|
398
|
+
if (spec.extract) {
|
|
399
|
+
assertSafePathSegment(spec.extract);
|
|
400
|
+
const tmp = path.join(os.tmpdir(), `agents-cli-bin-${process.pid}-${Date.now()}.tgz`);
|
|
401
|
+
try {
|
|
402
|
+
const dl = spawnSync('curl', ['-fsSL', spec.url, '-o', tmp], { stdio: 'inherit' });
|
|
403
|
+
if (dl.status !== 0) {
|
|
404
|
+
throw new Error(`binary download failed (status ${dl.status ?? 'unknown'})`);
|
|
405
|
+
}
|
|
406
|
+
const x = spawnSync('tar', ['-xzf', tmp, '-C', '/usr/local/bin', spec.extract], {
|
|
407
|
+
stdio: 'inherit',
|
|
408
|
+
});
|
|
409
|
+
if (x.status !== 0) {
|
|
410
|
+
throw new Error(`tar extract failed (status ${x.status ?? 'unknown'})`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
finally {
|
|
414
|
+
try {
|
|
415
|
+
fs.unlinkSync(tmp);
|
|
416
|
+
}
|
|
417
|
+
catch { /* best effort */ }
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
const r = spawnSync('curl', ['-fsSL', spec.url, '-o', '/usr/local/bin/agents-cli-downloaded'], { stdio: 'inherit' });
|
|
422
|
+
if (r.status !== 0) {
|
|
423
|
+
throw new Error(`binary download failed (status ${r.status ?? 'unknown'})`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Install a single CLI by running its first compatible method. Streams the
|
|
431
|
+
* underlying command's output to the parent terminal so users see brew/npm
|
|
432
|
+
* progress live. Verifies success by re-running `check`.
|
|
433
|
+
*/
|
|
434
|
+
export function installCli(manifest, opts = {}) {
|
|
435
|
+
const method = selectInstallMethod(manifest);
|
|
436
|
+
if (!method) {
|
|
437
|
+
return {
|
|
438
|
+
manifest,
|
|
439
|
+
method: null,
|
|
440
|
+
installed: false,
|
|
441
|
+
error: `No compatible install method for this host (${process.platform}-${process.arch}). Declared methods: ${manifest.install.map(describeMethod).join('; ')}`,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
if (opts.dryRun) {
|
|
445
|
+
return { manifest, method, installed: false, output: `[dry-run] would run: ${describeMethod(method)}` };
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
runInstallMethod(method);
|
|
449
|
+
}
|
|
450
|
+
catch (err) {
|
|
451
|
+
return {
|
|
452
|
+
manifest,
|
|
453
|
+
method,
|
|
454
|
+
installed: false,
|
|
455
|
+
error: `install command failed: ${err.message}`,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
// Re-check; many installers exit 0 but leave the binary off PATH for the
|
|
459
|
+
// current shell (e.g. brew on a fresh install). Trust `check`, not the
|
|
460
|
+
// installer's exit code.
|
|
461
|
+
cmdExistsCache.delete(manifest.name);
|
|
462
|
+
const installed = isCliInstalled(manifest);
|
|
463
|
+
return { manifest, method, installed };
|
|
464
|
+
}
|
|
465
|
+
/** Convenience: list all manifests + their installed-on-host status. */
|
|
466
|
+
export function listCliStatus(cwd) {
|
|
467
|
+
const { manifests, errors } = listCliManifests(cwd);
|
|
468
|
+
const statuses = manifests.map((manifest) => ({
|
|
469
|
+
manifest,
|
|
470
|
+
installed: isCliInstalled(manifest),
|
|
471
|
+
}));
|
|
472
|
+
return { statuses, errors };
|
|
473
|
+
}
|
|
474
|
+
/** Names of CLIs that are declared but not currently installed on the host. */
|
|
475
|
+
export function getMissingClis(cwd) {
|
|
476
|
+
return listCliStatus(cwd).statuses.filter((s) => !s.installed).map((s) => s.manifest);
|
|
477
|
+
}
|
|
@@ -8,7 +8,7 @@ import type { CloudProvider, CloudTask, CloudTaskStatus, CloudEvent, DispatchOpt
|
|
|
8
8
|
/**
|
|
9
9
|
* Factory/Droid cloud provider — stub for Phase 2.
|
|
10
10
|
*
|
|
11
|
-
* Integration path: `droid daemon` running on a remote machine (
|
|
11
|
+
* Integration path: `droid daemon` running on a remote machine (workstation, cloud VM, k8s pod).
|
|
12
12
|
* Dispatch via HTTP to the daemon, stream output, cancel via HTTP DELETE.
|
|
13
13
|
*
|
|
14
14
|
* Not yet implemented because:
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
/**
|
|
8
8
|
* Factory/Droid cloud provider — stub for Phase 2.
|
|
9
9
|
*
|
|
10
|
-
* Integration path: `droid daemon` running on a remote machine (
|
|
10
|
+
* Integration path: `droid daemon` running on a remote machine (workstation, cloud VM, k8s pod).
|
|
11
11
|
* Dispatch via HTTP to the daemon, stream output, cancel via HTTP DELETE.
|
|
12
12
|
*
|
|
13
13
|
* Not yet implemented because:
|
package/dist/lib/cloud/rush.js
CHANGED
|
@@ -16,7 +16,7 @@ import { listInstalledVersions, getVersionHomePath } from '../versions.js';
|
|
|
16
16
|
import { getAccountInfo } from '../agents.js';
|
|
17
17
|
import { loadClaudeOauth } from '../usage.js';
|
|
18
18
|
import { selectBalancedVersion } from '../rotate.js';
|
|
19
|
-
const PROXY_BASE = 'https://api.prix.dev';
|
|
19
|
+
const PROXY_BASE = process.env.RUSH_PROXY_BASE ?? 'https://api.prix.dev';
|
|
20
20
|
const PROXY_HOST = new URL(PROXY_BASE).host;
|
|
21
21
|
const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
|
|
22
22
|
// Persistent consent record for uploading Claude OAuth blobs to Rush Cloud.
|
|
@@ -441,7 +441,7 @@ export class RushCloudProvider {
|
|
|
441
441
|
}
|
|
442
442
|
async status(taskId) {
|
|
443
443
|
const token = readToken();
|
|
444
|
-
const res = await api('GET', `/api/v1/cloud-runs/${taskId}`, token);
|
|
444
|
+
const res = await api('GET', `/api/v1/cloud-runs/${encodeURIComponent(taskId)}`, token);
|
|
445
445
|
if (!res.ok) {
|
|
446
446
|
throw new Error(`Failed to get task status (${res.status}).`);
|
|
447
447
|
}
|
|
@@ -487,7 +487,7 @@ export class RushCloudProvider {
|
|
|
487
487
|
}
|
|
488
488
|
async *stream(taskId) {
|
|
489
489
|
const token = readToken();
|
|
490
|
-
const res = await fetch(`${PROXY_BASE}/api/v1/cloud-runs/${taskId}/stream`, {
|
|
490
|
+
const res = await fetch(`${PROXY_BASE}/api/v1/cloud-runs/${encodeURIComponent(taskId)}/stream`, {
|
|
491
491
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
492
492
|
});
|
|
493
493
|
if (!res.ok) {
|
|
@@ -497,14 +497,14 @@ export class RushCloudProvider {
|
|
|
497
497
|
}
|
|
498
498
|
async cancel(taskId) {
|
|
499
499
|
const token = readToken();
|
|
500
|
-
const res = await api('DELETE', `/api/v1/cloud-runs/${taskId}`, token);
|
|
500
|
+
const res = await api('DELETE', `/api/v1/cloud-runs/${encodeURIComponent(taskId)}`, token);
|
|
501
501
|
if (!res.ok) {
|
|
502
502
|
throw new Error(`Failed to cancel task (${res.status}).`);
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
505
|
async message(taskId, content) {
|
|
506
506
|
const token = readToken();
|
|
507
|
-
const res = await api('POST', `/api/v1/cloud-runs/${taskId}/message`, token, { content });
|
|
507
|
+
const res = await api('POST', `/api/v1/cloud-runs/${encodeURIComponent(taskId)}/message`, token, { content });
|
|
508
508
|
if (!res.ok) {
|
|
509
509
|
throw new Error(`Failed to send message (${res.status}).`);
|
|
510
510
|
}
|
|
@@ -50,8 +50,6 @@ function readSkillCommandMarker(skillMdPath) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
export function shouldInstallCommandAsSkill(agent, version) {
|
|
53
|
-
if (agent !== 'codex')
|
|
54
|
-
return false;
|
|
55
53
|
return !supports(agent, 'commands', version).ok && supports(agent, 'skills', version).ok;
|
|
56
54
|
}
|
|
57
55
|
export function commandSkillName(commandName) {
|