@phnx-labs/agents-cli 1.22.27 → 1.22.28
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 +49 -0
- package/README.md +62 -26
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +54 -10
- package/dist/commands/hosts.js +4 -3
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +14 -6
- package/dist/commands/projects.js +24 -10
- package/dist/commands/routines.js +31 -4
- package/dist/commands/run-account-picker.d.ts +37 -0
- package/dist/commands/run-account-picker.js +101 -17
- package/dist/commands/secrets.js +86 -2
- package/dist/commands/sessions.d.ts +26 -6
- package/dist/commands/sessions.js +46 -12
- package/dist/commands/teams.js +19 -8
- package/dist/lib/activity.js +45 -99
- package/dist/lib/claude-account-token.js +38 -11
- package/dist/lib/cloud/host.js +1 -0
- package/dist/lib/devices/resolve-target.d.ts +1 -0
- package/dist/lib/devices/resolve-target.js +9 -2
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +2 -2
- package/dist/lib/git.d.ts +9 -3
- package/dist/lib/git.js +39 -22
- package/dist/lib/hooks/profile.d.ts +12 -1
- package/dist/lib/hooks/profile.js +6 -1
- package/dist/lib/hooks.js +4 -1
- package/dist/lib/hosts/dispatch.d.ts +10 -0
- package/dist/lib/hosts/dispatch.js +78 -22
- package/dist/lib/hosts/logs.js +11 -1
- package/dist/lib/hosts/passthrough.d.ts +5 -0
- package/dist/lib/hosts/passthrough.js +5 -2
- package/dist/lib/hosts/progress.d.ts +10 -0
- package/dist/lib/hosts/progress.js +37 -5
- package/dist/lib/hosts/ready.d.ts +41 -2
- package/dist/lib/hosts/ready.js +100 -12
- package/dist/lib/hosts/reconcile.d.ts +2 -1
- package/dist/lib/hosts/reconcile.js +13 -5
- package/dist/lib/hosts/reconnect.js +5 -3
- package/dist/lib/hosts/registry.js +1 -0
- package/dist/lib/hosts/tasks.d.ts +1 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/snapshot.js +15 -2
- package/dist/lib/models.d.ts +18 -11
- package/dist/lib/models.js +19 -15
- package/dist/lib/perf/db.js +16 -3
- package/dist/lib/perf/types.d.ts +12 -1
- package/dist/lib/project-probe.d.ts +22 -3
- package/dist/lib/project-probe.js +105 -17
- package/dist/lib/project-status.d.ts +9 -0
- package/dist/lib/project-status.js +15 -0
- package/dist/lib/refresh.js +22 -11
- package/dist/lib/rotate.d.ts +22 -0
- package/dist/lib/rotate.js +26 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +19 -3
- package/dist/lib/secrets/agent.js +58 -20
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +2 -0
- package/dist/lib/secrets/bundles.d.ts +3 -3
- package/dist/lib/secrets/bundles.js +15 -5
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +4 -0
- package/dist/lib/secrets/session-store.d.ts +7 -0
- package/dist/lib/secrets/session-store.js +21 -0
- package/dist/lib/session/active.d.ts +49 -0
- package/dist/lib/session/active.js +175 -11
- package/dist/lib/session/bash-command.d.ts +16 -0
- package/dist/lib/session/bash-command.js +65 -0
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +32 -15
- package/dist/lib/session/remote-bundle.js +1 -1
- package/dist/lib/staleness/detectors/skills.d.ts +2 -0
- package/dist/lib/staleness/detectors/skills.js +17 -2
- package/dist/lib/staleness/index.d.ts +10 -1
- package/dist/lib/staleness/index.js +28 -3
- package/dist/lib/teams/agents.d.ts +1 -0
- package/dist/lib/teams/agents.js +38 -11
- package/dist/lib/teams/remoteWorktree.d.ts +11 -7
- package/dist/lib/teams/remoteWorktree.js +29 -27
- package/dist/lib/versions.d.ts +1 -0
- package/dist/lib/versions.js +40 -22
- package/package.json +3 -1
- package/dist/lib/exec.bench.d.ts +0 -1
- package/dist/lib/exec.bench.js +0 -186
package/dist/lib/activity.js
CHANGED
|
@@ -26,6 +26,7 @@ import { getActivityDir, getUserAgentsDir } from './state.js';
|
|
|
26
26
|
import { normalizeHost } from './machine-id.js';
|
|
27
27
|
import { projectKeyFromCwd } from './project-key.js';
|
|
28
28
|
import { stampProvenance } from './event-provenance.js';
|
|
29
|
+
import { pythonToolRegistryLiteral, pythonValueFlagsLiteral, } from './session/bash-command.js';
|
|
29
30
|
/** The set of milestone events, for tier classification and ordering. */
|
|
30
31
|
export const MILESTONE_EVENTS = [
|
|
31
32
|
'plan.created',
|
|
@@ -730,115 +731,30 @@ def first_line(text, limit=140):
|
|
|
730
731
|
return ""
|
|
731
732
|
|
|
732
733
|
|
|
733
|
-
# Canonical command taxonomy for Bash tool calls
|
|
734
|
-
#
|
|
734
|
+
# Canonical command taxonomy for Bash tool calls, generated from the single
|
|
735
|
+
# source of truth in lib/session/bash-command.ts's TOOL_REGISTRY (#1889) so the
|
|
736
|
+
# two can no longer drift — this hook runs standalone under python3 and can't
|
|
737
|
+
# import that module, so the literal below is generated, not hand-duplicated.
|
|
735
738
|
BASH_TOOL_REGISTRY = {
|
|
736
|
-
|
|
737
|
-
"gh": {"category": "vcs", "action": "using GitHub CLI"},
|
|
738
|
-
"bun": {"category": "build-test", "action": "running bun"},
|
|
739
|
-
"npm": {"category": "build-test", "action": "running npm"},
|
|
740
|
-
"pnpm": {"category": "build-test", "action": "running pnpm"},
|
|
741
|
-
"yarn": {"category": "build-test", "action": "running yarn"},
|
|
742
|
-
"vitest": {"category": "build-test", "action": "running vitest"},
|
|
743
|
-
"jest": {"category": "build-test", "action": "running jest"},
|
|
744
|
-
"mocha": {"category": "build-test", "action": "running mocha"},
|
|
745
|
-
"pytest": {"category": "build-test", "action": "running pytest"},
|
|
746
|
-
"cargo": {"category": "build-test", "action": "running cargo"},
|
|
747
|
-
"go": {"category": "build-test", "action": "running go"},
|
|
748
|
-
"tsc": {"category": "build-test", "action": "running tsc"},
|
|
749
|
-
"tsx": {"category": "build-test", "action": "running tsx"},
|
|
750
|
-
"node": {"category": "build-test", "action": "running node"},
|
|
751
|
-
"python": {"category": "build-test", "action": "running python"},
|
|
752
|
-
"python3": {"category": "build-test", "action": "running python"},
|
|
753
|
-
"make": {"category": "build-test", "action": "running make"},
|
|
754
|
-
"brew": {"category": "install", "action": "installing with brew"},
|
|
755
|
-
"pip": {"category": "install", "action": "installing with pip"},
|
|
756
|
-
"pip3": {"category": "install", "action": "installing with pip"},
|
|
757
|
-
"apt": {"category": "install", "action": "installing with apt"},
|
|
758
|
-
"apk": {"category": "install", "action": "installing with apk"},
|
|
759
|
-
"ssh": {"category": "remote", "action": "using ssh"},
|
|
760
|
-
"scp": {"category": "remote", "action": "using scp"},
|
|
761
|
-
"rsync": {"category": "remote", "action": "using rsync"},
|
|
762
|
-
"curl": {"category": "http", "action": "fetching with curl"},
|
|
763
|
-
"wget": {"category": "http", "action": "fetching with wget"},
|
|
764
|
-
"ffmpeg": {"category": "media", "action": "using ffmpeg"},
|
|
765
|
-
"ffprobe": {"category": "media", "action": "probing media"},
|
|
766
|
-
"magick": {"category": "media", "action": "using ImageMagick"},
|
|
767
|
-
"convert": {"category": "media", "action": "converting images"},
|
|
768
|
-
"composite": {"category": "media", "action": "compositing images"},
|
|
769
|
-
"montage": {"category": "media", "action": "montaging images"},
|
|
770
|
-
"identify": {"category": "media", "action": "identifying images"},
|
|
771
|
-
"realesrgan": {"category": "upscaling", "action": "upscaling with realesrgan"},
|
|
772
|
-
"realesrgan-ncnn-vulkan": {"category": "upscaling", "action": "upscaling with realesrgan"},
|
|
773
|
-
"waifu2x": {"category": "upscaling", "action": "upscaling with waifu2x"},
|
|
774
|
-
"waifu2x-caffe": {"category": "upscaling", "action": "upscaling with waifu2x"},
|
|
775
|
-
"waifu2x-converter-cpp": {"category": "upscaling", "action": "upscaling with waifu2x"},
|
|
776
|
-
"swin2sr": {"category": "upscaling", "action": "upscaling with swin2sr"},
|
|
777
|
-
"resdet": {"category": "upscaling", "action": "detecting upscale"},
|
|
778
|
-
"id3v2": {"category": "metadata", "action": "editing id3 tags"},
|
|
779
|
-
"exiftool": {"category": "metadata", "action": "editing exif metadata"},
|
|
780
|
-
"metaflac": {"category": "metadata", "action": "editing flac metadata"},
|
|
781
|
-
"vorbiscomment": {"category": "metadata", "action": "editing vorbis comments"},
|
|
782
|
-
# Shell
|
|
783
|
-
"rm": {"category": "shell", "action": "removing files"},
|
|
784
|
-
"mv": {"category": "shell", "action": "moving files"},
|
|
785
|
-
"cp": {"category": "shell", "action": "copying files"},
|
|
786
|
-
"mkdir": {"category": "shell", "action": "making directories"},
|
|
787
|
-
"touch": {"category": "shell", "action": "touching files"},
|
|
788
|
-
"echo": {"category": "shell", "action": "echoing"},
|
|
789
|
-
"printf": {"category": "shell", "action": "printing"},
|
|
790
|
-
"chmod": {"category": "shell", "action": "changing permissions"},
|
|
791
|
-
"ln": {"category": "shell", "action": "linking files"},
|
|
792
|
-
"awk": {"category": "shell", "action": "running awk"},
|
|
793
|
-
"sed": {"category": "shell", "action": "running sed"},
|
|
794
|
-
"tee": {"category": "shell", "action": "teeing output"},
|
|
795
|
-
"xargs": {"category": "shell", "action": "running xargs"},
|
|
796
|
-
# Probes
|
|
797
|
-
"ls": {"category": "probe", "action": "listing files"},
|
|
798
|
-
"cat": {"category": "probe", "action": "reading files"},
|
|
799
|
-
"head": {"category": "probe", "action": "reading files"},
|
|
800
|
-
"tail": {"category": "probe", "action": "reading files"},
|
|
801
|
-
"wc": {"category": "probe", "action": "counting"},
|
|
802
|
-
"stat": {"category": "probe", "action": "statting files"},
|
|
803
|
-
"file": {"category": "probe", "action": "inspecting files"},
|
|
804
|
-
"which": {"category": "probe", "action": "locating binaries"},
|
|
805
|
-
"tree": {"category": "probe", "action": "listing files"},
|
|
806
|
-
"pwd": {"category": "probe", "action": "printing pwd"},
|
|
807
|
-
# Search
|
|
808
|
-
"grep": {"category": "search", "action": "searching"},
|
|
809
|
-
"rg": {"category": "search", "action": "searching with ripgrep"},
|
|
810
|
-
"ag": {"category": "search", "action": "searching with the silver searcher"},
|
|
811
|
-
"fd": {"category": "search", "action": "searching files"},
|
|
812
|
-
"find": {"category": "search", "action": "finding files"},
|
|
813
|
-
# Wait
|
|
814
|
-
"sleep": {"category": "wait", "action": "sleeping"},
|
|
815
|
-
"wait": {"category": "wait", "action": "waiting"},
|
|
739
|
+
${pythonToolRegistryLiteral()}
|
|
816
740
|
}
|
|
817
741
|
|
|
818
742
|
# Two-level tools mapped to the flags that consume the following token as their
|
|
819
|
-
# value, per tool; the subcommand scan skips both.
|
|
820
|
-
#
|
|
821
|
-
# tool with no such leading flags maps to an empty set. Mirrors VALUE_FLAGS in
|
|
822
|
-
# lib/session/bash-command.ts; TWO_LEVEL_TOOLS is derived from its keys.
|
|
743
|
+
# value, per tool; the subcommand scan skips both. Generated from VALUE_FLAGS in
|
|
744
|
+
# lib/session/bash-command.ts (#1889) — TWO_LEVEL_TOOLS is derived from its keys.
|
|
823
745
|
VALUE_FLAGS = {
|
|
824
|
-
|
|
825
|
-
"gh": {"-R", "--repo"},
|
|
826
|
-
"bun": {"--cwd"},
|
|
827
|
-
"npm": {"--prefix", "-w", "--workspace"},
|
|
828
|
-
"pnpm": {"--filter", "-C", "--dir"},
|
|
829
|
-
"yarn": {"--cwd"},
|
|
830
|
-
"cargo": {"--manifest-path"},
|
|
831
|
-
"docker": {"-H", "--host", "-c", "--context", "--config", "-l", "--log-level"},
|
|
832
|
-
"kubectl": {"-n", "--namespace", "--kubeconfig", "--context", "--cluster", "--user", "-s", "--server", "--as", "--token", "--cache-dir", "--request-timeout"},
|
|
833
|
-
"rush": set(),
|
|
834
|
-
"openclaw": set(),
|
|
746
|
+
${pythonValueFlagsLiteral()}
|
|
835
747
|
}
|
|
836
748
|
|
|
837
749
|
TWO_LEVEL_TOOLS = set(VALUE_FLAGS.keys())
|
|
838
750
|
|
|
839
751
|
|
|
840
752
|
def _unwrap_command(cmd):
|
|
841
|
-
"""Strip wrapper prefixes so the real executable is classified.
|
|
753
|
+
"""Strip wrapper prefixes so the real executable is classified.
|
|
754
|
+
|
|
755
|
+
Mirrors unwrapCommand() in lib/session/bash-command.ts — keep the shapes
|
|
756
|
+
aligned when extending either side (#1889).
|
|
757
|
+
"""
|
|
842
758
|
s = (cmd or "").strip()
|
|
843
759
|
ssh = re.match(r'^ssh\s+\S+\s+["\']?(.+?)["\']?\s*(?:\|.*)?$', s)
|
|
844
760
|
if ssh:
|
|
@@ -847,16 +763,46 @@ def _unwrap_command(cmd):
|
|
|
847
763
|
env = re.match(r'^([A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|' + r"'[^']*'" + r'|\S+)\s+)+(.+)$', s)
|
|
848
764
|
if env:
|
|
849
765
|
return _unwrap_command(env.group(2))
|
|
766
|
+
# export VAR=value && command / export A=1 B=2; command
|
|
767
|
+
export_prefix = re.match(
|
|
768
|
+
r'^export\s+(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|' + r"'[^']*'" + r'|\S+)\s+)*'
|
|
769
|
+
r'[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|' + r"'[^']*'" + r'|\S+)\s*(?:&&|;|\n)\s*([\s\S]+)$',
|
|
770
|
+
s,
|
|
771
|
+
)
|
|
772
|
+
if export_prefix:
|
|
773
|
+
return _unwrap_command(export_prefix.group(1))
|
|
850
774
|
# sudo / time prefix (value-taking flags such as -u user consume their argument)
|
|
851
775
|
prefix = re.match(r'^(?:sudo|time)(?:\s+(?:-[uUgGhpCrtDR]\s+\S+|-\S+))*\s+(.+)$', s)
|
|
852
776
|
if prefix:
|
|
853
777
|
return _unwrap_command(prefix.group(1))
|
|
854
|
-
|
|
778
|
+
# set -euo pipefail && command / set -x; command
|
|
779
|
+
set_prefix = re.match(r'^set\s+[^&;\n]+?(?:&&|;|\n)\s*([\s\S]+)$', s)
|
|
780
|
+
if set_prefix:
|
|
781
|
+
return _unwrap_command(set_prefix.group(1))
|
|
782
|
+
# cd foo && command (also ; and newline separators)
|
|
783
|
+
cd = re.match(r'^cd\s+\S+\s*(?:&&|;|\n)\s*([\s\S]+)$', s)
|
|
855
784
|
if cd:
|
|
856
785
|
return _unwrap_command(cd.group(1))
|
|
857
786
|
npx = re.match(r'^(?:npx|bunx)\s+(?:-\S+\s+)*(.+)$', s)
|
|
858
787
|
if npx:
|
|
859
788
|
return _unwrap_command(npx.group(1))
|
|
789
|
+
for_loop = re.match(r'^for\s+[\s\S]+?\bdo\b\s*([\s\S]+?)\s*;?\s*done\b[\s\S]*$', s)
|
|
790
|
+
if for_loop:
|
|
791
|
+
return _unwrap_command(for_loop.group(1))
|
|
792
|
+
until_loop = re.match(r'^until\s+[\s\S]+?\bdo\b\s*([\s\S]+?)\s*;?\s*done\b[\s\S]*$', s)
|
|
793
|
+
if until_loop:
|
|
794
|
+
return _unwrap_command(until_loop.group(1))
|
|
795
|
+
if_cond = re.match(
|
|
796
|
+
r'^if\s+[\s\S]+?\bthen\b\s*([\s\S]+?)\s*;?\s*(?:elif\b|else\b|fi\b)[\s\S]*$',
|
|
797
|
+
s,
|
|
798
|
+
)
|
|
799
|
+
if if_cond:
|
|
800
|
+
return _unwrap_command(if_cond.group(1))
|
|
801
|
+
subshell = re.match(r'^\(\s*([\s\S]+?)\s*\)\s*(?:&&|;|\|\|)[\s\S]*$', s) or re.match(
|
|
802
|
+
r'^\(\s*([\s\S]+?)\s*\)\s*$', s
|
|
803
|
+
)
|
|
804
|
+
if subshell:
|
|
805
|
+
return _unwrap_command(subshell.group(1))
|
|
860
806
|
return s
|
|
861
807
|
|
|
862
808
|
|
|
@@ -2,6 +2,8 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { bundleBackend, bundleExists, readAndResolveBundleEnv } from './secrets/bundles.js';
|
|
5
|
+
import { fileStoreItemPath } from './secrets/filestore.js';
|
|
6
|
+
import { secretsKeychainItem } from './secrets/index.js';
|
|
5
7
|
/**
|
|
6
8
|
* Reserved FILE-BASED secrets bundle holding long-lived, non-rotating Claude
|
|
7
9
|
* setup-tokens. Usage/probe reads authenticate with these instead of Claude
|
|
@@ -25,6 +27,17 @@ const AUTH_BUNDLE = 'auth';
|
|
|
25
27
|
* where a corrupt bundle entry must be caught before it reaches the auth header.
|
|
26
28
|
*/
|
|
27
29
|
const SETUP_TOKEN_RE = /^sk-ant-oat01-[A-Za-z0-9_-]+$/;
|
|
30
|
+
/** Process-local only: plaintext setup-tokens are never written to another cache. */
|
|
31
|
+
const setupTokenCache = new Map();
|
|
32
|
+
function credentialFingerprint(credentialPath) {
|
|
33
|
+
try {
|
|
34
|
+
const stat = fs.statSync(credentialPath, { bigint: true });
|
|
35
|
+
return `${stat.dev}:${stat.ino}:${stat.ctimeNs}:${stat.mtimeNs}:${stat.size}`;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return 'missing';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
28
41
|
/** True only for a clean, single-line `sk-ant-oat01-…` token — see {@link SETUP_TOKEN_RE}. */
|
|
29
42
|
export function isValidClaudeSetupToken(value) {
|
|
30
43
|
return SETUP_TOKEN_RE.test(value);
|
|
@@ -71,17 +84,31 @@ export function resolveClaudeSetupToken(home) {
|
|
|
71
84
|
return null;
|
|
72
85
|
if (!bundleExists(AUTH_BUNDLE) || bundleBackend(AUTH_BUNDLE) !== 'file')
|
|
73
86
|
return null;
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
const cacheKey = home ?? os.homedir();
|
|
88
|
+
const item = secretsKeychainItem(AUTH_BUNDLE, claudeAccountTokenKey(email));
|
|
89
|
+
const credentialPath = fileStoreItemPath(item);
|
|
90
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
91
|
+
const before = credentialFingerprint(credentialPath);
|
|
92
|
+
const cached = setupTokenCache.get(cacheKey);
|
|
93
|
+
if (cached?.credentialPath === credentialPath && cached.fingerprint === before) {
|
|
94
|
+
return cached.token;
|
|
95
|
+
}
|
|
96
|
+
if (before === 'missing') {
|
|
97
|
+
setupTokenCache.set(cacheKey, { credentialPath, fingerprint: before, token: null });
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const { env } = readAndResolveBundleEnv(AUTH_BUNDLE, { caller: 'usage', agentOnly: true });
|
|
101
|
+
const v = (env[claudeAccountTokenKey(email)] ?? '').trim();
|
|
102
|
+
const token = v.length > 0 && isValidClaudeSetupToken(v) ? v : null;
|
|
103
|
+
const after = credentialFingerprint(credentialPath);
|
|
104
|
+
if (before === after) {
|
|
105
|
+
setupTokenCache.set(cacheKey, { credentialPath, fingerprint: after, token });
|
|
106
|
+
return token;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// The credential changed during both decrypt attempts. Fail closed rather
|
|
110
|
+
// than return a token whose current file fingerprint was never observed.
|
|
111
|
+
return null;
|
|
85
112
|
}
|
|
86
113
|
catch {
|
|
87
114
|
return null;
|
package/dist/lib/cloud/host.js
CHANGED
|
@@ -153,6 +153,7 @@ export class HostCloudProvider {
|
|
|
153
153
|
remoteExit: task.remoteExit,
|
|
154
154
|
taskId: task.id,
|
|
155
155
|
offset,
|
|
156
|
+
extraSshArgs: task.identityFile ? ['-i', task.identityFile, '-o', 'IdentitiesOnly=yes'] : [],
|
|
156
157
|
});
|
|
157
158
|
if (fetched) {
|
|
158
159
|
if (fetched.logChunk.length > 0) {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* `user@host` all resolve identically no matter which subcommand called.
|
|
19
19
|
*/
|
|
20
20
|
import chalk from 'chalk';
|
|
21
|
-
import { sshTargetFor } from '../hosts/types.js';
|
|
21
|
+
import { hostIdentityArgs, sshTargetFor } from '../hosts/types.js';
|
|
22
22
|
import { matchHost, splitUserHost } from '../hosts/registry.js';
|
|
23
23
|
import { normalizeHost } from '../machine-id.js';
|
|
24
24
|
import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
@@ -61,7 +61,14 @@ async function toResolvedTarget(token) {
|
|
|
61
61
|
}
|
|
62
62
|
const hostPart = host.device ? host.device.name : splitUserHost(host.name).host;
|
|
63
63
|
const name = host.device ? host.device.name : host.name;
|
|
64
|
-
|
|
64
|
+
const extraSshArgs = hostIdentityArgs(host);
|
|
65
|
+
return {
|
|
66
|
+
target,
|
|
67
|
+
machine: normalizeHost(hostPart),
|
|
68
|
+
name,
|
|
69
|
+
os: host.os ?? resolveRemoteOsSync(name),
|
|
70
|
+
...(extraSshArgs.length > 0 ? { extraSshArgs } : {}),
|
|
71
|
+
};
|
|
65
72
|
}
|
|
66
73
|
/**
|
|
67
74
|
* Resolve a target token to a full {@link DeviceProfile} for `agents ssh`. Same
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { ActorKind } from './actor.js';
|
|
15
15
|
export type EventLevel = 'audit' | 'warn' | 'info' | 'debug';
|
|
16
|
-
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'computer.action' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
16
|
+
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'computer.action' | 'secrets.get' | 'secrets.unlocked' | 'secrets.create' | 'secrets.import' | 'secrets.export' | 'secrets.view' | 'secrets.lease-denied' | 'secrets.lease-expire' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'cloud.cancel' | 'cloud.message' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'teams.disband' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'mcp.add' | 'mcp.remove' | 'mcp.register' | 'resource.sync' | 'rotation.resolved' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'webhook.received' | 'webhook.authorized' | 'webhook.rejected' | 'webhook.matched' | 'webhook.fired' | 'webhook.handler.start' | 'webhook.handler.end' | 'plan.created' | 'pr.opened' | 'pr.merged' | 'worktree.created' | 'worktree.removed' | 'commit.created' | 'pushed' | 'subagent.spawned' | 'artifact.created' | 'task.completed' | 'checklist.created' | 'status.posted' | 'file.edited' | 'factory.command' | 'factory.action' | 'factory.uri' | 'factory.launch' | 'friction' | 'error' | 'warn' | 'info' | 'debug';
|
|
17
17
|
/** Every known event kind. Derived from {@link EVENT_TYPE_TABLE}, never hand-listed. */
|
|
18
18
|
export declare const EVENT_TYPES: readonly EventType[];
|
|
19
19
|
/** Runtime guard for an event kind arriving from outside this process. */
|
package/dist/lib/events.js
CHANGED
|
@@ -113,7 +113,7 @@ const EVENT_TYPE_TABLE = {
|
|
|
113
113
|
'skill.install': true, 'skill.remove': true,
|
|
114
114
|
'browser.launch': true, 'browser.close': true, 'browser.navigate': true, 'browser.screenshot': true,
|
|
115
115
|
'computer.action': true,
|
|
116
|
-
'secrets.get': true, 'secrets.unlocked': true, 'secrets.create': true, 'secrets.import': true, 'secrets.export': true, 'secrets.view': true, 'secrets.set': true, 'secrets.delete': true, 'secrets.rename': true,
|
|
116
|
+
'secrets.get': true, 'secrets.unlocked': true, 'secrets.create': true, 'secrets.import': true, 'secrets.export': true, 'secrets.view': true, 'secrets.lease-denied': true, 'secrets.lease-expire': true, 'secrets.set': true, 'secrets.delete': true, 'secrets.rename': true,
|
|
117
117
|
'cloud.dispatch': true, 'cloud.complete': true, 'cloud.cancel': true, 'cloud.message': true,
|
|
118
118
|
'teams.create': true, 'teams.add': true, 'teams.start': true, 'teams.complete': true, 'teams.disband': true,
|
|
119
119
|
'hook.fire': true, 'hook.complete': true, 'hook.error': true,
|
|
@@ -141,7 +141,7 @@ export function isEventType(value) {
|
|
|
141
141
|
}
|
|
142
142
|
const AUDIT_EVENTS = new Set([
|
|
143
143
|
'command.start', 'command.end',
|
|
144
|
-
'secrets.get', 'secrets.unlocked', 'secrets.create', 'secrets.import', 'secrets.export', 'secrets.view',
|
|
144
|
+
'secrets.get', 'secrets.unlocked', 'secrets.create', 'secrets.import', 'secrets.export', 'secrets.view', 'secrets.lease-denied', 'secrets.lease-expire',
|
|
145
145
|
'secrets.set', 'secrets.delete', 'secrets.rename',
|
|
146
146
|
'teams.create', 'teams.add', 'teams.start', 'teams.complete', 'teams.disband',
|
|
147
147
|
'cloud.dispatch', 'cloud.complete', 'cloud.cancel', 'cloud.message',
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -269,9 +269,15 @@ export declare function displayHomePath(dir: string): string;
|
|
|
269
269
|
* Pull changes in an existing repo.
|
|
270
270
|
* Refuses to pull if the working tree is dirty -- user must commit or discard changes first.
|
|
271
271
|
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
272
|
+
* Strategy (RUSH-2282):
|
|
273
|
+
* 1. Fetch, then compare HEAD to the resolved tracking ref.
|
|
274
|
+
* 2. Clean behind-only (HEAD is an ancestor of tracking) → `merge --ff-only`
|
|
275
|
+
* against the tracking ref. Never re-enter `git pull` after a bare fetch —
|
|
276
|
+
* multi-entry FETCH_HEAD (concurrent fetch, multi-branch remote) makes
|
|
277
|
+
* `git pull --rebase` die with "Cannot rebase onto multiple branches" even
|
|
278
|
+
* when the checkout is a pure fast-forward.
|
|
279
|
+
* 3. Genuinely diverged (local commits not on tracking) → `rebase` onto the
|
|
280
|
+
* tracking ref (same outcome as {@link syncRepoGit}, without a second pull).
|
|
275
281
|
*/
|
|
276
282
|
export declare function pullRepo(dir: string): Promise<{
|
|
277
283
|
success: boolean;
|
package/dist/lib/git.js
CHANGED
|
@@ -867,9 +867,15 @@ export function displayHomePath(dir) {
|
|
|
867
867
|
* Pull changes in an existing repo.
|
|
868
868
|
* Refuses to pull if the working tree is dirty -- user must commit or discard changes first.
|
|
869
869
|
*
|
|
870
|
-
*
|
|
871
|
-
*
|
|
872
|
-
*
|
|
870
|
+
* Strategy (RUSH-2282):
|
|
871
|
+
* 1. Fetch, then compare HEAD to the resolved tracking ref.
|
|
872
|
+
* 2. Clean behind-only (HEAD is an ancestor of tracking) → `merge --ff-only`
|
|
873
|
+
* against the tracking ref. Never re-enter `git pull` after a bare fetch —
|
|
874
|
+
* multi-entry FETCH_HEAD (concurrent fetch, multi-branch remote) makes
|
|
875
|
+
* `git pull --rebase` die with "Cannot rebase onto multiple branches" even
|
|
876
|
+
* when the checkout is a pure fast-forward.
|
|
877
|
+
* 3. Genuinely diverged (local commits not on tracking) → `rebase` onto the
|
|
878
|
+
* tracking ref (same outcome as {@link syncRepoGit}, without a second pull).
|
|
873
879
|
*/
|
|
874
880
|
export async function pullRepo(dir) {
|
|
875
881
|
try {
|
|
@@ -933,16 +939,16 @@ export async function pullRepo(dir) {
|
|
|
933
939
|
tracking = `origin/${branch}`;
|
|
934
940
|
}
|
|
935
941
|
}
|
|
936
|
-
// Split the remote-tracking ref (<remote>/<branch>)
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
//
|
|
940
|
-
//
|
|
941
|
-
// nothing" failure this change exists to remove. Branch names may contain
|
|
942
|
-
// slashes, so split on the FIRST separator only.
|
|
942
|
+
// Split the remote-tracking ref (<remote>/<branch>) so callers/logs can name
|
|
943
|
+
// the remote. Branch names may contain slashes, so split on the FIRST
|
|
944
|
+
// separator only. The integrate step below uses `tracking` directly (not a
|
|
945
|
+
// second `git pull <remote> <branch>`) so a multi-entry FETCH_HEAD cannot
|
|
946
|
+
// turn a clean fast-forward into "Cannot rebase onto multiple branches".
|
|
943
947
|
const sep = tracking.indexOf('/');
|
|
944
|
-
const remoteName = sep > 0 ? tracking.slice(0, sep) : 'origin';
|
|
945
948
|
const remoteBranch = sep > 0 ? tracking.slice(sep + 1) : branch;
|
|
949
|
+
// Keep branch-name validation on the ref we would have passed to pull —
|
|
950
|
+
// rejects traversal / flag-smuggling shapes before any integrate command.
|
|
951
|
+
assertValidBranchName(remoteBranch);
|
|
946
952
|
// Bare fetch: updates every remote, so the revparse below sees a fresh ref
|
|
947
953
|
// whichever one the branch tracks. Deliberately argument-less — simple-git's
|
|
948
954
|
// fetchTask only forwards a remote when BOTH remote and branch are passed,
|
|
@@ -962,17 +968,27 @@ export async function pullRepo(dir) {
|
|
|
962
968
|
branch,
|
|
963
969
|
};
|
|
964
970
|
}
|
|
971
|
+
// Behind-only when tracking has commits we lack AND we have none of our own
|
|
972
|
+
// on top. Use rev-list counts — simple-git does not reject on the exit-1
|
|
973
|
+
// that `merge-base --is-ancestor` returns for a non-ancestor, so a try/catch
|
|
974
|
+
// around that call would always report "can ff" (RUSH-2282).
|
|
975
|
+
const aheadCount = parseInt((await git.raw(['rev-list', '--count', `${tracking}..HEAD`])).trim(), 10);
|
|
976
|
+
const behindCount = parseInt((await git.raw(['rev-list', '--count', `HEAD..${tracking}`])).trim(), 10);
|
|
977
|
+
const canFastForward = Number.isFinite(aheadCount) &&
|
|
978
|
+
Number.isFinite(behindCount) &&
|
|
979
|
+
aheadCount === 0 &&
|
|
980
|
+
behindCount > 0;
|
|
965
981
|
try {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
982
|
+
if (canFastForward) {
|
|
983
|
+
// Integrate the already-fetched tracking ref. No network, no FETCH_HEAD.
|
|
984
|
+
await git.raw(['merge', '--ff-only', tracking]);
|
|
985
|
+
}
|
|
986
|
+
else {
|
|
987
|
+
// Diverged (or local-only commits). Rebase onto the tracking tip —
|
|
988
|
+
// same outcome as `git pull --rebase <remote> <branch>` without
|
|
989
|
+
// re-fetching or consulting FETCH_HEAD.
|
|
990
|
+
await git.raw(['rebase', tracking]);
|
|
991
|
+
}
|
|
976
992
|
}
|
|
977
993
|
catch (err) {
|
|
978
994
|
// Abort so the tree is restored, matching the atomicity --ff-only gave us.
|
|
@@ -982,10 +998,11 @@ export async function pullRepo(dir) {
|
|
|
982
998
|
// the cause. `agents sync` reaches this path unattended across the fleet,
|
|
983
999
|
// so a wedged checkout would be worse than the bug this fixes.
|
|
984
1000
|
await git.raw(['rebase', '--abort']).catch(() => { });
|
|
1001
|
+
const verb = canFastForward ? 'Fast-forward' : 'Rebase';
|
|
985
1002
|
return {
|
|
986
1003
|
success: false,
|
|
987
1004
|
commit: '',
|
|
988
|
-
error:
|
|
1005
|
+
error: `${verb} onto ${tracking} failed — the pull was rolled back, nothing changed.\n\nResolve the divergence, then pull again:\n\n cd ${displayHomePath(dir)} && git log --oneline HEAD...${tracking}\n\n${err.message}`,
|
|
989
1006
|
};
|
|
990
1007
|
}
|
|
991
1008
|
installGithooksSymlinks(dir);
|
|
@@ -10,8 +10,19 @@ export interface HookProfileRow {
|
|
|
10
10
|
cacheStalePct: number;
|
|
11
11
|
cacheMissPct: number;
|
|
12
12
|
errorCount: number;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Fraction (0-1) of fires with a real crash exit (exit 1 / other nonzero
|
|
15
|
+
* except intentional PreToolUse deny code 2). Exit 2 is blockRate.
|
|
16
|
+
*/
|
|
14
17
|
errorRate?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Count of intentional deny/block exits (PreToolUse exit 2). Deny-by-design
|
|
20
|
+
* guards (ask-user-question-guard, git-guard, plan-html-reminder) use this
|
|
21
|
+
* path — not a crash.
|
|
22
|
+
*/
|
|
23
|
+
blockCount: number;
|
|
24
|
+
/** Fraction (0-1) of fires with exit code 2 (intentional deny/block). */
|
|
25
|
+
blockRate?: number;
|
|
15
26
|
/** Fraction (0-1) of fires that hit their configured timeout. */
|
|
16
27
|
timeoutRate?: number;
|
|
17
28
|
/** Project key the row is scoped to (see project-key.ts) — set only when
|
|
@@ -72,7 +72,10 @@ export function aggregateHookProfile(events) {
|
|
|
72
72
|
const hits = evs.filter(e => e.cache === 'hit').length;
|
|
73
73
|
const stale = evs.filter(e => e.cache === 'stale-prefetch').length;
|
|
74
74
|
const misses = evs.filter(e => e.cache === 'miss').length;
|
|
75
|
-
|
|
75
|
+
// Exit classes (Claude/Codex PreToolUse convention): 0 allow, 2 deny/block,
|
|
76
|
+
// 1 / other nonzero = real error. Exit 2 is not a crash.
|
|
77
|
+
const blocks = evs.filter(e => e.exit === 2).length;
|
|
78
|
+
const errors = evs.filter(e => typeof e.exit === 'number' && e.exit !== 0 && e.exit !== 2).length;
|
|
76
79
|
rows.push({
|
|
77
80
|
hook,
|
|
78
81
|
n,
|
|
@@ -85,7 +88,9 @@ export function aggregateHookProfile(events) {
|
|
|
85
88
|
cacheStalePct: Math.round((stale / n) * 100),
|
|
86
89
|
cacheMissPct: Math.round((misses / n) * 100),
|
|
87
90
|
errorCount: errors,
|
|
91
|
+
blockCount: blocks,
|
|
88
92
|
...(errors > 0 ? { errorRate: Math.round((errors / n) * 1000) / 1000 } : {}),
|
|
93
|
+
...(blocks > 0 ? { blockRate: Math.round((blocks / n) * 1000) / 1000 } : {}),
|
|
89
94
|
// timeoutRate is not derivable here: the daily JSONL a shim writes only
|
|
90
95
|
// covers fires that reached their own trailing printf — an externally
|
|
91
96
|
// enforced timeout (the agent harness killing the process) never gets
|
package/dist/lib/hooks.js
CHANGED
|
@@ -1610,6 +1610,7 @@ function registerHooksForOpenCode(versionHome, manifest, resolveScript) {
|
|
|
1610
1610
|
const pluginSource = `// Generated by agents-cli. Re-run agents sync to update.
|
|
1611
1611
|
import fs from "node:fs"
|
|
1612
1612
|
import os from "node:os"
|
|
1613
|
+
import path from "node:path"
|
|
1613
1614
|
|
|
1614
1615
|
const directHooks = ${serializedDirect}
|
|
1615
1616
|
const lifecycleHooks = ${serializedLifecycle}
|
|
@@ -1622,7 +1623,9 @@ function recordTimeoutSample(hook, payload) {
|
|
|
1622
1623
|
// printf, so a timed-out fire would otherwise leave ZERO trace in the
|
|
1623
1624
|
// warehouse. Write the sample ourselves from the side that knows it timed out.
|
|
1624
1625
|
try {
|
|
1625
|
-
|
|
1626
|
+
// path.dirname — not lastIndexOf('/') — so Windows backslash spool paths
|
|
1627
|
+
// still resolve to the parent dir (see #1869).
|
|
1628
|
+
fs.mkdirSync(path.dirname(PERF_SPOOL), { recursive: true })
|
|
1626
1629
|
const line = JSON.stringify({
|
|
1627
1630
|
ts_ms: Date.now(),
|
|
1628
1631
|
kind: "hook.fire",
|
|
@@ -74,6 +74,15 @@ export declare function remoteRunShellPrelude(agent: string): string;
|
|
|
74
74
|
* reliable whole-tree operation for both normal stops and rollback cleanup.
|
|
75
75
|
*/
|
|
76
76
|
export declare function buildDetachedLaunchCommand(inner: string): string;
|
|
77
|
+
/** Build the detached PowerShell launch protocol used by Windows SSH hosts. */
|
|
78
|
+
export declare function buildWindowsDetachedLaunchCommand(opts: {
|
|
79
|
+
forwardedArgs: string[];
|
|
80
|
+
remoteCwd?: string;
|
|
81
|
+
mirrorCwd?: boolean;
|
|
82
|
+
remoteLog: string;
|
|
83
|
+
remoteExit: string;
|
|
84
|
+
env: Record<string, string>;
|
|
85
|
+
}): string;
|
|
77
86
|
export interface DispatchResult {
|
|
78
87
|
task: HostTask;
|
|
79
88
|
/** Exit code when followed; undefined when detached (--no-follow). */
|
|
@@ -92,6 +101,7 @@ export declare function terminateDispatchedTask(task: HostTask): void;
|
|
|
92
101
|
* Exit 1 if the group is still alive after TERM/KILL (can't stop it).
|
|
93
102
|
*/
|
|
94
103
|
export declare function buildStopRemoteCommand(pid: number, remoteExit: string): string;
|
|
104
|
+
export declare function buildWindowsStopRemoteCommand(pid: number, remoteExit: string): string;
|
|
95
105
|
/**
|
|
96
106
|
* Stop a running host task from the origin machine (`agents hosts stop <id>`).
|
|
97
107
|
*
|