@phnx-labs/agents-cli 1.20.72 → 1.20.73
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 +299 -0
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +12 -0
- package/dist/commands/activity.js +68 -0
- package/dist/commands/apply.d.ts +13 -0
- package/dist/commands/apply.js +28 -3
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cloud.js +21 -11
- package/dist/commands/doctor.js +8 -4
- package/dist/commands/events.d.ts +11 -10
- package/dist/commands/events.js +30 -21
- package/dist/commands/exec.d.ts +34 -0
- package/dist/commands/exec.js +337 -39
- package/dist/commands/feed.d.ts +13 -0
- package/dist/commands/feed.js +130 -29
- package/dist/commands/hq.d.ts +2 -0
- package/dist/commands/hq.js +58 -0
- package/dist/commands/lease.d.ts +19 -0
- package/dist/commands/lease.js +138 -11
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +123 -0
- package/dist/commands/logs.js +1 -1
- package/dist/commands/mine.d.ts +27 -0
- package/dist/commands/mine.js +207 -0
- package/dist/commands/monitors.js +79 -49
- package/dist/commands/plugins.js +1 -0
- package/dist/commands/profiles.js +158 -0
- package/dist/commands/repo.js +2 -60
- package/dist/commands/resources.d.ts +5 -0
- package/dist/commands/resources.js +95 -0
- package/dist/commands/routines.d.ts +2 -0
- package/dist/commands/routines.js +139 -37
- package/dist/commands/secrets.js +234 -47
- package/dist/commands/send.d.ts +15 -0
- package/dist/commands/send.js +62 -0
- package/dist/commands/sessions-tail.d.ts +1 -0
- package/dist/commands/sessions-tail.js +20 -9
- package/dist/commands/sessions.js +18 -3
- package/dist/commands/setup-mine.d.ts +18 -0
- package/dist/commands/setup-mine.js +106 -0
- package/dist/commands/setup-share.js +2 -2
- package/dist/commands/setup.js +3 -1
- package/dist/commands/share.d.ts +8 -1
- package/dist/commands/share.js +122 -37
- package/dist/commands/ssh.d.ts +9 -0
- package/dist/commands/ssh.js +196 -26
- package/dist/commands/sync.js +1 -1
- package/dist/commands/teams.js +11 -2
- package/dist/index.js +79 -10
- package/dist/lib/activity.d.ts +124 -0
- package/dist/lib/activity.js +542 -0
- package/dist/lib/agents.js +16 -5
- package/dist/lib/artifact-actions.d.ts +1 -1
- package/dist/lib/artifact-actions.js +1 -1
- package/dist/lib/ask-classifier.d.ts +2 -1
- package/dist/lib/ask-classifier.js +3 -3
- package/dist/lib/auth-health.d.ts +32 -4
- package/dist/lib/auth-health.js +40 -1
- package/dist/lib/auto-pull.js +8 -1
- package/dist/lib/brand.d.ts +40 -0
- package/dist/lib/brand.js +122 -0
- package/dist/lib/browser/drivers/ssh.js +4 -7
- package/dist/lib/browser/service.js +6 -8
- package/dist/lib/channels/providers/index.d.ts +2 -0
- package/dist/lib/channels/providers/index.js +20 -0
- package/dist/lib/channels/providers/mailbox.d.ts +2 -0
- package/dist/lib/channels/providers/mailbox.js +26 -0
- package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
- package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
- package/dist/lib/channels/providers/rush.d.ts +6 -0
- package/dist/lib/channels/providers/rush.js +55 -0
- package/dist/lib/channels/registry.d.ts +42 -0
- package/dist/lib/channels/registry.js +20 -0
- package/dist/lib/channels/resolve.d.ts +11 -0
- package/dist/lib/channels/resolve.js +12 -0
- package/dist/lib/cli-resources.d.ts +18 -0
- package/dist/lib/cli-resources.js +53 -4
- package/dist/lib/cloud/codex.js +6 -3
- package/dist/lib/cloud/factory.d.ts +1 -0
- package/dist/lib/cloud/factory.js +10 -4
- package/dist/lib/cloud/rush.d.ts +3 -1
- package/dist/lib/cloud/rush.js +6 -1
- package/dist/lib/cloud/types.d.ts +2 -0
- package/dist/lib/codex-home.d.ts +35 -0
- package/dist/lib/codex-home.js +136 -0
- package/dist/lib/crabbox/cli.d.ts +44 -0
- package/dist/lib/crabbox/cli.js +120 -20
- package/dist/lib/crabbox/lease.d.ts +52 -1
- package/dist/lib/crabbox/lease.js +117 -16
- package/dist/lib/crabbox/progress.d.ts +31 -0
- package/dist/lib/crabbox/progress.js +76 -0
- package/dist/lib/crabbox/runtimes.d.ts +4 -0
- package/dist/lib/crabbox/runtimes.js +33 -8
- package/dist/lib/crabbox/setup-copy.d.ts +87 -0
- package/dist/lib/crabbox/setup-copy.js +127 -0
- package/dist/lib/daemon.d.ts +5 -5
- package/dist/lib/daemon.js +9 -9
- package/dist/lib/drive-sync.js +6 -3
- package/dist/lib/event-stream.d.ts +36 -0
- package/dist/lib/event-stream.js +68 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +11 -0
- package/dist/lib/exec.js +23 -2
- package/dist/lib/feed-policy.d.ts +1 -1
- package/dist/lib/feed-policy.js +14 -9
- package/dist/lib/feed-ranking.d.ts +32 -0
- package/dist/lib/feed-ranking.js +224 -0
- package/dist/lib/feed.d.ts +35 -2
- package/dist/lib/feed.js +66 -3
- package/dist/lib/fleet/auth-sync.d.ts +53 -0
- package/dist/lib/fleet/auth-sync.js +92 -0
- package/dist/lib/fleet/remote-login.d.ts +187 -0
- package/dist/lib/fleet/remote-login.js +556 -0
- package/dist/lib/format.d.ts +30 -3
- package/dist/lib/format.js +34 -5
- package/dist/lib/fs-atomic.d.ts +7 -2
- package/dist/lib/fs-atomic.js +8 -12
- package/dist/lib/git.d.ts +16 -0
- package/dist/lib/git.js +79 -2
- package/dist/lib/heal.js +11 -3
- package/dist/lib/hosts/progress.d.ts +28 -10
- package/dist/lib/hosts/progress.js +79 -22
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/hq/floor.d.ts +87 -0
- package/dist/lib/hq/floor.js +226 -0
- package/dist/lib/menubar/install-menubar.js +14 -20
- package/dist/lib/notify.d.ts +1 -0
- package/dist/lib/notify.js +3 -3
- package/dist/lib/open-url.d.ts +2 -0
- package/dist/lib/open-url.js +19 -0
- package/dist/lib/openclaw-keychain.d.ts +56 -0
- package/dist/lib/openclaw-keychain.js +236 -0
- package/dist/lib/overdue.js +10 -0
- package/dist/lib/permissions.d.ts +44 -1
- package/dist/lib/permissions.js +284 -7
- package/dist/lib/project-launch.d.ts +6 -12
- package/dist/lib/project-launch.js +13 -228
- package/dist/lib/project-resources.d.ts +7 -0
- package/dist/lib/project-resources.js +291 -0
- package/dist/lib/pty-client.d.ts +27 -0
- package/dist/lib/pty-client.js +136 -10
- package/dist/lib/refresh.js +14 -10
- package/dist/lib/resource-profiles.d.ts +26 -0
- package/dist/lib/resource-profiles.js +157 -0
- package/dist/lib/resources/permissions.js +7 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/resources.js +32 -3
- package/dist/lib/routines.d.ts +11 -0
- package/dist/lib/routines.js +56 -15
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +66 -5
- package/dist/lib/secrets/bundles.d.ts +16 -3
- package/dist/lib/secrets/bundles.js +206 -37
- package/dist/lib/secrets/icloud-import.d.ts +2 -2
- package/dist/lib/secrets/icloud-import.js +9 -6
- package/dist/lib/secrets/mcp.js +1 -1
- package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
- package/dist/lib/secrets/vault-age-helper.js +34 -0
- package/dist/lib/secrets/vault.d.ts +49 -0
- package/dist/lib/secrets/vault.js +397 -0
- package/dist/lib/self-heal/checks/path.js +3 -1
- package/dist/lib/self-heal/checks/shadowing.js +10 -2
- package/dist/lib/self-heal/checks/shims.js +19 -11
- package/dist/lib/session/cloud.d.ts +2 -2
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +44 -7
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +114 -3
- package/dist/lib/session/stream-render.d.ts +3 -0
- package/dist/lib/session/stream-render.js +130 -0
- package/dist/lib/session/types.d.ts +6 -0
- package/dist/lib/share/analytics.d.ts +13 -0
- package/dist/lib/share/analytics.js +45 -0
- package/dist/lib/share/config.d.ts +19 -5
- package/dist/lib/share/config.js +44 -8
- package/dist/lib/share/provision.d.ts +58 -6
- package/dist/lib/share/provision.js +97 -22
- package/dist/lib/share/publish.d.ts +36 -13
- package/dist/lib/share/publish.js +55 -8
- package/dist/lib/share/worker-template.js +83 -17
- package/dist/lib/shims.d.ts +9 -0
- package/dist/lib/shims.js +91 -13
- package/dist/lib/ssh-exec.d.ts +14 -0
- package/dist/lib/ssh-exec.js +57 -0
- package/dist/lib/staleness/detectors/hooks.js +25 -1
- package/dist/lib/staleness/detectors/permissions.js +66 -0
- package/dist/lib/staleness/detectors/workflows.js +20 -0
- package/dist/lib/staleness/writers/hooks.js +58 -4
- package/dist/lib/staleness/writers/permissions.js +2 -2
- package/dist/lib/staleness/writers/skills.js +1 -1
- package/dist/lib/staleness/writers/sources.d.ts +10 -1
- package/dist/lib/staleness/writers/sources.js +68 -1
- package/dist/lib/star-nudge.d.ts +45 -0
- package/dist/lib/star-nudge.js +91 -0
- package/dist/lib/startup/command-registry.d.ts +6 -1
- package/dist/lib/startup/command-registry.js +17 -3
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +3 -0
- package/dist/lib/subagents-registry.d.ts +2 -0
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/teams/parsers.js +214 -6
- package/dist/lib/teams/supervisor.d.ts +7 -0
- package/dist/lib/teams/supervisor.js +2 -1
- package/dist/lib/template.d.ts +1 -1
- package/dist/lib/template.js +1 -1
- package/dist/lib/triggers/webhook.js +36 -3
- package/dist/lib/types.d.ts +73 -0
- package/dist/lib/version.d.ts +40 -0
- package/dist/lib/version.js +94 -16
- package/dist/lib/versions.d.ts +11 -0
- package/dist/lib/versions.js +208 -76
- package/dist/lib/workflows.d.ts +2 -0
- package/dist/lib/workflows.js +88 -0
- package/package.json +2 -1
- package/dist/commands/daemon.d.ts +0 -10
- package/dist/commands/daemon.js +0 -121
package/dist/lib/shims.js
CHANGED
|
@@ -17,6 +17,7 @@ import { IS_WINDOWS, prependToWindowsUserPath } from './platform/index.js';
|
|
|
17
17
|
import { getShimsDir, getVersionsDir, getBackupsDir, getHistoryDir, ensureAgentsDir } from './state.js';
|
|
18
18
|
export { getShimsDir };
|
|
19
19
|
import { AGENTS, agentConfigDirName, readAuthAccountIdentity } from './agents.js';
|
|
20
|
+
import { codexHomeShimBash } from './codex-home.js';
|
|
20
21
|
/**
|
|
21
22
|
* Files and directories to always skip during conflict detection and migration.
|
|
22
23
|
* These are never user config that should be migrated.
|
|
@@ -261,12 +262,7 @@ if [ "\$(uname -s)" = "Linux" ] && [ -z "\${CLAUDE_CODE_OAUTH_TOKEN:-}" ] && [ -
|
|
|
261
262
|
fi
|
|
262
263
|
`
|
|
263
264
|
: agent === 'codex'
|
|
264
|
-
? `
|
|
265
|
-
# Codex reads its config (approval_policy, sandbox_mode, MCP servers, rules)
|
|
266
|
-
# from CODEX_HOME. Point it at the versioned home so permissions/rules
|
|
267
|
-
# written by agents-cli actually take effect.
|
|
268
|
-
export CODEX_HOME="$VERSION_DIR/home/${configDirName}"
|
|
269
|
-
`
|
|
265
|
+
? codexHomeShimBash(`$VERSION_DIR/home/${configDirName}`, `$AGENTS_USER_DIR/.codex-homes/$VERSION`)
|
|
270
266
|
: agent === 'copilot'
|
|
271
267
|
? `
|
|
272
268
|
# GitHub Copilot CLI honors COPILOT_HOME to relocate its config and state
|
|
@@ -682,6 +678,75 @@ export function createShim(agent) {
|
|
|
682
678
|
}
|
|
683
679
|
return shimPath;
|
|
684
680
|
}
|
|
681
|
+
// ─── White-label brand shims ──────────────────────────────────────────────────
|
|
682
|
+
// A brand shim is a PURE pass-through: unlike an agent shim (which resolves a
|
|
683
|
+
// version and execs the agent binary) or an alias shim (which injects a fixed
|
|
684
|
+
// subcommand), it forwards argv verbatim to the agents-cli entrypoint with
|
|
685
|
+
// `AGENTS_BRAND` set, so `<brand> <any-verb>` behaves exactly like
|
|
686
|
+
// `agents <any-verb>` but presents under the brand's name. See lib/brand.ts.
|
|
687
|
+
const BRAND_SHIM_MARKER = '# Brand shim:';
|
|
688
|
+
/** The POSIX pass-through shim for a brand. */
|
|
689
|
+
export function generateBrandShim(name) {
|
|
690
|
+
const agentsBin = shellQuote(getAgentsBinForGeneratedShim());
|
|
691
|
+
return `#!/bin/sh
|
|
692
|
+
# Auto-generated by agents-cli - do not edit
|
|
693
|
+
${BRAND_SHIM_MARKER} ${name} -> agents-cli (white-label)
|
|
694
|
+
AGENTS_BIN=${agentsBin}
|
|
695
|
+
if [ -z "$AGENTS_BIN" ] || [ ! -x "$AGENTS_BIN" ]; then
|
|
696
|
+
echo "${name}: agents-cli entrypoint missing or not executable: $AGENTS_BIN" >&2
|
|
697
|
+
exit 127
|
|
698
|
+
fi
|
|
699
|
+
export AGENTS_BRAND=${name}
|
|
700
|
+
exec "$AGENTS_BIN" "$@"
|
|
701
|
+
`;
|
|
702
|
+
}
|
|
703
|
+
/** Write a brand's pass-through shim(s) onto PATH; returns the shim path. */
|
|
704
|
+
export function createBrandShim(name) {
|
|
705
|
+
ensureAgentsDir();
|
|
706
|
+
const shimsDir = getShimsDir();
|
|
707
|
+
const shimPath = path.join(shimsDir, name);
|
|
708
|
+
const targets = shimTargetsFor(process.platform);
|
|
709
|
+
if (targets.bash) {
|
|
710
|
+
fs.writeFileSync(shimPath, generateBrandShim(name), { mode: 0o755 });
|
|
711
|
+
}
|
|
712
|
+
if (targets.cmd) {
|
|
713
|
+
writeWindowsBrandShim(shimPath + '.cmd', name);
|
|
714
|
+
}
|
|
715
|
+
return shimPath;
|
|
716
|
+
}
|
|
717
|
+
/** Windows `.cmd` pass-through: set AGENTS_BRAND then forward argv to the entrypoint. */
|
|
718
|
+
function writeWindowsBrandShim(cmdPath, name) {
|
|
719
|
+
const indexJs = getAgentsBinForGeneratedShim();
|
|
720
|
+
const content = `@echo off\r\n` +
|
|
721
|
+
`rem Auto-generated by agents-cli - do not edit\r\n` +
|
|
722
|
+
`rem Brand shim: ${name}\r\n` +
|
|
723
|
+
`set AGENTS_BRAND=${name}\r\n` +
|
|
724
|
+
`node "${indexJs}" %*\r\n`;
|
|
725
|
+
fs.writeFileSync(cmdPath, content);
|
|
726
|
+
}
|
|
727
|
+
/** True when the file at the given path is an agents-cli brand shim. */
|
|
728
|
+
export function isBrandShim(filePath) {
|
|
729
|
+
try {
|
|
730
|
+
const head = fs.readFileSync(filePath, 'utf-8').slice(0, 300);
|
|
731
|
+
return head.includes(BRAND_SHIM_MARKER) || head.includes('rem Brand shim:');
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
return false;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/** Remove a brand's shim companions. Returns true if anything was removed. */
|
|
738
|
+
export function removeBrandShim(name) {
|
|
739
|
+
const shimsDir = getShimsDir();
|
|
740
|
+
const shimPath = path.join(shimsDir, name);
|
|
741
|
+
let removed = false;
|
|
742
|
+
for (const p of [shimPath, shimPath + '.cmd']) {
|
|
743
|
+
if (fs.existsSync(p) && isBrandShim(p)) {
|
|
744
|
+
fs.unlinkSync(p);
|
|
745
|
+
removed = true;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return removed;
|
|
749
|
+
}
|
|
685
750
|
/**
|
|
686
751
|
* Generate a Windows `.cmd` launcher that delegates to `agents __shim <spec>`.
|
|
687
752
|
* `spec` is the agent's cliCommand for the default-version shim, or
|
|
@@ -829,12 +894,7 @@ export CLAUDE_CONFIG_DIR="$HOME/.agents/.history/versions/${agent}/${version}/ho
|
|
|
829
894
|
export DISABLE_AUTOUPDATER="\${DISABLE_AUTOUPDATER:-1}"
|
|
830
895
|
`
|
|
831
896
|
: agent === 'codex'
|
|
832
|
-
? `
|
|
833
|
-
# Codex reads its config (approval_policy, sandbox_mode, MCP servers, rules)
|
|
834
|
-
# from CODEX_HOME. Point direct aliases at the versioned home so permissions
|
|
835
|
-
# and rules written by agents-cli actually take effect.
|
|
836
|
-
export CODEX_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
|
|
837
|
-
`
|
|
897
|
+
? codexHomeShimBash(`$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}`, `$HOME/.agents/.codex-homes/${version}`)
|
|
838
898
|
: agent === 'copilot'
|
|
839
899
|
? `
|
|
840
900
|
# Copilot honors COPILOT_HOME to relocate ~/.copilot (settings, mcp-config.json,
|
|
@@ -2368,6 +2428,24 @@ export function listAgentsWithInstalledVersions() {
|
|
|
2368
2428
|
.map((entry) => entry.name)
|
|
2369
2429
|
.filter((agent) => fs.readdirSync(path.join(versionsDir, agent), { withFileTypes: true }).some((entry) => entry.isDirectory()));
|
|
2370
2430
|
}
|
|
2431
|
+
function isInstalledVersionIsolated(agent, version) {
|
|
2432
|
+
return fs.existsSync(path.join(getVersionsDir(), agent, version, '.isolated'));
|
|
2433
|
+
}
|
|
2434
|
+
export function listAgentsWithNonIsolatedInstalledVersions() {
|
|
2435
|
+
const versionsDir = getVersionsDir();
|
|
2436
|
+
if (!fs.existsSync(versionsDir)) {
|
|
2437
|
+
return [];
|
|
2438
|
+
}
|
|
2439
|
+
const entries = fs.readdirSync(versionsDir, { withFileTypes: true });
|
|
2440
|
+
return entries
|
|
2441
|
+
.filter((entry) => entry.isDirectory() && AGENTS[entry.name])
|
|
2442
|
+
.map((entry) => entry.name)
|
|
2443
|
+
.filter((agent) => {
|
|
2444
|
+
const agentVersionsDir = path.join(versionsDir, agent);
|
|
2445
|
+
return fs.readdirSync(agentVersionsDir, { withFileTypes: true })
|
|
2446
|
+
.some((entry) => entry.isDirectory() && !isInstalledVersionIsolated(agent, entry.name));
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2371
2449
|
/**
|
|
2372
2450
|
* Create shims for all installed agents.
|
|
2373
2451
|
*/
|
|
@@ -2382,7 +2460,7 @@ function ensureAllShims() {
|
|
|
2382
2460
|
const agent = entry.name;
|
|
2383
2461
|
const agentVersionsDir = path.join(versionsDir, agent);
|
|
2384
2462
|
const versions = fs.readdirSync(agentVersionsDir, { withFileTypes: true })
|
|
2385
|
-
.filter((e) => e.isDirectory());
|
|
2463
|
+
.filter((e) => e.isDirectory() && !isInstalledVersionIsolated(agent, e.name));
|
|
2386
2464
|
if (versions.length > 0 && !shimExists(agent)) {
|
|
2387
2465
|
createShim(agent);
|
|
2388
2466
|
}
|
package/dist/lib/ssh-exec.d.ts
CHANGED
|
@@ -89,6 +89,20 @@ export interface SshExecRawResult {
|
|
|
89
89
|
* U+FFFD replacement char (which would desync a byte offset from the wire).
|
|
90
90
|
*/
|
|
91
91
|
export declare function sshExecRaw(target: string, remoteCmd: string, opts?: SshExecOptions): SshExecRawResult;
|
|
92
|
+
export interface SshExecRawStreamOptions extends SshExecOptions {
|
|
93
|
+
onStdout: (chunk: Buffer) => void;
|
|
94
|
+
onStderr?: (chunk: Buffer) => void;
|
|
95
|
+
signal?: AbortSignal;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Stream raw stdout from a long-lived remote command over ssh.
|
|
99
|
+
*
|
|
100
|
+
* Unlike {@link sshStream}, this does not inherit local stdio: callers receive
|
|
101
|
+
* byte-exact stdout chunks and decide how to account for them. That matters for
|
|
102
|
+
* offset-resumed log following where a UTF-8 decode boundary must not shift the
|
|
103
|
+
* byte cursor.
|
|
104
|
+
*/
|
|
105
|
+
export declare function sshExecRawStream(target: string, remoteCmd: string, opts: SshExecRawStreamOptions): Promise<Omit<SshExecRawResult, 'stdout'>>;
|
|
92
106
|
/** True if `target` is reachable over ssh (a passwordless `true` succeeds quickly). */
|
|
93
107
|
export declare function sshReachable(target: string, timeoutMs?: number): boolean;
|
|
94
108
|
export interface SshStreamOptions {
|
package/dist/lib/ssh-exec.js
CHANGED
|
@@ -205,6 +205,63 @@ export function sshExecRaw(target, remoteCmd, opts = {}) {
|
|
|
205
205
|
timedOut,
|
|
206
206
|
};
|
|
207
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Stream raw stdout from a long-lived remote command over ssh.
|
|
210
|
+
*
|
|
211
|
+
* Unlike {@link sshStream}, this does not inherit local stdio: callers receive
|
|
212
|
+
* byte-exact stdout chunks and decide how to account for them. That matters for
|
|
213
|
+
* offset-resumed log following where a UTF-8 decode boundary must not shift the
|
|
214
|
+
* byte cursor.
|
|
215
|
+
*/
|
|
216
|
+
export function sshExecRawStream(target, remoteCmd, opts) {
|
|
217
|
+
assertValidSshTarget(target);
|
|
218
|
+
const mux = opts.multiplex === false ? [] : controlOpts();
|
|
219
|
+
const args = [...sshConnectOpts(mux, opts.hostKeyOpts), ...(opts.extraSshArgs ?? []), target, remoteCmd];
|
|
220
|
+
return new Promise((resolve) => {
|
|
221
|
+
const child = spawn('ssh', args, {
|
|
222
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
223
|
+
windowsHide: true,
|
|
224
|
+
});
|
|
225
|
+
const stderr = [];
|
|
226
|
+
let settled = false;
|
|
227
|
+
let timedOut = false;
|
|
228
|
+
let aborted = false;
|
|
229
|
+
const finish = (code) => {
|
|
230
|
+
if (settled)
|
|
231
|
+
return;
|
|
232
|
+
settled = true;
|
|
233
|
+
if (timer)
|
|
234
|
+
clearTimeout(timer);
|
|
235
|
+
opts.signal?.removeEventListener('abort', abort);
|
|
236
|
+
resolve({ code, stderr: Buffer.concat(stderr), timedOut });
|
|
237
|
+
};
|
|
238
|
+
const abort = () => {
|
|
239
|
+
aborted = true;
|
|
240
|
+
child.kill('SIGTERM');
|
|
241
|
+
};
|
|
242
|
+
const timer = opts.timeoutMs
|
|
243
|
+
? setTimeout(() => {
|
|
244
|
+
timedOut = true;
|
|
245
|
+
child.kill('SIGTERM');
|
|
246
|
+
}, opts.timeoutMs)
|
|
247
|
+
: null;
|
|
248
|
+
child.stdout.on('data', (chunk) => { opts.onStdout(chunk); });
|
|
249
|
+
child.stderr.on('data', (chunk) => {
|
|
250
|
+
stderr.push(chunk);
|
|
251
|
+
opts.onStderr?.(chunk);
|
|
252
|
+
});
|
|
253
|
+
child.stdin.on('error', () => { });
|
|
254
|
+
child.stdin.end(opts.input ?? '');
|
|
255
|
+
opts.signal?.addEventListener('abort', abort, { once: true });
|
|
256
|
+
child.on('error', (err) => {
|
|
257
|
+
stderr.push(Buffer.from(err.message));
|
|
258
|
+
finish(null);
|
|
259
|
+
});
|
|
260
|
+
child.on('close', (code) => {
|
|
261
|
+
finish(aborted ? null : code);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
}
|
|
208
265
|
/** True if `target` is reachable over ssh (a passwordless `true` succeeds quickly). */
|
|
209
266
|
export function sshReachable(target, timeoutMs = 10000) {
|
|
210
267
|
return sshExec(target, 'true', { timeoutMs, multiplex: true }).code === 0;
|
|
@@ -8,6 +8,30 @@ import * as path from 'path';
|
|
|
8
8
|
import { capableAgents } from '../../capabilities.js';
|
|
9
9
|
import { resolveHookSource } from '../writers/sources.js';
|
|
10
10
|
import { lazyAgentMap } from '../writers/lazy-map.js';
|
|
11
|
+
function hookSourcesMatch(src, dest) {
|
|
12
|
+
const srcStat = fs.lstatSync(src);
|
|
13
|
+
if (srcStat.isSymbolicLink())
|
|
14
|
+
return false;
|
|
15
|
+
if (srcStat.isDirectory()) {
|
|
16
|
+
if (!fs.existsSync(dest) || !fs.lstatSync(dest).isDirectory())
|
|
17
|
+
return false;
|
|
18
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (entry.isSymbolicLink())
|
|
21
|
+
continue;
|
|
22
|
+
const srcPath = path.join(src, entry.name);
|
|
23
|
+
const destPath = path.join(dest, entry.name);
|
|
24
|
+
if (!hookSourcesMatch(srcPath, destPath))
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (!srcStat.isFile())
|
|
30
|
+
return false;
|
|
31
|
+
if (!fs.existsSync(dest) || !fs.lstatSync(dest).isFile())
|
|
32
|
+
return false;
|
|
33
|
+
return fs.readFileSync(src, 'utf-8') === fs.readFileSync(dest, 'utf-8');
|
|
34
|
+
}
|
|
11
35
|
function buildHooksDetector(agent) {
|
|
12
36
|
return {
|
|
13
37
|
kind: 'hooks',
|
|
@@ -26,7 +50,7 @@ function buildHooksDetector(agent) {
|
|
|
26
50
|
continue;
|
|
27
51
|
}
|
|
28
52
|
try {
|
|
29
|
-
if (
|
|
53
|
+
if (hookSourcesMatch(src, path.join(hooksDir, hook))) {
|
|
30
54
|
synced.push(hook);
|
|
31
55
|
}
|
|
32
56
|
}
|
|
@@ -288,6 +288,69 @@ function buildOpenClawDetector() {
|
|
|
288
288
|
},
|
|
289
289
|
};
|
|
290
290
|
}
|
|
291
|
+
function buildCopilotDetector() {
|
|
292
|
+
return {
|
|
293
|
+
kind: 'permissions',
|
|
294
|
+
agent: 'copilot',
|
|
295
|
+
list({ versionHome, cwd }) {
|
|
296
|
+
const configPath = path.join(versionHome, '.copilot', 'permissions-config.json');
|
|
297
|
+
if (!fs.existsSync(configPath))
|
|
298
|
+
return [];
|
|
299
|
+
try {
|
|
300
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
301
|
+
const locations = config.locations && typeof config.locations === 'object' && !Array.isArray(config.locations)
|
|
302
|
+
? config.locations
|
|
303
|
+
: {};
|
|
304
|
+
const location = locations[path.resolve(cwd)];
|
|
305
|
+
const hasApprovals = Array.isArray(location?.tool_approvals) && location.tool_approvals.length > 0;
|
|
306
|
+
const hasDirectories = Array.isArray(location?.allowed_directories) && location.allowed_directories.length > 0;
|
|
307
|
+
if (hasApprovals || hasDirectories)
|
|
308
|
+
return discoverPermissionGroups().map(g => g.name);
|
|
309
|
+
}
|
|
310
|
+
catch { /* parse fail */ }
|
|
311
|
+
return [];
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function buildForgeDetector() {
|
|
316
|
+
return {
|
|
317
|
+
kind: 'permissions',
|
|
318
|
+
agent: 'forge',
|
|
319
|
+
list({ versionHome }) {
|
|
320
|
+
const permissionsPath = path.join(versionHome, '.forge', 'permissions.yaml');
|
|
321
|
+
if (!fs.existsSync(permissionsPath))
|
|
322
|
+
return [];
|
|
323
|
+
try {
|
|
324
|
+
const config = yaml.parse(fs.readFileSync(permissionsPath, 'utf-8'));
|
|
325
|
+
if (config && Array.isArray(config.policies) && config.policies.length > 0) {
|
|
326
|
+
return discoverPermissionGroups().map(g => g.name);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch { /* parse fail */ }
|
|
330
|
+
return [];
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function buildHermesDetector() {
|
|
335
|
+
return {
|
|
336
|
+
kind: 'permissions',
|
|
337
|
+
agent: 'hermes',
|
|
338
|
+
list({ versionHome }) {
|
|
339
|
+
const configPath = path.join(versionHome, '.hermes', 'config.yaml');
|
|
340
|
+
if (!fs.existsSync(configPath))
|
|
341
|
+
return [];
|
|
342
|
+
try {
|
|
343
|
+
const config = yaml.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
344
|
+
const hasAllow = Array.isArray(config?.command_allowlist) && config.command_allowlist.length > 0;
|
|
345
|
+
const hasDeny = Array.isArray(config?.approvals?.deny) && config.approvals.deny.length > 0;
|
|
346
|
+
if (hasAllow || hasDeny)
|
|
347
|
+
return discoverPermissionGroups().map(g => g.name);
|
|
348
|
+
}
|
|
349
|
+
catch { /* parse fail */ }
|
|
350
|
+
return [];
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
291
354
|
const handlers = {
|
|
292
355
|
claude: buildClaudeDetector,
|
|
293
356
|
codex: buildCodexDetector,
|
|
@@ -301,6 +364,9 @@ const handlers = {
|
|
|
301
364
|
droid: buildDroidDetector,
|
|
302
365
|
kiro: buildKiroDetector,
|
|
303
366
|
openclaw: buildOpenClawDetector,
|
|
367
|
+
copilot: buildCopilotDetector,
|
|
368
|
+
forge: buildForgeDetector,
|
|
369
|
+
hermes: buildHermesDetector,
|
|
304
370
|
};
|
|
305
371
|
export const permissionsDetectors = lazyAgentMap(() => {
|
|
306
372
|
const m = {};
|
|
@@ -73,6 +73,26 @@ function buildWorkflowsDetector(agent) {
|
|
|
73
73
|
.filter(d => d.isFile() && d.name.endsWith('.yaml') && !d.name.startsWith('.'))
|
|
74
74
|
.map(d => d.name.slice(0, -'.yaml'.length));
|
|
75
75
|
}
|
|
76
|
+
if (agent === 'openclaw') {
|
|
77
|
+
const dir = path.join(versionHome, '.openclaw', 'workflows');
|
|
78
|
+
if (!fs.existsSync(dir))
|
|
79
|
+
return [];
|
|
80
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
81
|
+
.filter(d => d.isFile() && d.name.endsWith('.lobster') && !d.name.startsWith('.'))
|
|
82
|
+
.filter(d => {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = yaml.parse(fs.readFileSync(path.join(dir, d.name), 'utf-8'));
|
|
85
|
+
const env = parsed && typeof parsed === 'object' && parsed.env && typeof parsed.env === 'object' && !Array.isArray(parsed.env)
|
|
86
|
+
? parsed.env
|
|
87
|
+
: {};
|
|
88
|
+
return env.AGENTS_CLI_WORKFLOW === d.name.slice(0, -'.lobster'.length);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
.map(d => d.name.slice(0, -'.lobster'.length));
|
|
95
|
+
}
|
|
76
96
|
const workflowsDir = path.join(versionHome, 'workflows');
|
|
77
97
|
if (!fs.existsSync(workflowsDir))
|
|
78
98
|
return [];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hooks writer — copies trusted hook
|
|
2
|
+
* Hooks writer — copies trusted hook sources into `{agentDir}/hooks/`.
|
|
3
3
|
* Caller filters by `supports(agent, 'hooks', version)` before invoking.
|
|
4
4
|
* Orphan sweep (delete hooks in version-home that aren't in `availableNames`)
|
|
5
5
|
* stays in the orchestrator since it depends on the broader available set.
|
|
@@ -12,6 +12,60 @@ import { safeJoin } from '../../paths.js';
|
|
|
12
12
|
import { registerHooksToSettings } from '../../hooks.js';
|
|
13
13
|
import { resolveHookSource } from './sources.js';
|
|
14
14
|
import { lazyAgentMap } from './lazy-map.js';
|
|
15
|
+
function removePath(target) {
|
|
16
|
+
try {
|
|
17
|
+
const stat = fs.lstatSync(target);
|
|
18
|
+
if (stat.isSymbolicLink() || stat.isFile()) {
|
|
19
|
+
fs.unlinkSync(target);
|
|
20
|
+
}
|
|
21
|
+
else if (stat.isDirectory()) {
|
|
22
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* already gone */
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function copyDir(src, dest) {
|
|
30
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
31
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (entry.isSymbolicLink())
|
|
34
|
+
continue;
|
|
35
|
+
const srcPath = safeJoin(src, entry.name);
|
|
36
|
+
const destPath = safeJoin(dest, entry.name);
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
copyDir(srcPath, destPath);
|
|
39
|
+
}
|
|
40
|
+
else if (entry.isFile()) {
|
|
41
|
+
fs.copyFileSync(srcPath, destPath);
|
|
42
|
+
fs.chmodSync(destPath, fs.statSync(srcPath).mode);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function copyHookSource(src, dest) {
|
|
47
|
+
let stat;
|
|
48
|
+
try {
|
|
49
|
+
stat = fs.lstatSync(src);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (stat.isSymbolicLink())
|
|
55
|
+
return false;
|
|
56
|
+
removePath(dest);
|
|
57
|
+
if (stat.isDirectory()) {
|
|
58
|
+
copyDir(src, dest);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
if (stat.isFile()) {
|
|
62
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
63
|
+
fs.copyFileSync(src, dest);
|
|
64
|
+
fs.chmodSync(dest, 0o755);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
15
69
|
function buildHooksWriter(agent) {
|
|
16
70
|
return {
|
|
17
71
|
kind: 'hooks',
|
|
@@ -26,9 +80,9 @@ function buildHooksWriter(agent) {
|
|
|
26
80
|
if (!srcFile)
|
|
27
81
|
continue;
|
|
28
82
|
const destFile = safeJoin(hooksTarget, hook);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
83
|
+
if (copyHookSource(srcFile, destFile)) {
|
|
84
|
+
synced.push(hook);
|
|
85
|
+
}
|
|
32
86
|
}
|
|
33
87
|
// Native hook registration in settings.json/hooks.json. Grok is included
|
|
34
88
|
// so subrule-bundled guards (absolute paths outside the central hooks/
|
|
@@ -5,7 +5,7 @@ function buildPermissionsWriter(agent) {
|
|
|
5
5
|
return {
|
|
6
6
|
kind: 'permissions',
|
|
7
7
|
agent,
|
|
8
|
-
write({ versionHome, selection }) {
|
|
8
|
+
write({ versionHome, selection, cwd }) {
|
|
9
9
|
if (selection.length === 0)
|
|
10
10
|
return { synced: [] };
|
|
11
11
|
const built = buildPermissionsFromGroups(selection);
|
|
@@ -13,7 +13,7 @@ function buildPermissionsWriter(agent) {
|
|
|
13
13
|
const hasDeny = (built.deny?.length ?? 0) > 0;
|
|
14
14
|
if (!hasAllow && !hasDeny)
|
|
15
15
|
return { synced: [] };
|
|
16
|
-
const r = applyPermsToVersion(agent, built, versionHome, true);
|
|
16
|
+
const r = applyPermsToVersion(agent, built, versionHome, true, cwd);
|
|
17
17
|
return { synced: r.success ? selection : [] };
|
|
18
18
|
},
|
|
19
19
|
};
|
|
@@ -56,7 +56,7 @@ function buildSkillsWriter(agent) {
|
|
|
56
56
|
fs.mkdirSync(skillsTarget, { recursive: true });
|
|
57
57
|
const synced = [];
|
|
58
58
|
for (const skill of selection) {
|
|
59
|
-
const srcDir = resolveSkillSource(skill);
|
|
59
|
+
const srcDir = resolveSkillSource(skill, { agent });
|
|
60
60
|
if (!srcDir)
|
|
61
61
|
continue;
|
|
62
62
|
const destDir = safeJoin(skillsTarget, skill);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AgentId } from '../../types.js';
|
|
1
2
|
export type EnabledExtra = {
|
|
2
3
|
alias: string;
|
|
3
4
|
dir: string;
|
|
@@ -9,7 +10,15 @@ export declare function trustedSourceBases(): {
|
|
|
9
10
|
/** Find the trusted source for a command markdown by name. */
|
|
10
11
|
export declare function resolveCommandSource(name: string): string | null;
|
|
11
12
|
/** Find the trusted source directory for a skill by name. */
|
|
12
|
-
export declare function resolveSkillSource(name: string
|
|
13
|
+
export declare function resolveSkillSource(name: string, options?: {
|
|
14
|
+
agent?: AgentId;
|
|
15
|
+
plugins?: Set<string>;
|
|
16
|
+
}): string | null;
|
|
17
|
+
/** List trusted plugin-bundled skill names, filtered to an optional plugin/agent scope. */
|
|
18
|
+
export declare function listPluginSkillNames(options?: {
|
|
19
|
+
agent?: AgentId;
|
|
20
|
+
plugins?: Set<string>;
|
|
21
|
+
}): string[];
|
|
13
22
|
/** Find the trusted source file for a hook by name. */
|
|
14
23
|
export declare function resolveHookSource(name: string): string | null;
|
|
15
24
|
/** All trusted command-skill source roots, used to dedup name collisions for commands-as-skills writes. */
|
|
@@ -35,6 +35,50 @@ function isLiveDir(p) {
|
|
|
35
35
|
return false;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
function readPluginManifest(pluginRoot) {
|
|
39
|
+
const manifestPath = path.join(pluginRoot, '.claude-plugin', 'plugin.json');
|
|
40
|
+
if (!isLiveFile(manifestPath))
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
44
|
+
if (!manifest.name || !manifest.version)
|
|
45
|
+
return null;
|
|
46
|
+
return manifest;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function pluginSupportsAgent(manifest, agent) {
|
|
53
|
+
if (!agent)
|
|
54
|
+
return true;
|
|
55
|
+
return !manifest.agents || manifest.agents.length === 0 || manifest.agents.includes(agent);
|
|
56
|
+
}
|
|
57
|
+
function pluginSkillDirs(options = {}) {
|
|
58
|
+
const dirs = [];
|
|
59
|
+
for (const base of trustedSourceBases()) {
|
|
60
|
+
const pluginsDir = path.join(base.dir, 'plugins');
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
70
|
+
continue;
|
|
71
|
+
if (options.plugins && !options.plugins.has(entry.name))
|
|
72
|
+
continue;
|
|
73
|
+
const pluginRoot = path.join(pluginsDir, entry.name);
|
|
74
|
+
const manifest = readPluginManifest(pluginRoot);
|
|
75
|
+
if (!manifest || !pluginSupportsAgent(manifest, options.agent))
|
|
76
|
+
continue;
|
|
77
|
+
dirs.push(path.join(pluginRoot, 'skills'));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return dirs;
|
|
81
|
+
}
|
|
38
82
|
/** Find the trusted source for a command markdown by name. */
|
|
39
83
|
export function resolveCommandSource(name) {
|
|
40
84
|
const candidates = [
|
|
@@ -45,14 +89,36 @@ export function resolveCommandSource(name) {
|
|
|
45
89
|
return candidates.find(isLiveFile) ?? null;
|
|
46
90
|
}
|
|
47
91
|
/** Find the trusted source directory for a skill by name. */
|
|
48
|
-
export function resolveSkillSource(name) {
|
|
92
|
+
export function resolveSkillSource(name, options = {}) {
|
|
49
93
|
const candidates = [
|
|
50
94
|
safeJoin(path.join(getUserAgentsDir(), 'skills'), name),
|
|
51
95
|
safeJoin(getSkillsDir(), name),
|
|
52
96
|
...getEnabledExtraRepos().map((e) => safeJoin(path.join(e.dir, 'skills'), name)),
|
|
97
|
+
...pluginSkillDirs(options).map((skillsDir) => safeJoin(skillsDir, name)),
|
|
53
98
|
];
|
|
54
99
|
return candidates.find(isLiveDir) ?? null;
|
|
55
100
|
}
|
|
101
|
+
/** List trusted plugin-bundled skill names, filtered to an optional plugin/agent scope. */
|
|
102
|
+
export function listPluginSkillNames(options = {}) {
|
|
103
|
+
const names = new Set();
|
|
104
|
+
for (const skillsDir of pluginSkillDirs(options)) {
|
|
105
|
+
let entries;
|
|
106
|
+
try {
|
|
107
|
+
entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
114
|
+
continue;
|
|
115
|
+
if (isLiveFile(path.join(skillsDir, entry.name, 'SKILL.md'))) {
|
|
116
|
+
names.add(entry.name);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return Array.from(names);
|
|
121
|
+
}
|
|
56
122
|
/** Find the trusted source file for a hook by name. */
|
|
57
123
|
export function resolveHookSource(name) {
|
|
58
124
|
const candidates = [
|
|
@@ -68,5 +134,6 @@ export function trustedSkillRoots() {
|
|
|
68
134
|
path.join(getUserAgentsDir(), 'skills'),
|
|
69
135
|
getSkillsDir(),
|
|
70
136
|
...getEnabledExtraRepos().map((e) => path.join(e.dir, 'skills')),
|
|
137
|
+
...pluginSkillDirs(),
|
|
71
138
|
];
|
|
72
139
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time "star us on GitHub" nudge.
|
|
3
|
+
*
|
|
4
|
+
* Printed a single time, ever, after a user's first *successful* headline run
|
|
5
|
+
* (`agents run` / `agents teams`). Modelled on the existing warn-once sentinel
|
|
6
|
+
* pattern (see maybeWarnMultiInstall in src/index.ts): a marker under the
|
|
7
|
+
* regenerable runtime-state dir records that the hint was shown so it never
|
|
8
|
+
* repeats. It is a plain inline line — not a toast, not a nag — and stays out
|
|
9
|
+
* of the way of non-interactive, CI, quiet, and JSON output.
|
|
10
|
+
*/
|
|
11
|
+
/** Canonical GitHub repo the nudge points at. */
|
|
12
|
+
export declare const REPO_URL = "https://github.com/phnx-labs/agents-cli";
|
|
13
|
+
/** Has the star nudge already been shown on this machine? */
|
|
14
|
+
export declare function hasShownStarNudge(): boolean;
|
|
15
|
+
/** Inputs to the pure show/skip decision (kept side-effect free for testing). */
|
|
16
|
+
export interface StarNudgeContext {
|
|
17
|
+
/** Caller asked for quiet / JSON output. */
|
|
18
|
+
quiet?: boolean;
|
|
19
|
+
/** stdout is attached to an interactive terminal. */
|
|
20
|
+
isTTY: boolean;
|
|
21
|
+
/** CI is set in the environment. */
|
|
22
|
+
ci: boolean;
|
|
23
|
+
/** User opted out via AGENTS_NO_NUDGE=1. */
|
|
24
|
+
optedOut: boolean;
|
|
25
|
+
/** The one-time sentinel already exists. */
|
|
26
|
+
alreadyShown: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Pure decision: should the one-time star nudge be shown? Skipped for
|
|
30
|
+
* quiet/JSON output, non-interactive terminals (pipes, redirects), CI, an
|
|
31
|
+
* explicit opt-out, or once it has already been shown.
|
|
32
|
+
*/
|
|
33
|
+
export declare function shouldShowStarNudge(ctx: StarNudgeContext): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Show the one-time star nudge if it hasn't been shown yet and the context is
|
|
36
|
+
* appropriate. Best-effort by contract: never throws, never blocks the run it
|
|
37
|
+
* follows.
|
|
38
|
+
*
|
|
39
|
+
* Skipped when: the caller asked for quiet/JSON output, stdout is not an
|
|
40
|
+
* interactive terminal (pipes, redirects), CI is set, or the user opted out
|
|
41
|
+
* with AGENTS_NO_NUDGE=1.
|
|
42
|
+
*/
|
|
43
|
+
export declare function maybeShowStarNudge(opts?: {
|
|
44
|
+
quiet?: boolean;
|
|
45
|
+
}): void;
|