@phnx-labs/agents-cli 1.20.77 → 1.20.82
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 +589 -0
- package/README.md +27 -25
- 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 +74 -0
- package/dist/commands/doctor.js +718 -40
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.js +53 -8
- 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/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 +195 -11
- package/dist/commands/routines.js +16 -4
- package/dist/commands/rules.js +13 -12
- package/dist/commands/secrets.d.ts +3 -1
- package/dist/commands/secrets.js +108 -34
- package/dist/commands/sessions-browser.d.ts +45 -3
- package/dist/commands/sessions-browser.js +118 -12
- package/dist/commands/sessions-export.d.ts +3 -0
- package/dist/commands/sessions-export.js +13 -10
- package/dist/commands/sessions-inject.d.ts +7 -4
- package/dist/commands/sessions-inject.js +18 -18
- package/dist/commands/sessions-picker.d.ts +15 -1
- package/dist/commands/sessions-picker.js +90 -7
- package/dist/commands/sessions-resume.d.ts +16 -0
- package/dist/commands/sessions-resume.js +14 -13
- package/dist/commands/sessions.d.ts +126 -1
- package/dist/commands/sessions.js +365 -42
- 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/ssh.js +9 -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 +8 -13
- 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 -23
- package/dist/lib/agents.js +72 -100
- package/dist/lib/auto-pull.d.ts +16 -8
- package/dist/lib/auto-pull.js +23 -28
- 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 +26 -40
- package/dist/lib/daemon.js +63 -123
- package/dist/lib/devices/fleet-divergence.d.ts +101 -0
- package/dist/lib/devices/fleet-divergence.js +188 -0
- package/dist/lib/devices/fleet-inventory.d.ts +19 -0
- package/dist/lib/devices/fleet-inventory.js +57 -0
- package/dist/lib/devices/fleet.d.ts +1 -1
- package/dist/lib/devices/fleet.js +1 -1
- package/dist/lib/devices/health-report.d.ts +10 -2
- package/dist/lib/devices/health-report.js +32 -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.js +35 -8
- 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 +27 -14
- package/dist/lib/git.js +138 -43
- package/dist/lib/hooks.d.ts +42 -0
- package/dist/lib/hooks.js +137 -93
- 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/remote-cmd.js +2 -0
- 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/Info.plist +2 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -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 +20 -0
- package/dist/lib/routines.js +6 -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 +71 -29
- package/dist/lib/sandbox.js +0 -16
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/agent.d.ts +18 -0
- package/dist/lib/secrets/agent.js +131 -17
- package/dist/lib/secrets/audit.d.ts +46 -0
- package/dist/lib/secrets/audit.js +56 -0
- package/dist/lib/secrets/bundles.d.ts +12 -9
- package/dist/lib/secrets/bundles.js +47 -31
- 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/scope.d.ts +26 -0
- package/dist/lib/secrets/scope.js +29 -0
- package/dist/lib/secrets/session-store.d.ts +10 -0
- package/dist/lib/secrets/session-store.js +64 -11
- package/dist/lib/secrets/sync.js +3 -3
- package/dist/lib/session/active.d.ts +117 -26
- package/dist/lib/session/active.js +199 -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 +41 -1
- package/dist/lib/session/db.js +173 -8
- package/dist/lib/session/digest.js +126 -21
- package/dist/lib/session/discover.d.ts +19 -2
- package/dist/lib/session/discover.js +32 -11
- package/dist/lib/session/hook-sessions.d.ts +9 -0
- package/dist/lib/session/hook-sessions.js +57 -8
- package/dist/lib/session/origin-machine.d.ts +18 -0
- package/dist/lib/session/origin-machine.js +34 -0
- 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/state.d.ts +1 -0
- package/dist/lib/session/state.js +1 -1
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/types.d.ts +16 -0
- package/dist/lib/smart-launch.d.ts +86 -0
- package/dist/lib/smart-launch.js +172 -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/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/secrets/account-token.d.ts +0 -20
- package/dist/lib/secrets/account-token.js +0 -64
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
|
|
@@ -334,6 +334,34 @@ export async function getRepoCommit(repoPath) {
|
|
|
334
334
|
return 'unknown';
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
|
+
/** Read {@link RepoStateSnapshot} for `repoPath` using plumbing commands so the
|
|
338
|
+
* result is stable across git versions and never mutates the tree. Returns null
|
|
339
|
+
* when the path is not a git worktree. */
|
|
340
|
+
export function readRepoState(repoPath) {
|
|
341
|
+
const runGit = (args) => {
|
|
342
|
+
try {
|
|
343
|
+
return execFileSync('git', ['-C', repoPath, ...args], {
|
|
344
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
345
|
+
})
|
|
346
|
+
.toString()
|
|
347
|
+
.trim();
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
// Gate on a real worktree first; `rev-parse --is-inside-work-tree` prints
|
|
354
|
+
// `true` only inside one, and returns non-zero (→ null) otherwise.
|
|
355
|
+
if (runGit(['rev-parse', '--is-inside-work-tree']) !== 'true')
|
|
356
|
+
return null;
|
|
357
|
+
const branchRaw = runGit(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
358
|
+
const branch = branchRaw && branchRaw !== 'HEAD' ? branchRaw : null; // detached → null
|
|
359
|
+
const headRaw = runGit(['rev-parse', 'HEAD']);
|
|
360
|
+
const head = headRaw ? headRaw.slice(0, 8) : null;
|
|
361
|
+
const porcelain = runGit(['status', '--porcelain']);
|
|
362
|
+
const dirty = porcelain != null && porcelain.length > 0;
|
|
363
|
+
return { branch, head, dirty };
|
|
364
|
+
}
|
|
337
365
|
/**
|
|
338
366
|
* Get the current GitHub username using gh CLI.
|
|
339
367
|
* Returns null if gh is not installed or user is not authenticated.
|
|
@@ -775,47 +803,42 @@ export function displayHomePath(dir) {
|
|
|
775
803
|
* branch reconciles instead of failing with "Need to specify how to reconcile
|
|
776
804
|
* divergent branches".
|
|
777
805
|
*/
|
|
778
|
-
/**
|
|
779
|
-
* Auto-commit the machine's OWN generated per-device meta file
|
|
780
|
-
* (`devices/<machineId>/agents.yaml`) if it's the dirty state blocking a pull.
|
|
781
|
-
*
|
|
782
|
-
* That file is committed + synced (so every machine can introspect every other
|
|
783
|
-
* machine's pins), but each box rewrites its own copy whenever a pin changes —
|
|
784
|
-
* leaving the tree perpetually dirty and wedging `agents repo pull` (which
|
|
785
|
-
* refuses a dirty tree). This durably commits just that one path (explicit
|
|
786
|
-
* pathspec) so the pull can proceed; genuine user edits to OTHER files are left
|
|
787
|
-
* untouched and still (correctly) block the pull. No-op when the meta path isn't
|
|
788
|
-
* inside `dir` (system/extra repos) or isn't dirty. Returns the committed rel
|
|
789
|
-
* path, or null. `metaAbs` is injectable for tests; defaults to the live path.
|
|
790
|
-
*/
|
|
791
|
-
export async function commitOwnDeviceMeta(dir, metaAbs = getDeviceMetaPath()) {
|
|
792
|
-
const resolvedDir = path.resolve(dir);
|
|
793
|
-
const resolvedMeta = path.resolve(metaAbs);
|
|
794
|
-
if (resolvedMeta !== resolvedDir && !resolvedMeta.startsWith(resolvedDir + path.sep)) {
|
|
795
|
-
return null; // meta lives outside this repo — not ours to commit here
|
|
796
|
-
}
|
|
797
|
-
const rel = path.relative(resolvedDir, resolvedMeta).split(path.sep).join('/');
|
|
798
|
-
try {
|
|
799
|
-
const git = simpleGit(dir);
|
|
800
|
-
const status = await git.status();
|
|
801
|
-
const dirty = status.files.some((f) => f.path === rel);
|
|
802
|
-
if (!dirty)
|
|
803
|
-
return null;
|
|
804
|
-
await git.add([rel]);
|
|
805
|
-
const machine = path.basename(path.dirname(resolvedMeta));
|
|
806
|
-
await git.commit(`chore(devices): snapshot ${machine} agent pins`, [rel]);
|
|
807
|
-
return rel;
|
|
808
|
-
}
|
|
809
|
-
catch {
|
|
810
|
-
return null; // best-effort — never let this block the pull path
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
806
|
export async function pullRepo(dir) {
|
|
814
807
|
try {
|
|
815
808
|
const git = simpleGit(dir);
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
|
|
809
|
+
// A rebase left in progress by an earlier run must be reported as itself.
|
|
810
|
+
// Without this the dirty-tree guard below claims "Blocked by local changes",
|
|
811
|
+
// which is both wrong and actively harmful advice mid-rebase on a detached
|
|
812
|
+
// HEAD.
|
|
813
|
+
// Ask git where the state dirs live rather than assuming `<dir>/.git/` is a
|
|
814
|
+
// directory. In a worktree `.git` is a FILE containing `gitdir: <path>`, so
|
|
815
|
+
// path.join(dir, '.git', 'rebase-merge') can never exist and the check would
|
|
816
|
+
// silently never fire. `rev-parse --git-path` resolves both layouts.
|
|
817
|
+
const gitPath = async (name) => {
|
|
818
|
+
try {
|
|
819
|
+
const raw = (await git.raw(['rev-parse', '--git-path', name])).trim();
|
|
820
|
+
return path.isAbsolute(raw) ? raw : path.join(dir, raw);
|
|
821
|
+
}
|
|
822
|
+
catch {
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
const [rebaseMerge, rebaseApply] = await Promise.all([
|
|
827
|
+
gitPath('rebase-merge'),
|
|
828
|
+
gitPath('rebase-apply'),
|
|
829
|
+
]);
|
|
830
|
+
const rebaseInProgress = (rebaseMerge !== null && fs.existsSync(rebaseMerge)) ||
|
|
831
|
+
(rebaseApply !== null && fs.existsSync(rebaseApply));
|
|
832
|
+
if (rebaseInProgress) {
|
|
833
|
+
return {
|
|
834
|
+
success: false,
|
|
835
|
+
commit: '',
|
|
836
|
+
error: `A previous rebase is still in progress — finish or abort it, then pull again.\n\n` +
|
|
837
|
+
` cd ${displayHomePath(dir)} && git status\n` +
|
|
838
|
+
` git rebase --continue # after resolving\n` +
|
|
839
|
+
` git rebase --abort # to discard the attempt`,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
819
842
|
const status = await git.status();
|
|
820
843
|
if (!status.isClean()) {
|
|
821
844
|
return {
|
|
@@ -825,12 +848,15 @@ export async function pullRepo(dir) {
|
|
|
825
848
|
};
|
|
826
849
|
}
|
|
827
850
|
const branch = status.current || 'main';
|
|
828
|
-
await git.fetch('origin');
|
|
829
851
|
// Resolve the upstream ref to fast-forward against. Prefer the local
|
|
830
852
|
// branch's tracking config; otherwise ask origin for its default branch.
|
|
831
853
|
let tracking = status.tracking;
|
|
832
854
|
if (!tracking) {
|
|
855
|
+
// No tracking config: fetch origin so its HEAD is known, then ask which
|
|
856
|
+
// branch it points at. This path only ever concerns origin — a branch with
|
|
857
|
+
// no upstream has no other remote to consult.
|
|
833
858
|
try {
|
|
859
|
+
await git.fetch('origin');
|
|
834
860
|
await git.raw(['remote', 'set-head', 'origin', '--auto']);
|
|
835
861
|
const sym = await git.raw(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);
|
|
836
862
|
tracking = sym.trim();
|
|
@@ -839,6 +865,22 @@ export async function pullRepo(dir) {
|
|
|
839
865
|
tracking = `origin/${branch}`;
|
|
840
866
|
}
|
|
841
867
|
}
|
|
868
|
+
// Split the remote-tracking ref (<remote>/<branch>) into its parts and pull
|
|
869
|
+
// THOSE. Hardcoding 'origin' while comparing against `tracking` is how a
|
|
870
|
+
// branch tracking e.g. upstream/main silently 'succeeded': revparse saw a
|
|
871
|
+
// difference, the pull fetched origin/main (already current), and pullRepo
|
|
872
|
+
// returned success having moved nothing — the same "reported ok, pulled
|
|
873
|
+
// nothing" failure this change exists to remove. Branch names may contain
|
|
874
|
+
// slashes, so split on the FIRST separator only.
|
|
875
|
+
const sep = tracking.indexOf('/');
|
|
876
|
+
const remoteName = sep > 0 ? tracking.slice(0, sep) : 'origin';
|
|
877
|
+
const remoteBranch = sep > 0 ? tracking.slice(sep + 1) : branch;
|
|
878
|
+
// Bare fetch: updates every remote, so the revparse below sees a fresh ref
|
|
879
|
+
// whichever one the branch tracks. Deliberately argument-less — simple-git's
|
|
880
|
+
// fetchTask only forwards a remote when BOTH remote and branch are passed,
|
|
881
|
+
// so `fetch(remoteName)` would silently drop the argument and do exactly
|
|
882
|
+
// this anyway. Saying so beats an inert argument that reads as targeted.
|
|
883
|
+
await git.fetch();
|
|
842
884
|
const localRef = await git.revparse(['HEAD']);
|
|
843
885
|
const remoteRef = await git.revparse([tracking]).catch(() => null);
|
|
844
886
|
if (!remoteRef) {
|
|
@@ -853,13 +895,29 @@ export async function pullRepo(dir) {
|
|
|
853
895
|
};
|
|
854
896
|
}
|
|
855
897
|
try {
|
|
856
|
-
|
|
898
|
+
// Rebase, not --ff-only. Fast-forward refuses ANY divergence, conflict or
|
|
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.
|
|
901
|
+
//
|
|
902
|
+
// Pull the RESOLVED tracking ref, not `branch`: when the local branch has
|
|
903
|
+
// no tracking config the block above falls back to origin's default head,
|
|
904
|
+
// which may be named differently (local `main` vs origin `master`). Using
|
|
905
|
+
// `branch` there asks origin for a ref it does not have.
|
|
906
|
+
assertValidBranchName(remoteBranch);
|
|
907
|
+
await git.pull(remoteName, remoteBranch, { '--rebase': 'true' });
|
|
857
908
|
}
|
|
858
|
-
catch {
|
|
909
|
+
catch (err) {
|
|
910
|
+
// Abort so the tree is restored, matching the atomicity --ff-only gave us.
|
|
911
|
+
// Without this a conflict leaves the repo detached, mid-rebase, with
|
|
912
|
+
// conflict markers written into live config (this repo is ~/.agents —
|
|
913
|
+
// agents.yaml and AGENTS.md are in it), and every later pull misreports
|
|
914
|
+
// the cause. `agents sync` reaches this path unattended across the fleet,
|
|
915
|
+
// so a wedged checkout would be worse than the bug this fixes.
|
|
916
|
+
await git.raw(['rebase', '--abort']).catch(() => { });
|
|
859
917
|
return {
|
|
860
918
|
success: false,
|
|
861
919
|
commit: '',
|
|
862
|
-
error: `
|
|
920
|
+
error: `Rebase onto ${tracking} hit a conflict — 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}`,
|
|
863
921
|
};
|
|
864
922
|
}
|
|
865
923
|
installGithooksSymlinks(dir);
|
|
@@ -1113,3 +1171,40 @@ export async function tryAutoPull(dir) {
|
|
|
1113
1171
|
return { pulled: false, error: err.message };
|
|
1114
1172
|
}
|
|
1115
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
|
@@ -59,6 +59,48 @@ export declare function getVersionHooksDir(agent: AgentId, version: string): str
|
|
|
59
59
|
* List hook entries in a specific version home.
|
|
60
60
|
*/
|
|
61
61
|
export declare function listHooksInVersionHome(agent: AgentId, version: string): HookEntry[];
|
|
62
|
+
export interface HookWiringIssue {
|
|
63
|
+
/** Hook name (script basename minus extension). */
|
|
64
|
+
name: string;
|
|
65
|
+
/** Native lifecycle event the hook should be wired to (Stop, PreToolUse, …). */
|
|
66
|
+
event: string;
|
|
67
|
+
/** Matcher group the hook belongs to under `event` (`''` = the catch-all
|
|
68
|
+
* group). Real hooks scope by matcher — ask-user-question-guard=AskUserQuestion,
|
|
69
|
+
* user-message-guard=Bash — so wiring is verified per (event, matcher). */
|
|
70
|
+
matcher: string;
|
|
71
|
+
/** The command settings.json should reference for this hook under `event`. */
|
|
72
|
+
command: string;
|
|
73
|
+
}
|
|
74
|
+
export interface HookWiringReport {
|
|
75
|
+
/** Whether this agent's hook config format is understood by the inspector. */
|
|
76
|
+
supported: boolean;
|
|
77
|
+
/** Absolute path of the settings file inspected (when supported). */
|
|
78
|
+
settingsPath?: string;
|
|
79
|
+
/** Number of hooks the manifest says should be wired for this version. */
|
|
80
|
+
expected?: number;
|
|
81
|
+
/** settings.json does not exist — nothing declared can be wired. */
|
|
82
|
+
settingsMissing?: boolean;
|
|
83
|
+
/** settings.json exists but is not valid JSON — wiring can't be verified. */
|
|
84
|
+
settingsUnparseable?: boolean;
|
|
85
|
+
/** Hooks whose file is present/resolvable but that are NOT referenced in the
|
|
86
|
+
* event array settings.json should carry them in. */
|
|
87
|
+
unwired: HookWiringIssue[];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Verify that every hook the manifest says should be wired for a (claude|droid)
|
|
91
|
+
* version is actually REFERENCED in that version's native settings.json — not
|
|
92
|
+
* merely present as a file on disk.
|
|
93
|
+
*
|
|
94
|
+
* `agents doctor` compares hook FILES against source (see diffHooks in
|
|
95
|
+
* doctor-diff.ts) but never checks the wiring, so a hook whose script is
|
|
96
|
+
* byte-identical to source yet missing from settings.json's PreToolUse/Stop/…
|
|
97
|
+
* array reads as "ok" while it never fires. This closes that blind spot.
|
|
98
|
+
*
|
|
99
|
+
* Read-only by construction: it mirrors registerHooksForClaude's command
|
|
100
|
+
* resolution WITHOUT the shim-generation / chmod side effects that
|
|
101
|
+
* resolveHookCommand performs, so it never mutates the version home.
|
|
102
|
+
*/
|
|
103
|
+
export declare function checkVersionHookWiring(agent: AgentId, version: string): HookWiringReport;
|
|
62
104
|
export interface VersionHookDiff {
|
|
63
105
|
agent: AgentId;
|
|
64
106
|
version: string;
|
package/dist/lib/hooks.js
CHANGED
|
@@ -13,9 +13,8 @@ import * as os from 'os';
|
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as yaml from 'yaml';
|
|
15
15
|
import * as TOML from 'smol-toml';
|
|
16
|
-
import { AGENTS, agentConfigDirName } from './agents.js';
|
|
16
|
+
import { AGENTS, agentConfigDirName, isAgentHardDeprecated } from './agents.js';
|
|
17
17
|
import { supports, explainSkip, capableAgents } from './capabilities.js';
|
|
18
|
-
import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from './gemini-settings.js';
|
|
19
18
|
import { getHooksDir as getSystemHooksDir, getUserHooksDir, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, getTrashHooksDir, getEnabledExtraRepos, getResolvedRulesDir, getUserRulesDir } from './state.js';
|
|
20
19
|
import { collectSubruleHooksFromState } from './rules/compose.js';
|
|
21
20
|
function getCentralHooksDir() { return getUserHooksDir(); }
|
|
@@ -148,7 +147,7 @@ function isStaleSiblingVersionCommand(command, current) {
|
|
|
148
147
|
return id !== null && id.agent === current.agent && id.version !== current.version;
|
|
149
148
|
}
|
|
150
149
|
import { getEffectiveHome, getVersionHomePath, listInstalledVersions } from './versions.js';
|
|
151
|
-
import { generateHookShim, isValidHookShimName, parseCacheConfig, removeHookShim } from './hooks/cache.js';
|
|
150
|
+
import { generateHookShim, getHookShimPath, isValidHookShimName, parseCacheConfig, removeHookShim } from './hooks/cache.js';
|
|
152
151
|
import { getHookShimsDir } from './state.js';
|
|
153
152
|
/**
|
|
154
153
|
* Resolve the command path to register for a hook.
|
|
@@ -488,6 +487,130 @@ export function getVersionHooksDir(agent, version) {
|
|
|
488
487
|
export function listHooksInVersionHome(agent, version) {
|
|
489
488
|
return listHookEntriesFromDir(getVersionHooksDir(agent, version));
|
|
490
489
|
}
|
|
490
|
+
// ─── wiring inspection (settings.json family: claude, droid) ──────────────────
|
|
491
|
+
/**
|
|
492
|
+
* Agents whose hooks register through {@link registerHooksForClaude} — a native
|
|
493
|
+
* settings.json shaped `hooks[event] = [{ matcher, hooks: [{ command }] }]`, with
|
|
494
|
+
* no event renaming. These are the only agents this read-only wiring inspector
|
|
495
|
+
* understands; every other harness uses a divergent config format and/or event
|
|
496
|
+
* map (Gemini/Antigravity settings.json variants, Codex config.toml, the OpenCode
|
|
497
|
+
* plugin, …), so it reports them unsupported rather than risk a false verdict.
|
|
498
|
+
*/
|
|
499
|
+
const SETTINGS_JSON_HOOK_FAMILY = ['claude', 'droid'];
|
|
500
|
+
/**
|
|
501
|
+
* Verify that every hook the manifest says should be wired for a (claude|droid)
|
|
502
|
+
* version is actually REFERENCED in that version's native settings.json — not
|
|
503
|
+
* merely present as a file on disk.
|
|
504
|
+
*
|
|
505
|
+
* `agents doctor` compares hook FILES against source (see diffHooks in
|
|
506
|
+
* doctor-diff.ts) but never checks the wiring, so a hook whose script is
|
|
507
|
+
* byte-identical to source yet missing from settings.json's PreToolUse/Stop/…
|
|
508
|
+
* array reads as "ok" while it never fires. This closes that blind spot.
|
|
509
|
+
*
|
|
510
|
+
* Read-only by construction: it mirrors registerHooksForClaude's command
|
|
511
|
+
* resolution WITHOUT the shim-generation / chmod side effects that
|
|
512
|
+
* resolveHookCommand performs, so it never mutates the version home.
|
|
513
|
+
*/
|
|
514
|
+
export function checkVersionHookWiring(agent, version) {
|
|
515
|
+
if (!AGENTS[agent].supportsHooks || !SETTINGS_JSON_HOOK_FAMILY.includes(agent)) {
|
|
516
|
+
return { supported: false, unwired: [] };
|
|
517
|
+
}
|
|
518
|
+
const versionHome = getVersionHomePath(agent, version);
|
|
519
|
+
const settingsPath = path.join(versionHome, agentConfigDirName(agent), 'settings.json');
|
|
520
|
+
const localHooksDir = getVersionHooksDir(agent, version);
|
|
521
|
+
// Resolve ONLY to a script that was actually synced for THIS agent+version: the
|
|
522
|
+
// copy in the version home hooks dir, or an absolute subrule-dir path (those are
|
|
523
|
+
// registered in place, never copied). Deliberately NO central-source fallback —
|
|
524
|
+
// sync wires `selectHookManifest(parseHookManifest(), hooksToSync)` where
|
|
525
|
+
// hooksToSync is the per-agent selected set (versions.ts), so a hook the manifest
|
|
526
|
+
// declares but that was never synced into THIS version home (e.g. a claude-scoped
|
|
527
|
+
// hook viewed for droid) must not be expected here. A missing-but-declared hook
|
|
528
|
+
// is a FILE gap that diffHooks reports as `missing`, not a wiring gap.
|
|
529
|
+
// No ensureExecutable — that chmods, and this path must not touch disk.
|
|
530
|
+
const resolveScript = (script) => {
|
|
531
|
+
if (path.isAbsolute(script) && fs.existsSync(script))
|
|
532
|
+
return script;
|
|
533
|
+
return resolveContainedHookPath(localHooksDir, script);
|
|
534
|
+
};
|
|
535
|
+
// Read-only mirror of resolveHookCommand — same command string, no shim write.
|
|
536
|
+
const expectedCommand = (name, hookDef) => {
|
|
537
|
+
const scriptPath = resolveScript(hookDef.script);
|
|
538
|
+
if (!scriptPath)
|
|
539
|
+
return null;
|
|
540
|
+
if (!isValidHookShimName(name))
|
|
541
|
+
return null;
|
|
542
|
+
const cache = parseCacheConfig(hookDef.cache);
|
|
543
|
+
const hasMatches = hookDef.matches != null && Object.keys(hookDef.matches).length > 0;
|
|
544
|
+
if (!cache && !hasMatches)
|
|
545
|
+
return toPortableCommand(scriptPath);
|
|
546
|
+
return toPortableCommand(getHookShimPath(name));
|
|
547
|
+
};
|
|
548
|
+
const manifest = parseHookManifest({ warn: false });
|
|
549
|
+
const expected = [];
|
|
550
|
+
for (const [name, hookDef] of Object.entries(manifest)) {
|
|
551
|
+
if (!hookDef.events || hookDef.events.length === 0)
|
|
552
|
+
continue;
|
|
553
|
+
const command = expectedCommand(name, hookDef);
|
|
554
|
+
if (!command)
|
|
555
|
+
continue; // script unresolved — a file gap, reported by diffHooks
|
|
556
|
+
// Mirror registerHooksForClaude: a hook registers under the matcher group
|
|
557
|
+
// `hookDef.matcher || ''` for each of its events.
|
|
558
|
+
const matcher = hookDef.matcher || '';
|
|
559
|
+
for (const event of hookDef.events)
|
|
560
|
+
expected.push({ name, event, matcher, command });
|
|
561
|
+
}
|
|
562
|
+
if (!fs.existsSync(settingsPath)) {
|
|
563
|
+
return {
|
|
564
|
+
supported: true,
|
|
565
|
+
settingsPath,
|
|
566
|
+
expected: expected.length,
|
|
567
|
+
settingsMissing: expected.length > 0,
|
|
568
|
+
unwired: [],
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
let config;
|
|
572
|
+
try {
|
|
573
|
+
config = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
return {
|
|
577
|
+
supported: true,
|
|
578
|
+
settingsPath,
|
|
579
|
+
expected: expected.length,
|
|
580
|
+
settingsUnparseable: true,
|
|
581
|
+
unwired: [],
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
// Command strings actually referenced, keyed by (event, matcher) — a hook wired
|
|
585
|
+
// under the WRONG matcher group must NOT read as wired, so scope by matcher and
|
|
586
|
+
// not just by event.
|
|
587
|
+
const wiredByGroup = new Map();
|
|
588
|
+
const groupKey = (event, matcher) => `${event}\n${matcher}`;
|
|
589
|
+
const hooks = config.hooks && typeof config.hooks === 'object'
|
|
590
|
+
? config.hooks
|
|
591
|
+
: {};
|
|
592
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
593
|
+
if (!Array.isArray(groups))
|
|
594
|
+
continue;
|
|
595
|
+
for (const group of groups) {
|
|
596
|
+
if (!group || !Array.isArray(group.hooks))
|
|
597
|
+
continue;
|
|
598
|
+
const matcher = typeof group.matcher === 'string' ? group.matcher : '';
|
|
599
|
+
const key = groupKey(event, matcher);
|
|
600
|
+
let cmds = wiredByGroup.get(key);
|
|
601
|
+
if (!cmds) {
|
|
602
|
+
cmds = new Set();
|
|
603
|
+
wiredByGroup.set(key, cmds);
|
|
604
|
+
}
|
|
605
|
+
for (const h of group.hooks) {
|
|
606
|
+
if (h && typeof h.command === 'string')
|
|
607
|
+
cmds.add(h.command);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const unwired = expected.filter((e) => !wiredByGroup.get(groupKey(e.event, e.matcher))?.has(e.command));
|
|
612
|
+
return { supported: true, settingsPath, expected: expected.length, unwired };
|
|
613
|
+
}
|
|
491
614
|
/**
|
|
492
615
|
* Check if a hook installed in a specific version matches central content.
|
|
493
616
|
*/
|
|
@@ -936,6 +1059,9 @@ function sweepOrphanShims(manifest) {
|
|
|
936
1059
|
}
|
|
937
1060
|
}
|
|
938
1061
|
export function registerHooksToSettings(agentId, versionHome, hookManifest, agentsDirOverride) {
|
|
1062
|
+
if (isAgentHardDeprecated(agentId)) {
|
|
1063
|
+
return { registered: [], errors: [] };
|
|
1064
|
+
}
|
|
939
1065
|
const manifest = hookManifest || parseHookManifest();
|
|
940
1066
|
if (Object.keys(manifest).length === 0) {
|
|
941
1067
|
if (agentId === 'opencode') {
|
|
@@ -995,9 +1121,6 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
|
|
|
995
1121
|
if (agentId === 'codex') {
|
|
996
1122
|
return registerHooksForCodex(versionHome, manifest, resolveScript, managedPrefixes);
|
|
997
1123
|
}
|
|
998
|
-
if (agentId === 'gemini') {
|
|
999
|
-
return registerHooksForGemini(versionHome, manifest, resolveScript, managedPrefixes);
|
|
1000
|
-
}
|
|
1001
1124
|
if (agentId === 'antigravity') {
|
|
1002
1125
|
return registerHooksForAntigravity(versionHome, manifest, resolveScript, managedPrefixes);
|
|
1003
1126
|
}
|
|
@@ -1183,16 +1306,6 @@ const ANTIGRAVITY_EVENT_MAP = {
|
|
|
1183
1306
|
Stop: 'on_loop_stop',
|
|
1184
1307
|
OnError: 'on_error',
|
|
1185
1308
|
};
|
|
1186
|
-
/**
|
|
1187
|
-
* Gemini has no native UserPromptSubmit event — map it to BeforeAgent,
|
|
1188
|
-
* the closest lifecycle phase that fires before the model sees the prompt.
|
|
1189
|
-
* Note: gemini's BeforeAgent can only APPEND via additionalContext — it
|
|
1190
|
-
* cannot replace the prompt. The hook script branches on caller to emit
|
|
1191
|
-
* the correct protocol.
|
|
1192
|
-
*/
|
|
1193
|
-
const GEMINI_EVENT_MAP = {
|
|
1194
|
-
UserPromptSubmit: 'BeforeAgent',
|
|
1195
|
-
};
|
|
1196
1309
|
function registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes, configDirName = '.claude') {
|
|
1197
1310
|
const registered = [];
|
|
1198
1311
|
const errors = [];
|
|
@@ -1387,12 +1500,17 @@ function registerHooksForCodex(versionHome, manifest, resolveScript, managedPref
|
|
|
1387
1500
|
if (resolved)
|
|
1388
1501
|
currentManifestPaths.add(resolved);
|
|
1389
1502
|
}
|
|
1503
|
+
// Identity of the version home being synced. Codex stores hooks in a shared
|
|
1504
|
+
// hooks.json per CODEX_HOME, but agents-cli registers version-scoped command
|
|
1505
|
+
// paths; without this, old version entries keep firing after upgrades.
|
|
1506
|
+
const currentVh = versionHomeIdentity(versionHome);
|
|
1390
1507
|
// Remove stale entries from all event groups
|
|
1391
1508
|
for (const eventGroups of Object.values(hooksFile.hooks)) {
|
|
1392
1509
|
for (const group of eventGroups) {
|
|
1393
1510
|
if (!group.hooks)
|
|
1394
1511
|
continue;
|
|
1395
|
-
group.hooks = group.hooks.filter((h) => !isManagedHookCommand(h.command, managedPrefixes) || currentManifestPaths.has(h.command))
|
|
1512
|
+
group.hooks = group.hooks.filter((h) => (!isManagedHookCommand(h.command, managedPrefixes) || currentManifestPaths.has(h.command)) &&
|
|
1513
|
+
!isStaleSiblingVersionCommand(h.command, currentVh));
|
|
1396
1514
|
}
|
|
1397
1515
|
}
|
|
1398
1516
|
for (const [event, eventGroups] of Object.entries(hooksFile.hooks)) {
|
|
@@ -1436,7 +1554,8 @@ function registerHooksForCodex(versionHome, manifest, resolveScript, managedPref
|
|
|
1436
1554
|
group.hooks = [];
|
|
1437
1555
|
}
|
|
1438
1556
|
const existingIdx = group.hooks.findIndex((h) => h.command === commandPath);
|
|
1439
|
-
const
|
|
1557
|
+
const eventTimeout = event === 'SessionEnd' ? Math.min(timeout, 3) : timeout;
|
|
1558
|
+
const hookEntry = { type: 'command', command: commandPath, timeout: eventTimeout };
|
|
1440
1559
|
if (existingIdx >= 0) {
|
|
1441
1560
|
group.hooks[existingIdx] = hookEntry;
|
|
1442
1561
|
}
|
|
@@ -1529,81 +1648,6 @@ function registerHooksForCodex(versionHome, manifest, resolveScript, managedPref
|
|
|
1529
1648
|
}
|
|
1530
1649
|
return { registered, errors };
|
|
1531
1650
|
}
|
|
1532
|
-
function registerHooksForGemini(versionHome, manifest, resolveScript, managedPrefixes) {
|
|
1533
|
-
const registered = [];
|
|
1534
|
-
const errors = [];
|
|
1535
|
-
const settingsPath = path.join(versionHome, '.gemini', 'settings.json');
|
|
1536
|
-
try {
|
|
1537
|
-
updateGeminiSettings(settingsPath, (config) => {
|
|
1538
|
-
setGeminiAutoUpdateDisabled(config);
|
|
1539
|
-
if (!config.hooks || typeof config.hooks !== 'object') {
|
|
1540
|
-
config.hooks = {};
|
|
1541
|
-
}
|
|
1542
|
-
const hooks = config.hooks;
|
|
1543
|
-
const currentManifestPaths = new Set();
|
|
1544
|
-
for (const [hookName, hookDef] of Object.entries(manifest)) {
|
|
1545
|
-
if (!hookDef.events || hookDef.events.length === 0)
|
|
1546
|
-
continue;
|
|
1547
|
-
const resolved = resolveHookCommand(hookName, hookDef, resolveScript);
|
|
1548
|
-
if (resolved)
|
|
1549
|
-
currentManifestPaths.add(resolved);
|
|
1550
|
-
}
|
|
1551
|
-
for (const eventEntries of Object.values(hooks)) {
|
|
1552
|
-
if (!Array.isArray(eventEntries))
|
|
1553
|
-
continue;
|
|
1554
|
-
for (const group of eventEntries) {
|
|
1555
|
-
if (!group.hooks)
|
|
1556
|
-
continue;
|
|
1557
|
-
group.hooks = group.hooks.filter((h) => !isManagedHookCommand(h.command, managedPrefixes) || currentManifestPaths.has(h.command));
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
for (const [event, eventEntries] of Object.entries(hooks)) {
|
|
1561
|
-
if (!Array.isArray(eventEntries))
|
|
1562
|
-
continue;
|
|
1563
|
-
hooks[event] = eventEntries.filter((g) => g.hooks && g.hooks.length > 0);
|
|
1564
|
-
}
|
|
1565
|
-
for (const [name, hookDef] of Object.entries(manifest)) {
|
|
1566
|
-
if (!hookDef.events || hookDef.events.length === 0)
|
|
1567
|
-
continue;
|
|
1568
|
-
const commandPath = resolveHookCommand(name, hookDef, resolveScript);
|
|
1569
|
-
if (!commandPath) {
|
|
1570
|
-
errors.push(`${name}: script not found in user or system hooks dir`);
|
|
1571
|
-
continue;
|
|
1572
|
-
}
|
|
1573
|
-
const timeoutMs = (hookDef.timeout || 600) * 1000;
|
|
1574
|
-
for (const event of hookDef.events) {
|
|
1575
|
-
const geminiEvent = GEMINI_EVENT_MAP[event] ?? event;
|
|
1576
|
-
if (!hooks[geminiEvent]) {
|
|
1577
|
-
hooks[geminiEvent] = [];
|
|
1578
|
-
}
|
|
1579
|
-
const eventEntries = hooks[geminiEvent];
|
|
1580
|
-
const matcher = hookDef.matcher || '';
|
|
1581
|
-
let matcherGroup = eventEntries.find((e) => (e.matcher || '') === matcher);
|
|
1582
|
-
if (!matcherGroup) {
|
|
1583
|
-
matcherGroup = { matcher, hooks: [] };
|
|
1584
|
-
eventEntries.push(matcherGroup);
|
|
1585
|
-
}
|
|
1586
|
-
if (!matcherGroup.hooks) {
|
|
1587
|
-
matcherGroup.hooks = [];
|
|
1588
|
-
}
|
|
1589
|
-
const existingIdx = matcherGroup.hooks.findIndex((h) => h.command === commandPath);
|
|
1590
|
-
const hookEntry = { name, type: 'command', command: commandPath, timeout: timeoutMs };
|
|
1591
|
-
if (existingIdx >= 0) {
|
|
1592
|
-
matcherGroup.hooks[existingIdx] = hookEntry;
|
|
1593
|
-
}
|
|
1594
|
-
else {
|
|
1595
|
-
matcherGroup.hooks.push(hookEntry);
|
|
1596
|
-
}
|
|
1597
|
-
registered.push(`${name} -> ${geminiEvent}`);
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
});
|
|
1601
|
-
}
|
|
1602
|
-
catch (err) {
|
|
1603
|
-
errors.push(`Failed to write gemini settings.json: ${err.message}`);
|
|
1604
|
-
}
|
|
1605
|
-
return { registered, errors };
|
|
1606
|
-
}
|
|
1607
1651
|
/**
|
|
1608
1652
|
* Register hooks into antigravity's (agy) settings.json. Unlike gemini, agy uses
|
|
1609
1653
|
* a flat per-event array of `{ command }` entries (no matcher groups). Events
|
|
@@ -11,6 +11,18 @@
|
|
|
11
11
|
import type { Host } from './types.js';
|
|
12
12
|
import { type HostTask } from './tasks.js';
|
|
13
13
|
import { type HostCredentials } from './credentials.js';
|
|
14
|
+
/**
|
|
15
|
+
* Derive the remote directory to mirror from the local cwd, for a host run the
|
|
16
|
+
* caller gave no `--cwd`/`--remote-cwd`.
|
|
17
|
+
*
|
|
18
|
+
* Without this a `--host` run lands in the remote `$HOME`, so an agent launched
|
|
19
|
+
* from a repo starts with no project context and the user has to `cd` by hand.
|
|
20
|
+
* Only a cwd under the LOCAL home is mirrored — that is the part with a
|
|
21
|
+
* meaningful remote analogue (`~/src/x` re-roots onto the remote home). A path
|
|
22
|
+
* outside home returns undefined: `/opt/thing` on this box says nothing about
|
|
23
|
+
* the target's filesystem, so the run keeps the remote home.
|
|
24
|
+
*/
|
|
25
|
+
export declare function deriveMirroredCwd(localCwd: string): string | undefined;
|
|
14
26
|
/**
|
|
15
27
|
* Build a `cd <dir> && ` prefix that resolves on the REMOTE host.
|
|
16
28
|
*
|
|
@@ -18,8 +30,19 @@ import { type HostCredentials } from './credentials.js';
|
|
|
18
30
|
* the local one (`/home/<me>` vs `/Users/<me>`). We emit an unquoted `"$HOME"`
|
|
19
31
|
* for that segment — the remote login shell expands it — and shell-quote the
|
|
20
32
|
* remainder. Any other path (absolute or relative) is quoted verbatim.
|
|
33
|
+
*
|
|
34
|
+
* `mirror` marks a directory the caller DERIVED from the local cwd rather than
|
|
35
|
+
* one the user asked for (see `deriveMirroredCwd`). The same repo checked out at
|
|
36
|
+
* the same home-relative path on both boxes is the common fleet layout, so
|
|
37
|
+
* mirroring lands the remote agent in the project instead of `$HOME`. It is a
|
|
38
|
+
* best-effort mirror by definition — the host may simply not have that checkout
|
|
39
|
+
* — so a missing directory falls back to the remote home instead of failing the
|
|
40
|
+
* run. An explicit `--cwd`/`--remote-cwd` is never mirrored: the user named that
|
|
41
|
+
* directory, so a missing one must surface as a `cd` error.
|
|
21
42
|
*/
|
|
22
|
-
export declare function remoteCdPrefix(remoteCwd?: string
|
|
43
|
+
export declare function remoteCdPrefix(remoteCwd?: string, opts?: {
|
|
44
|
+
mirror?: boolean;
|
|
45
|
+
}): string;
|
|
23
46
|
/**
|
|
24
47
|
* Merge the resolved actor's provenance env UNDER a caller-supplied env, so every
|
|
25
48
|
* remote `agents …` invocation forwards `AGENTS_ACTOR*` / `GIT_*` across the SSH
|
|
@@ -103,6 +126,8 @@ export interface DispatchOptions {
|
|
|
103
126
|
/** Native-CLI passthrough (everything after `--`), appended last. */
|
|
104
127
|
passthroughArgs?: string[];
|
|
105
128
|
remoteCwd?: string;
|
|
129
|
+
/** `remoteCwd` was derived from the local cwd — mirror it, don't fail on it. */
|
|
130
|
+
mirrorCwd?: boolean;
|
|
106
131
|
/**
|
|
107
132
|
* Force the remote run's NEW session to use this exact id (Claude only, via
|
|
108
133
|
* `agents run --session-id`). Captured on the task record so the run is
|
|
@@ -165,6 +190,8 @@ export interface InteractiveDispatchOptions {
|
|
|
165
190
|
/** Route through the Agent Client Protocol. */
|
|
166
191
|
acp?: boolean;
|
|
167
192
|
remoteCwd?: string;
|
|
193
|
+
/** `remoteCwd` was derived from the local cwd — mirror it, don't fail on it. */
|
|
194
|
+
mirrorCwd?: boolean;
|
|
168
195
|
sessionId?: string;
|
|
169
196
|
name?: string;
|
|
170
197
|
resume?: string;
|