@phnx-labs/agents-cli 1.20.78 → 1.20.83
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 +595 -0
- package/README.md +26 -26
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +9 -0
- package/dist/commands/activity.js +153 -24
- package/dist/commands/apply.js +11 -4
- package/dist/commands/cli.js +1 -1
- package/dist/commands/commands.d.ts +1 -1
- package/dist/commands/commands.js +7 -6
- package/dist/commands/defaults.js +3 -11
- package/dist/commands/doctor.d.ts +66 -0
- package/dist/commands/doctor.js +596 -33
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.js +24 -5
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +87 -11
- package/dist/commands/harness.js +2 -2
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/import.js +8 -4
- package/dist/commands/logs.js +6 -0
- package/dist/commands/menubar.js +12 -0
- package/dist/commands/models.js +1 -1
- package/dist/commands/packages.js +6 -6
- package/dist/commands/permissions.js +1 -1
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/pull.d.ts +1 -1
- package/dist/commands/pull.js +4 -4
- package/dist/commands/repo.d.ts +78 -0
- package/dist/commands/repo.js +187 -11
- package/dist/commands/routines.d.ts +8 -1
- package/dist/commands/routines.js +241 -77
- package/dist/commands/rules.js +13 -12
- package/dist/commands/secrets.d.ts +3 -1
- package/dist/commands/secrets.js +100 -32
- package/dist/commands/sessions-inject.d.ts +7 -4
- package/dist/commands/sessions-inject.js +18 -18
- package/dist/commands/sessions-picker.js +32 -3
- package/dist/commands/sessions-resume.d.ts +16 -0
- package/dist/commands/sessions-resume.js +14 -13
- package/dist/commands/sessions.d.ts +115 -0
- package/dist/commands/sessions.js +189 -35
- package/dist/commands/set.d.ts +15 -0
- package/dist/commands/set.js +79 -0
- package/dist/commands/setup-fleet.d.ts +21 -0
- package/dist/commands/setup-fleet.js +201 -0
- package/dist/commands/setup-secrets.d.ts +20 -0
- package/dist/commands/setup-secrets.js +223 -0
- package/dist/commands/setup.js +17 -3
- package/dist/commands/skills.js +1 -1
- package/dist/commands/sync.js +7 -2
- package/dist/commands/teams-picker.js +0 -1
- package/dist/commands/teams.d.ts +33 -1
- package/dist/commands/teams.js +208 -44
- package/dist/commands/versions.js +10 -5
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.js +11 -0
- package/dist/index.js +52 -8
- package/dist/lib/acp/harnesses.js +0 -7
- package/dist/lib/activity.d.ts +156 -0
- package/dist/lib/activity.js +282 -0
- package/dist/lib/agents.d.ts +9 -2
- package/dist/lib/agents.js +72 -63
- package/dist/lib/browser/service.js +3 -0
- package/dist/lib/browser/types.d.ts +7 -0
- package/dist/lib/capabilities.js +6 -2
- package/dist/lib/crabbox/lease.js +2 -2
- package/dist/lib/crabbox/setup-copy.d.ts +4 -4
- package/dist/lib/crabbox/setup-copy.js +4 -4
- package/dist/lib/daemon.d.ts +18 -0
- package/dist/lib/daemon.js +47 -6
- package/dist/lib/devices/fleet.d.ts +1 -1
- package/dist/lib/devices/fleet.js +1 -1
- package/dist/lib/doctor-diff.d.ts +20 -0
- package/dist/lib/doctor-diff.js +6 -1
- package/dist/lib/drift.d.ts +31 -11
- package/dist/lib/drift.js +58 -7
- package/dist/lib/events.d.ts +8 -1
- package/dist/lib/events.js +10 -1
- package/dist/lib/exec.d.ts +7 -0
- package/dist/lib/exec.js +69 -11
- package/dist/lib/feed-post.d.ts +28 -1
- package/dist/lib/feed-post.js +110 -2
- package/dist/lib/fleet/apply.js +8 -1
- package/dist/lib/fleet/auth-sync.d.ts +18 -1
- package/dist/lib/fleet/auth-sync.js +25 -11
- package/dist/lib/fleet/remote-login.js +5 -0
- package/dist/lib/git.d.ts +14 -14
- package/dist/lib/git.js +41 -44
- package/dist/lib/hooks.d.ts +71 -0
- package/dist/lib/hooks.js +245 -95
- package/dist/lib/hosts/dispatch.d.ts +28 -1
- package/dist/lib/hosts/dispatch.js +33 -5
- package/dist/lib/hosts/option.js +2 -2
- package/dist/lib/hosts/passthrough.d.ts +22 -1
- package/dist/lib/hosts/passthrough.js +238 -13
- package/dist/lib/hosts/run-target.d.ts +2 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/hq/floor.d.ts +1 -1
- package/dist/lib/hq/floor.js +6 -1
- package/dist/lib/mcp.js +3 -71
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +24 -0
- package/dist/lib/menubar/install-menubar.js +52 -2
- package/dist/lib/menubar/notify-desktop.d.ts +15 -0
- package/dist/lib/menubar/notify-desktop.js +41 -5
- package/dist/lib/merged-resources.d.ts +11 -0
- package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
- package/dist/lib/permissions.d.ts +0 -32
- package/dist/lib/permissions.js +4 -157
- package/dist/lib/picker.js +1 -1
- package/dist/lib/platform/process.d.ts +16 -5
- package/dist/lib/platform/process.js +54 -0
- package/dist/lib/plugins.d.ts +0 -8
- package/dist/lib/plugins.js +4 -110
- package/dist/lib/project-resources.js +4 -1
- package/dist/lib/refresh.d.ts +5 -3
- package/dist/lib/refresh.js +7 -5
- package/dist/lib/resources/commands.js +4 -1
- package/dist/lib/resources/mcp.js +0 -4
- package/dist/lib/resources/permissions.d.ts +1 -1
- package/dist/lib/resources/permissions.js +1 -5
- package/dist/lib/resources/rules.js +4 -1
- package/dist/lib/resources/skills.js +4 -1
- package/dist/lib/resources/subagents.js +4 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/routines.d.ts +33 -0
- package/dist/lib/routines.js +122 -0
- package/dist/lib/run-defaults.d.ts +1 -0
- package/dist/lib/run-defaults.js +9 -0
- package/dist/lib/runner.d.ts +0 -12
- package/dist/lib/runner.js +49 -10
- package/dist/lib/scheduler.js +16 -4
- 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 +16 -0
- package/dist/lib/secrets/agent.js +105 -3
- package/dist/lib/secrets/audit.d.ts +46 -0
- package/dist/lib/secrets/audit.js +56 -0
- package/dist/lib/secrets/bundles.d.ts +11 -8
- package/dist/lib/secrets/bundles.js +31 -23
- package/dist/lib/secrets/index.d.ts +2 -1
- package/dist/lib/secrets/index.js +8 -2
- package/dist/lib/secrets/remote.d.ts +75 -2
- package/dist/lib/secrets/remote.js +139 -5
- package/dist/lib/secrets/sync.js +3 -3
- package/dist/lib/session/active.d.ts +128 -26
- package/dist/lib/session/active.js +192 -53
- package/dist/lib/session/actor-sidecar.d.ts +23 -0
- package/dist/lib/session/actor-sidecar.js +101 -0
- package/dist/lib/session/db.d.ts +4 -1
- package/dist/lib/session/db.js +65 -8
- package/dist/lib/session/discover.d.ts +4 -0
- package/dist/lib/session/discover.js +13 -0
- package/dist/lib/session/hook-sessions.d.ts +9 -0
- package/dist/lib/session/hook-sessions.js +57 -8
- package/dist/lib/session/pid-registry.d.ts +13 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/types.d.ts +16 -0
- package/dist/lib/staleness/detectors/commands.js +2 -2
- package/dist/lib/staleness/detectors/permissions.js +1 -45
- package/dist/lib/staleness/writers/commands.js +4 -5
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -2
- package/dist/lib/startup/command-registry.js +2 -4
- package/dist/lib/state.js +57 -1
- package/dist/lib/subagents-registry.js +5 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -12
- package/dist/lib/teams/agents.d.ts +0 -2
- package/dist/lib/teams/agents.js +10 -27
- package/dist/lib/teams/index.d.ts +1 -1
- package/dist/lib/teams/index.js +1 -1
- package/dist/lib/types.d.ts +17 -9
- package/dist/lib/usage.d.ts +28 -5
- package/dist/lib/usage.js +271 -8
- package/dist/lib/versions.js +7 -1
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +25 -13
- package/package.json +1 -2
- package/dist/commands/check.d.ts +0 -15
- package/dist/commands/check.js +0 -180
- package/dist/commands/resources.d.ts +0 -5
package/dist/lib/fleet/apply.js
CHANGED
|
@@ -12,7 +12,7 @@ import { sshTargetFor } from '../devices/connect.js';
|
|
|
12
12
|
import { readyProbe, bootstrapAgentsCli } from '../hosts/ready.js';
|
|
13
13
|
import { buildRemoteAgentsInvocation } from '../hosts/remote-cmd.js';
|
|
14
14
|
import { sshExec } from '../ssh-exec.js';
|
|
15
|
-
import { isPropagatableAgent, KEYCHAIN_BOUND_ON_MAC, buildAuthBundle, } from './auth-sync.js';
|
|
15
|
+
import { isPropagatableAgent, hasPortableAuthFiles, isCredentialSafeToPropagate, KEYCHAIN_BOUND_ON_MAC, buildAuthBundle, } from './auth-sync.js';
|
|
16
16
|
/** Strip a version suffix from an agent spec: `claude@latest` -> `claude`. */
|
|
17
17
|
export function agentIdOf(spec) {
|
|
18
18
|
return spec.split('@')[0].trim();
|
|
@@ -148,6 +148,13 @@ export function diffFleet(desired, probes, ctx) {
|
|
|
148
148
|
if (canPushLogin(id, probe.platform, ctx.sourceAuth)) {
|
|
149
149
|
rowActions.push({ device: d.device, kind: 'push-login', agent: id, detail: `propagate ${id} login` });
|
|
150
150
|
}
|
|
151
|
+
else if (hasPortableAuthFiles(id) && !isCredentialSafeToPropagate(id)) {
|
|
152
|
+
// Single-use rotating refresh tokens (e.g. droid/WorkOS) must never be
|
|
153
|
+
// copied across boxes — the first refresh invalidates every other copy.
|
|
154
|
+
// Surface per-machine login guidance instead of silently propagating.
|
|
155
|
+
loginBlocked.push(id);
|
|
156
|
+
rowActions.push({ device: d.device, kind: 'needs-login', agent: id, detail: `${id} login uses a single-use rotating refresh token — log in on ${d.device} itself` });
|
|
157
|
+
}
|
|
151
158
|
else if (isPropagatableAgent(id) &&
|
|
152
159
|
(ctx.sourceAuth.bound.has(id) || (probe.platform === 'macos' && KEYCHAIN_BOUND_ON_MAC.has(id)))) {
|
|
153
160
|
// A login-propagation candidate we still can't push: the source token
|
|
@@ -27,6 +27,23 @@ interface AuthFileSpec {
|
|
|
27
27
|
export declare const FLEET_AUTH_FILES: Record<string, AuthFileSpec[]>;
|
|
28
28
|
/** Agents whose macOS credentials live in the ACL-bound login keychain. */
|
|
29
29
|
export declare const KEYCHAIN_BOUND_ON_MAC: ReadonlySet<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Agents whose OAuth credentials rely on single-use refresh tokens that rotate
|
|
32
|
+
* server-side on every exchange. Copying these credential files across machines
|
|
33
|
+
* is fatal: the first refresh on any box invalidates every other holder's token,
|
|
34
|
+
* collapsing the fleet to a single working login (droid/WorkOS collapsed 10 boxes
|
|
35
|
+
* to 1 overnight — RUSH-1958). Add any newly-discovered single-use-rotation
|
|
36
|
+
* harness here; the predicate below is the one place the propagation decision is
|
|
37
|
+
* made. See also `remote-login.ts` and `usage.ts` for the per-machine-login policy.
|
|
38
|
+
*/
|
|
39
|
+
export declare const SINGLE_USE_ROTATING_REFRESH_AGENTS: ReadonlySet<string>;
|
|
40
|
+
/** True when `agent`'s portable credential file(s) are safe to copy between
|
|
41
|
+
* machines. Single-use rotating refresh tokens are never safe; keychain-bound
|
|
42
|
+
* tokens are handled separately by the caller via {@link KEYCHAIN_BOUND_ON_MAC}. */
|
|
43
|
+
export declare function isCredentialSafeToPropagate(agent: string): boolean;
|
|
44
|
+
/** True when the agent stores credentials in portable files we can read. This
|
|
45
|
+
* does NOT mean it is safe to propagate — check {@link isCredentialSafeToPropagate}. */
|
|
46
|
+
export declare function hasPortableAuthFiles(agent: string): boolean;
|
|
30
47
|
/** Which agents `apply` can propagate auth for at all. */
|
|
31
48
|
export declare function isPropagatableAgent(agent: string): boolean;
|
|
32
49
|
/**
|
|
@@ -48,7 +65,7 @@ export interface LoginFlow {
|
|
|
48
65
|
/**
|
|
49
66
|
* The exact command that starts the login flow on the remote box, run as
|
|
50
67
|
* `ssh -tt <box> <loginCommand>`. Bare `<cli>` for agents whose device flow
|
|
51
|
-
* starts on launch (droid, kimi,
|
|
68
|
+
* starts on launch (droid, kimi, antigravity), `<cli> login` for
|
|
52
69
|
* codex/grok, `<cli> auth login` for opencode.
|
|
53
70
|
*/
|
|
54
71
|
loginCommand: string;
|
|
@@ -23,7 +23,6 @@ import * as path from 'path';
|
|
|
23
23
|
export const FLEET_AUTH_FILES = {
|
|
24
24
|
claude: [{ rel: '.claude/.credentials.json', mode: 0o600 }],
|
|
25
25
|
codex: [{ rel: '.codex/auth.json', mode: 0o600 }],
|
|
26
|
-
gemini: [{ rel: '.gemini/oauth_creds.json', mode: 0o600 }],
|
|
27
26
|
grok: [{ rel: '.grok/auth.json', mode: 0o600 }],
|
|
28
27
|
kimi: [{ rel: '.kimi-code/credentials/kimi-code.json', mode: 0o600 }],
|
|
29
28
|
opencode: [{ rel: '.local/share/opencode/auth.json', mode: 0o600 }],
|
|
@@ -35,9 +34,30 @@ export const FLEET_AUTH_FILES = {
|
|
|
35
34
|
};
|
|
36
35
|
/** Agents whose macOS credentials live in the ACL-bound login keychain. */
|
|
37
36
|
export const KEYCHAIN_BOUND_ON_MAC = new Set(['claude', 'antigravity']);
|
|
37
|
+
/**
|
|
38
|
+
* Agents whose OAuth credentials rely on single-use refresh tokens that rotate
|
|
39
|
+
* server-side on every exchange. Copying these credential files across machines
|
|
40
|
+
* is fatal: the first refresh on any box invalidates every other holder's token,
|
|
41
|
+
* collapsing the fleet to a single working login (droid/WorkOS collapsed 10 boxes
|
|
42
|
+
* to 1 overnight — RUSH-1958). Add any newly-discovered single-use-rotation
|
|
43
|
+
* harness here; the predicate below is the one place the propagation decision is
|
|
44
|
+
* made. See also `remote-login.ts` and `usage.ts` for the per-machine-login policy.
|
|
45
|
+
*/
|
|
46
|
+
export const SINGLE_USE_ROTATING_REFRESH_AGENTS = new Set(['droid']);
|
|
47
|
+
/** True when `agent`'s portable credential file(s) are safe to copy between
|
|
48
|
+
* machines. Single-use rotating refresh tokens are never safe; keychain-bound
|
|
49
|
+
* tokens are handled separately by the caller via {@link KEYCHAIN_BOUND_ON_MAC}. */
|
|
50
|
+
export function isCredentialSafeToPropagate(agent) {
|
|
51
|
+
return !SINGLE_USE_ROTATING_REFRESH_AGENTS.has(agent);
|
|
52
|
+
}
|
|
53
|
+
/** True when the agent stores credentials in portable files we can read. This
|
|
54
|
+
* does NOT mean it is safe to propagate — check {@link isCredentialSafeToPropagate}. */
|
|
55
|
+
export function hasPortableAuthFiles(agent) {
|
|
56
|
+
return agent in FLEET_AUTH_FILES;
|
|
57
|
+
}
|
|
38
58
|
/** Which agents `apply` can propagate auth for at all. */
|
|
39
59
|
export function isPropagatableAgent(agent) {
|
|
40
|
-
return agent
|
|
60
|
+
return hasPortableAuthFiles(agent) && isCredentialSafeToPropagate(agent);
|
|
41
61
|
}
|
|
42
62
|
/** The success-file for an agent — the primary portable credential (first spec). */
|
|
43
63
|
function successFileFor(agent) {
|
|
@@ -107,14 +127,6 @@ export const FLEET_LOGIN_FLOWS = {
|
|
|
107
127
|
flowType: 'loopback',
|
|
108
128
|
successFile: successFileFor('antigravity'),
|
|
109
129
|
},
|
|
110
|
-
// gemini: bare `gemini`, Google login on launch. Not characterized as a
|
|
111
|
-
// device-code flow (typically loopback) and no captured pattern — mark unknown
|
|
112
|
-
// so it is flagged non-remotable rather than mis-driven.
|
|
113
|
-
gemini: {
|
|
114
|
-
loginCommand: 'gemini',
|
|
115
|
-
flowType: 'unknown',
|
|
116
|
-
successFile: successFileFor('gemini'),
|
|
117
|
-
},
|
|
118
130
|
// opencode: `opencode auth login`. No captured pattern — unknown/non-remotable.
|
|
119
131
|
opencode: {
|
|
120
132
|
loginCommand: 'opencode auth login',
|
|
@@ -144,7 +156,9 @@ export function snapshotAuth(agents, opts) {
|
|
|
144
156
|
for (const agent of agents) {
|
|
145
157
|
const specs = FLEET_AUTH_FILES[agent];
|
|
146
158
|
if (!specs)
|
|
147
|
-
continue; //
|
|
159
|
+
continue; // no portable file — caller surfaces separately if desired
|
|
160
|
+
if (!isCredentialSafeToPropagate(agent))
|
|
161
|
+
continue; // single-use rotating refresh tokens are never copied
|
|
148
162
|
if (opts.platform === 'darwin' && KEYCHAIN_BOUND_ON_MAC.has(agent)) {
|
|
149
163
|
bound.push(agent);
|
|
150
164
|
continue;
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* each verification URL + user code, and surfacing them all in ONE local browser
|
|
10
10
|
* page so the human enters codes back-to-back instead of babysitting N terminals.
|
|
11
11
|
*
|
|
12
|
+
* The propagation gate lives in `auth-sync.ts`: {@link isCredentialSafeToPropagate}
|
|
13
|
+
* and {@link SINGLE_USE_ROTATING_REFRESH_AGENTS} are the single place where the
|
|
14
|
+
* "is this credential safe to copy?" decision is made, shared by `agents apply`
|
|
15
|
+
* and any other propagation path.
|
|
16
|
+
*
|
|
12
17
|
* Layering — pure vs I/O, so the valuable logic is unit-testable without SSH:
|
|
13
18
|
* - `scrapeLogin` / `classifyLoginFlow` / `selectLoginTargets` /
|
|
14
19
|
* `buildRemoteLoginSshCommand` / `buildDashboardHtml` are PURE.
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -248,20 +248,6 @@ export declare function displayHomePath(dir: string): string;
|
|
|
248
248
|
* branch reconciles instead of failing with "Need to specify how to reconcile
|
|
249
249
|
* divergent branches".
|
|
250
250
|
*/
|
|
251
|
-
/**
|
|
252
|
-
* Auto-commit the machine's OWN generated per-device meta file
|
|
253
|
-
* (`devices/<machineId>/agents.yaml`) if it's the dirty state blocking a pull.
|
|
254
|
-
*
|
|
255
|
-
* That file is committed + synced (so every machine can introspect every other
|
|
256
|
-
* machine's pins), but each box rewrites its own copy whenever a pin changes —
|
|
257
|
-
* leaving the tree perpetually dirty and wedging `agents repo pull` (which
|
|
258
|
-
* refuses a dirty tree). This durably commits just that one path (explicit
|
|
259
|
-
* pathspec) so the pull can proceed; genuine user edits to OTHER files are left
|
|
260
|
-
* untouched and still (correctly) block the pull. No-op when the meta path isn't
|
|
261
|
-
* inside `dir` (system/extra repos) or isn't dirty. Returns the committed rel
|
|
262
|
-
* path, or null. `metaAbs` is injectable for tests; defaults to the live path.
|
|
263
|
-
*/
|
|
264
|
-
export declare function commitOwnDeviceMeta(dir: string, metaAbs?: string): Promise<string | null>;
|
|
265
251
|
export declare function pullRepo(dir: string): Promise<{
|
|
266
252
|
success: boolean;
|
|
267
253
|
commit: string;
|
|
@@ -341,3 +327,17 @@ export declare function tryAutoPull(dir: string): Promise<{
|
|
|
341
327
|
pulled: boolean;
|
|
342
328
|
error?: string;
|
|
343
329
|
}>;
|
|
330
|
+
/**
|
|
331
|
+
* How many commits `dir`'s checked-out branch is behind its upstream, read from
|
|
332
|
+
* the LAST-FETCHED remote-tracking ref — no network call. Returns null when the
|
|
333
|
+
* dir is not a git repo, has no upstream configured, or git errors.
|
|
334
|
+
*
|
|
335
|
+
* Used by `agents doctor` to flag a source layer (`~/.agents`, `~/.agents/.system`)
|
|
336
|
+
* that is reconciled against stale truth. Staleness relative to origin is a
|
|
337
|
+
* background auto-pull concern; this surfaces the same fact synchronously in the
|
|
338
|
+
* per-version verdict.
|
|
339
|
+
*/
|
|
340
|
+
export declare function commitsBehindUpstream(dir: string): {
|
|
341
|
+
behind: number;
|
|
342
|
+
branch: string;
|
|
343
|
+
} | null;
|
package/dist/lib/git.js
CHANGED
|
@@ -11,7 +11,7 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as os from 'os';
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import { IS_WINDOWS, isWindowsAbsolutePath } from './platform/index.js';
|
|
14
|
-
import { getPackageLocalPath
|
|
14
|
+
import { getPackageLocalPath } from './state.js';
|
|
15
15
|
import { DEFAULT_SYSTEM_REPO, systemRepoSlug } from './types.js';
|
|
16
16
|
/**
|
|
17
17
|
* Validate that a clone/pull source uses a safe git transport before it is
|
|
@@ -803,48 +803,13 @@ export function displayHomePath(dir) {
|
|
|
803
803
|
* branch reconciles instead of failing with "Need to specify how to reconcile
|
|
804
804
|
* divergent branches".
|
|
805
805
|
*/
|
|
806
|
-
/**
|
|
807
|
-
* Auto-commit the machine's OWN generated per-device meta file
|
|
808
|
-
* (`devices/<machineId>/agents.yaml`) if it's the dirty state blocking a pull.
|
|
809
|
-
*
|
|
810
|
-
* That file is committed + synced (so every machine can introspect every other
|
|
811
|
-
* machine's pins), but each box rewrites its own copy whenever a pin changes —
|
|
812
|
-
* leaving the tree perpetually dirty and wedging `agents repo pull` (which
|
|
813
|
-
* refuses a dirty tree). This durably commits just that one path (explicit
|
|
814
|
-
* pathspec) so the pull can proceed; genuine user edits to OTHER files are left
|
|
815
|
-
* untouched and still (correctly) block the pull. No-op when the meta path isn't
|
|
816
|
-
* inside `dir` (system/extra repos) or isn't dirty. Returns the committed rel
|
|
817
|
-
* path, or null. `metaAbs` is injectable for tests; defaults to the live path.
|
|
818
|
-
*/
|
|
819
|
-
export async function commitOwnDeviceMeta(dir, metaAbs = getDeviceMetaPath()) {
|
|
820
|
-
const resolvedDir = path.resolve(dir);
|
|
821
|
-
const resolvedMeta = path.resolve(metaAbs);
|
|
822
|
-
if (resolvedMeta !== resolvedDir && !resolvedMeta.startsWith(resolvedDir + path.sep)) {
|
|
823
|
-
return null; // meta lives outside this repo — not ours to commit here
|
|
824
|
-
}
|
|
825
|
-
const rel = path.relative(resolvedDir, resolvedMeta).split(path.sep).join('/');
|
|
826
|
-
try {
|
|
827
|
-
const git = simpleGit(dir);
|
|
828
|
-
const status = await git.status();
|
|
829
|
-
const dirty = status.files.some((f) => f.path === rel);
|
|
830
|
-
if (!dirty)
|
|
831
|
-
return null;
|
|
832
|
-
await git.add([rel]);
|
|
833
|
-
const machine = path.basename(path.dirname(resolvedMeta));
|
|
834
|
-
await git.commit(`chore(devices): snapshot ${machine} agent pins`, [rel]);
|
|
835
|
-
return rel;
|
|
836
|
-
}
|
|
837
|
-
catch {
|
|
838
|
-
return null; // best-effort — never let this block the pull path
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
806
|
export async function pullRepo(dir) {
|
|
842
807
|
try {
|
|
843
808
|
const git = simpleGit(dir);
|
|
844
809
|
// A rebase left in progress by an earlier run must be reported as itself.
|
|
845
810
|
// Without this the dirty-tree guard below claims "Blocked by local changes",
|
|
846
811
|
// which is both wrong and actively harmful advice mid-rebase on a detached
|
|
847
|
-
// HEAD.
|
|
812
|
+
// HEAD.
|
|
848
813
|
// Ask git where the state dirs live rather than assuming `<dir>/.git/` is a
|
|
849
814
|
// directory. In a worktree `.git` is a FILE containing `gitdir: <path>`, so
|
|
850
815
|
// path.join(dir, '.git', 'rebase-merge') can never exist and the check would
|
|
@@ -874,9 +839,6 @@ export async function pullRepo(dir) {
|
|
|
874
839
|
` git rebase --abort # to discard the attempt`,
|
|
875
840
|
};
|
|
876
841
|
}
|
|
877
|
-
// Commit this machine's own device-meta first so a per-machine pin change
|
|
878
|
-
// never wedges the pull. Genuine edits elsewhere still block below.
|
|
879
|
-
await commitOwnDeviceMeta(dir);
|
|
880
842
|
const status = await git.status();
|
|
881
843
|
if (!status.isClean()) {
|
|
882
844
|
return {
|
|
@@ -934,10 +896,8 @@ export async function pullRepo(dir) {
|
|
|
934
896
|
}
|
|
935
897
|
try {
|
|
936
898
|
// Rebase, not --ff-only. Fast-forward refuses ANY divergence, conflict or
|
|
937
|
-
// not, so a single local commit
|
|
938
|
-
//
|
|
939
|
-
// conflict. Every device carries its own devices/<host>/ path, so those
|
|
940
|
-
// replay cleanly. Matches syncRepoGit (below) and this function's own doc.
|
|
899
|
+
// not, so a single local commit permanently wedged the pull with nothing
|
|
900
|
+
// actually in conflict. Matches syncRepoGit (below) and this function's doc.
|
|
941
901
|
//
|
|
942
902
|
// Pull the RESOLVED tracking ref, not `branch`: when the local branch has
|
|
943
903
|
// no tracking config the block above falls back to origin's default head,
|
|
@@ -1211,3 +1171,40 @@ export async function tryAutoPull(dir) {
|
|
|
1211
1171
|
return { pulled: false, error: err.message };
|
|
1212
1172
|
}
|
|
1213
1173
|
}
|
|
1174
|
+
/**
|
|
1175
|
+
* How many commits `dir`'s checked-out branch is behind its upstream, read from
|
|
1176
|
+
* the LAST-FETCHED remote-tracking ref — no network call. Returns null when the
|
|
1177
|
+
* dir is not a git repo, has no upstream configured, or git errors.
|
|
1178
|
+
*
|
|
1179
|
+
* Used by `agents doctor` to flag a source layer (`~/.agents`, `~/.agents/.system`)
|
|
1180
|
+
* that is reconciled against stale truth. Staleness relative to origin is a
|
|
1181
|
+
* background auto-pull concern; this surfaces the same fact synchronously in the
|
|
1182
|
+
* per-version verdict.
|
|
1183
|
+
*/
|
|
1184
|
+
export function commitsBehindUpstream(dir) {
|
|
1185
|
+
if (!isGitRepo(dir))
|
|
1186
|
+
return null;
|
|
1187
|
+
const run = (args) => {
|
|
1188
|
+
try {
|
|
1189
|
+
return execFileSync('git', ['-C', dir, ...args], {
|
|
1190
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1191
|
+
encoding: 'utf8',
|
|
1192
|
+
}).trim();
|
|
1193
|
+
}
|
|
1194
|
+
catch {
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
// Name of the upstream ref (e.g. `origin/main`) for the human-readable message.
|
|
1199
|
+
const branch = run(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}']);
|
|
1200
|
+
if (!branch)
|
|
1201
|
+
return null;
|
|
1202
|
+
// `--count HEAD..@{upstream}` = commits on upstream not yet in HEAD = behind.
|
|
1203
|
+
const raw = run(['rev-list', '--count', 'HEAD..@{upstream}']);
|
|
1204
|
+
if (raw === null)
|
|
1205
|
+
return null;
|
|
1206
|
+
const behind = parseInt(raw, 10);
|
|
1207
|
+
if (!Number.isFinite(behind))
|
|
1208
|
+
return null;
|
|
1209
|
+
return { behind, branch };
|
|
1210
|
+
}
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -21,12 +21,41 @@ export declare function resolveHookScriptPath(script: string): string | null;
|
|
|
21
21
|
* os.homedir()/path.sep at the call site.
|
|
22
22
|
*/
|
|
23
23
|
export declare function toPortableCommand(absPath: string, home?: string, sep?: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Collapse version-scoped registrations by logical resource identity rather
|
|
26
|
+
* than absolute command path. When the same resource is present in several
|
|
27
|
+
* homes, the active home's command wins even if it appears later. Commands
|
|
28
|
+
* outside version homes are user-owned and remain untouched.
|
|
29
|
+
*/
|
|
30
|
+
export declare function deduplicateVersionHookCommands(commands: string[], activeVersionHome: string): string[];
|
|
24
31
|
import type { AgentId, InstalledHook, ManifestHook } from './types.js';
|
|
25
32
|
export type HookEntry = {
|
|
26
33
|
name: string;
|
|
27
34
|
scriptPath: string;
|
|
28
35
|
dataFile?: string;
|
|
29
36
|
};
|
|
37
|
+
export interface VersionHookCopy {
|
|
38
|
+
agent: AgentId;
|
|
39
|
+
version: string;
|
|
40
|
+
name: string;
|
|
41
|
+
path: string;
|
|
42
|
+
hash: string;
|
|
43
|
+
active: boolean;
|
|
44
|
+
}
|
|
45
|
+
export interface DuplicateVersionHook {
|
|
46
|
+
agent: AgentId;
|
|
47
|
+
name: string;
|
|
48
|
+
kind: 'duplicate' | 'drift';
|
|
49
|
+
authoritative: VersionHookCopy;
|
|
50
|
+
copies: VersionHookCopy[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Inspect every installed hooks-capable harness without assuming a native
|
|
54
|
+
* settings format. Same-name copies with one content hash are installation
|
|
55
|
+
* noise; multiple hashes are drift. The active version is always selected as
|
|
56
|
+
* the authoritative copy when it participates in the group.
|
|
57
|
+
*/
|
|
58
|
+
export declare function inspectDuplicateVersionHooks(cwd?: string): DuplicateVersionHook[];
|
|
30
59
|
/**
|
|
31
60
|
* List hook entries in a single directory, grouping script + data files by
|
|
32
61
|
* basename. Exported so doctor-diff can reuse the same grouping the sync path
|
|
@@ -59,6 +88,48 @@ export declare function getVersionHooksDir(agent: AgentId, version: string): str
|
|
|
59
88
|
* List hook entries in a specific version home.
|
|
60
89
|
*/
|
|
61
90
|
export declare function listHooksInVersionHome(agent: AgentId, version: string): HookEntry[];
|
|
91
|
+
export interface HookWiringIssue {
|
|
92
|
+
/** Hook name (script basename minus extension). */
|
|
93
|
+
name: string;
|
|
94
|
+
/** Native lifecycle event the hook should be wired to (Stop, PreToolUse, …). */
|
|
95
|
+
event: string;
|
|
96
|
+
/** Matcher group the hook belongs to under `event` (`''` = the catch-all
|
|
97
|
+
* group). Real hooks scope by matcher — ask-user-question-guard=AskUserQuestion,
|
|
98
|
+
* user-message-guard=Bash — so wiring is verified per (event, matcher). */
|
|
99
|
+
matcher: string;
|
|
100
|
+
/** The command settings.json should reference for this hook under `event`. */
|
|
101
|
+
command: string;
|
|
102
|
+
}
|
|
103
|
+
export interface HookWiringReport {
|
|
104
|
+
/** Whether this agent's hook config format is understood by the inspector. */
|
|
105
|
+
supported: boolean;
|
|
106
|
+
/** Absolute path of the settings file inspected (when supported). */
|
|
107
|
+
settingsPath?: string;
|
|
108
|
+
/** Number of hooks the manifest says should be wired for this version. */
|
|
109
|
+
expected?: number;
|
|
110
|
+
/** settings.json does not exist — nothing declared can be wired. */
|
|
111
|
+
settingsMissing?: boolean;
|
|
112
|
+
/** settings.json exists but is not valid JSON — wiring can't be verified. */
|
|
113
|
+
settingsUnparseable?: boolean;
|
|
114
|
+
/** Hooks whose file is present/resolvable but that are NOT referenced in the
|
|
115
|
+
* event array settings.json should carry them in. */
|
|
116
|
+
unwired: HookWiringIssue[];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Verify that every hook the manifest says should be wired for a (claude|droid)
|
|
120
|
+
* version is actually REFERENCED in that version's native settings.json — not
|
|
121
|
+
* merely present as a file on disk.
|
|
122
|
+
*
|
|
123
|
+
* `agents doctor` compares hook FILES against source (see diffHooks in
|
|
124
|
+
* doctor-diff.ts) but never checks the wiring, so a hook whose script is
|
|
125
|
+
* byte-identical to source yet missing from settings.json's PreToolUse/Stop/…
|
|
126
|
+
* array reads as "ok" while it never fires. This closes that blind spot.
|
|
127
|
+
*
|
|
128
|
+
* Read-only by construction: it mirrors registerHooksForClaude's command
|
|
129
|
+
* resolution WITHOUT the shim-generation / chmod side effects that
|
|
130
|
+
* resolveHookCommand performs, so it never mutates the version home.
|
|
131
|
+
*/
|
|
132
|
+
export declare function checkVersionHookWiring(agent: AgentId, version: string): HookWiringReport;
|
|
62
133
|
export interface VersionHookDiff {
|
|
63
134
|
agent: AgentId;
|
|
64
135
|
version: string;
|