@phnx-labs/agents-cli 1.20.76 → 1.20.78
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 +264 -0
- package/README.md +2 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.d.ts +9 -0
- package/dist/commands/doctor.js +143 -9
- package/dist/commands/exec.js +87 -19
- package/dist/commands/repo.js +8 -0
- package/dist/commands/routines.js +275 -15
- package/dist/commands/secrets.js +65 -17
- package/dist/commands/sessions-browser.d.ts +45 -3
- package/dist/commands/sessions-browser.js +128 -12
- package/dist/commands/sessions-export.d.ts +3 -0
- package/dist/commands/sessions-export.js +13 -10
- package/dist/commands/sessions-picker.d.ts +28 -1
- package/dist/commands/sessions-picker.js +222 -15
- package/dist/commands/sessions.d.ts +117 -4
- package/dist/commands/sessions.js +350 -62
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +48 -3
- package/dist/index.js +5 -6
- package/dist/lib/activity.d.ts +5 -4
- package/dist/lib/activity.js +450 -36
- package/dist/lib/agents.d.ts +0 -21
- package/dist/lib/agents.js +0 -37
- package/dist/lib/auto-pull.d.ts +16 -8
- package/dist/lib/auto-pull.js +23 -28
- package/dist/lib/cloud/session-index.js +2 -2
- package/dist/lib/daemon.d.ts +9 -32
- package/dist/lib/daemon.js +70 -84
- 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 +16 -1
- package/dist/lib/devices/fleet.js +10 -2
- package/dist/lib/devices/health-report.d.ts +10 -2
- package/dist/lib/devices/health-report.js +32 -1
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +16 -0
- package/dist/lib/exec.js +27 -4
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/git.d.ts +13 -0
- package/dist/lib/git.js +102 -4
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hosts/dispatch.d.ts +9 -0
- package/dist/lib/hosts/dispatch.js +24 -4
- package/dist/lib/hosts/passthrough.js +7 -2
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +33 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +4 -0
- package/dist/lib/hosts/run-target.js +5 -1
- package/dist/lib/hosts/session-index.js +3 -3
- 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/install-menubar.d.ts +9 -0
- package/dist/lib/menubar/install-menubar.js +14 -0
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/overdue.d.ts +4 -5
- package/dist/lib/overdue.js +8 -41
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +68 -0
- package/dist/lib/routines.js +74 -7
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +156 -28
- package/dist/lib/sandbox.js +0 -6
- 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 +11 -4
- package/dist/lib/secrets/agent.js +46 -24
- package/dist/lib/secrets/bundles.d.ts +21 -7
- package/dist/lib/secrets/bundles.js +82 -38
- package/dist/lib/secrets/index.d.ts +11 -2
- package/dist/lib/secrets/index.js +21 -9
- 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 +16 -2
- package/dist/lib/secrets/session-store.js +118 -15
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/session/active.d.ts +17 -0
- package/dist/lib/session/active.js +105 -7
- package/dist/lib/session/db.d.ts +43 -0
- package/dist/lib/session/db.js +191 -6
- package/dist/lib/session/digest.js +126 -21
- package/dist/lib/session/discover.d.ts +28 -3
- package/dist/lib/session/discover.js +110 -14
- package/dist/lib/session/origin-machine.d.ts +18 -0
- package/dist/lib/session/origin-machine.js +34 -0
- package/dist/lib/session/parse.js +11 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.js +18 -16
- package/dist/lib/session/state.d.ts +5 -0
- package/dist/lib/session/state.js +94 -12
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/smart-launch.d.ts +86 -0
- package/dist/lib/smart-launch.js +172 -0
- package/dist/lib/state.d.ts +7 -3
- package/dist/lib/state.js +20 -5
- package/dist/lib/types.d.ts +10 -0
- package/package.json +1 -1
package/dist/lib/feed.js
CHANGED
|
@@ -348,6 +348,10 @@ CLEAR_EVENTS = {
|
|
|
348
348
|
"Stop",
|
|
349
349
|
"SessionEnd",
|
|
350
350
|
}
|
|
351
|
+
# Codex emits a PermissionRequest event (not Claude's Notification) when it
|
|
352
|
+
# blocks on an approval prompt. Claude never fires PermissionRequest, so the
|
|
353
|
+
# same script handles both: PermissionRequest maps to an approval-class block
|
|
354
|
+
# with a high cost-of-delay so 'agents feed --dispatch' pages it as urgent.
|
|
351
355
|
|
|
352
356
|
|
|
353
357
|
def read_json(path):
|
|
@@ -398,6 +402,19 @@ def main():
|
|
|
398
402
|
hook_event = payload.get("hook_event_name", "PreToolUse")
|
|
399
403
|
|
|
400
404
|
if hook_event in CLEAR_EVENTS:
|
|
405
|
+
# A matcher-less PostToolUse clear (registered for Codex so an approved
|
|
406
|
+
# tool clears its approval card) must NOT wipe an open AskUserQuestion
|
|
407
|
+
# while an unrelated tool runs mid-question -- those are cleared only by
|
|
408
|
+
# the AskUserQuestion-matched PostToolUse. So on PostToolUse, keep a
|
|
409
|
+
# 'question' block; approval/notification blocks clear once the tool runs.
|
|
410
|
+
if hook_event == "PostToolUse":
|
|
411
|
+
try:
|
|
412
|
+
with open(target) as existing_file:
|
|
413
|
+
existing = json.load(existing_file)
|
|
414
|
+
if existing.get("kind") == "question" and payload.get("tool_name") != "AskUserQuestion":
|
|
415
|
+
return
|
|
416
|
+
except Exception:
|
|
417
|
+
pass
|
|
401
418
|
try:
|
|
402
419
|
os.unlink(target)
|
|
403
420
|
except FileNotFoundError:
|
|
@@ -442,6 +459,7 @@ def main():
|
|
|
442
459
|
return
|
|
443
460
|
|
|
444
461
|
notification_type = None
|
|
462
|
+
codex_approval = False
|
|
445
463
|
if hook_event == "Notification":
|
|
446
464
|
notification_type = payload.get("notification_type", "")
|
|
447
465
|
if notification_type not in WAITING_NOTIFICATION_TYPES:
|
|
@@ -466,6 +484,34 @@ def main():
|
|
|
466
484
|
"multiSelect": False,
|
|
467
485
|
}]
|
|
468
486
|
kind = "notification"
|
|
487
|
+
elif hook_event == "PermissionRequest":
|
|
488
|
+
# Codex approval prompt. The payload mirrors PreToolUse (tool_name,
|
|
489
|
+
# tool_input) but carries no questions -- Codex is asking to run a tool,
|
|
490
|
+
# not asking the operator a multiple-choice question. Publish it as a
|
|
491
|
+
# notification-kind approval block naming the tool so the feed and the
|
|
492
|
+
# phone notifier can surface it, and so the Factory extension can bridge
|
|
493
|
+
# it to a VS Code notification.
|
|
494
|
+
tool_name = payload.get("tool_name") or "a tool"
|
|
495
|
+
tool_input = payload.get("tool_input", {})
|
|
496
|
+
command = ""
|
|
497
|
+
if isinstance(tool_input, dict):
|
|
498
|
+
command = (
|
|
499
|
+
tool_input.get("command")
|
|
500
|
+
or tool_input.get("cmd")
|
|
501
|
+
or tool_input.get("path")
|
|
502
|
+
or ""
|
|
503
|
+
)
|
|
504
|
+
if isinstance(command, list):
|
|
505
|
+
command = " ".join(str(c) for c in command)
|
|
506
|
+
detail = f": {command}" if command else ""
|
|
507
|
+
normalized_questions = [{
|
|
508
|
+
"text": f"Codex needs approval to run {tool_name}{detail}",
|
|
509
|
+
"header": "Approval needed",
|
|
510
|
+
"multiSelect": False,
|
|
511
|
+
}]
|
|
512
|
+
kind = "notification"
|
|
513
|
+
notification_type = "permission_prompt"
|
|
514
|
+
codex_approval = True
|
|
469
515
|
else:
|
|
470
516
|
tool_input = payload.get("tool_input", {})
|
|
471
517
|
questions = tool_input.get("questions", [])
|
|
@@ -535,9 +581,21 @@ def main():
|
|
|
535
581
|
if notification_type:
|
|
536
582
|
block["notificationType"] = notification_type
|
|
537
583
|
|
|
584
|
+
# A Codex PermissionRequest is a real approval gate: mark it approval-class
|
|
585
|
+
# with a high cost-of-delay so 'agents feed --dispatch' classifies it urgent
|
|
586
|
+
# (isPhoneUrgent gates on costOfDelay >= phoneNotifyThreshold, default
|
|
587
|
+
# 'medium') and pages the phone. A plain 'deny' is the safe default.
|
|
588
|
+
if codex_approval:
|
|
589
|
+
block["blockClass"] = "approval"
|
|
590
|
+
block["costOfDelay"] = "high"
|
|
591
|
+
block["safeDefault"] = "deny"
|
|
592
|
+
|
|
538
593
|
# Optional multi-operator control metadata passed by the agent in the
|
|
539
|
-
# AskUserQuestion tool_input. Defaults keep the existing behavior.
|
|
540
|
-
|
|
594
|
+
# AskUserQuestion tool_input. Defaults keep the existing behavior. A Codex
|
|
595
|
+
# PermissionRequest carries tool ARGS in tool_input (command/path), not
|
|
596
|
+
# operator controls, so it is excluded here -- its class/cost is stamped
|
|
597
|
+
# above from codex_approval.
|
|
598
|
+
controls = payload.get("tool_input", {}) if hook_event not in ("Notification", "PermissionRequest") else {}
|
|
541
599
|
block_class = controls.get("blockClass") if isinstance(controls, dict) else None
|
|
542
600
|
if block_class in ("approval", "decision"):
|
|
543
601
|
block["blockClass"] = block_class
|
|
@@ -604,6 +662,15 @@ export const FEED_NOTIFICATION_HOOK_MANIFEST = {
|
|
|
604
662
|
script: '10-feed-publish.py',
|
|
605
663
|
timeout: 5,
|
|
606
664
|
};
|
|
665
|
+
// Codex fires PermissionRequest (not Claude's Notification) when it blocks on an
|
|
666
|
+
// approval prompt. The same script handles it, publishing a high-cost approval
|
|
667
|
+
// block so the feed dispatch pages the phone. PermissionRequest has no matcher.
|
|
668
|
+
export const FEED_PERMISSION_HOOK_MANIFEST = {
|
|
669
|
+
name: 'feed-publish-permission',
|
|
670
|
+
events: ['PermissionRequest'],
|
|
671
|
+
script: '10-feed-publish.py',
|
|
672
|
+
timeout: 5,
|
|
673
|
+
};
|
|
607
674
|
export const FEED_ANSWERED_HOOK_MANIFEST = {
|
|
608
675
|
name: 'feed-clear-answered',
|
|
609
676
|
events: ['PostToolUse'],
|
|
@@ -644,28 +711,51 @@ export function ensureFeedPublishHook(userAgentsDir = getUserAgentsDir()) {
|
|
|
644
711
|
}
|
|
645
712
|
const desiredHooks = {
|
|
646
713
|
'feed-publish': {
|
|
647
|
-
agents: ['claude'],
|
|
714
|
+
agents: ['claude', 'codex'],
|
|
648
715
|
events: ['PreToolUse'],
|
|
649
716
|
matcher: 'AskUserQuestion',
|
|
650
717
|
script: '10-feed-publish.py',
|
|
651
718
|
timeout: 5,
|
|
652
719
|
},
|
|
653
720
|
'feed-publish-notification': {
|
|
654
|
-
agents: ['claude'],
|
|
721
|
+
agents: ['claude', 'codex'],
|
|
655
722
|
events: ['Notification'],
|
|
656
723
|
matcher: 'permission_prompt|idle_prompt|elicitation_dialog',
|
|
657
724
|
script: '10-feed-publish.py',
|
|
658
725
|
timeout: 5,
|
|
659
726
|
},
|
|
727
|
+
// Codex-specific approval gate: Codex emits PermissionRequest (Claude does
|
|
728
|
+
// not), so this hook is where a blocked Codex agent surfaces to the feed.
|
|
729
|
+
'feed-publish-permission': {
|
|
730
|
+
agents: ['claude', 'codex'],
|
|
731
|
+
events: ['PermissionRequest'],
|
|
732
|
+
script: '10-feed-publish.py',
|
|
733
|
+
timeout: 5,
|
|
734
|
+
},
|
|
660
735
|
'feed-clear-answered': {
|
|
661
|
-
agents: ['claude'],
|
|
736
|
+
agents: ['claude', 'codex'],
|
|
662
737
|
events: ['PostToolUse'],
|
|
663
738
|
matcher: 'AskUserQuestion',
|
|
664
739
|
script: '10-feed-publish.py',
|
|
665
740
|
timeout: 5,
|
|
666
741
|
},
|
|
742
|
+
// Matcher-less PostToolUse clear: after Codex runs an approved tool, the
|
|
743
|
+
// approval card is stale, so clear it. Codex-only on purpose -- Claude
|
|
744
|
+
// never fires PermissionRequest, so it has no approval card to clear here,
|
|
745
|
+
// and a matcher-less PostToolUse for Claude would (1) re-run the script on
|
|
746
|
+
// every tool completion and (2) wipe Claude's notification-kind blocks
|
|
747
|
+
// (permission_prompt/idle_prompt/elicitation_dialog) the moment any later
|
|
748
|
+
// tool runs, instead of letting them persist to Stop/SessionEnd like they
|
|
749
|
+
// did before RUSH-2039. Registering it for codex alone keeps Claude's
|
|
750
|
+
// card lifetime exactly as it was.
|
|
751
|
+
'feed-clear-permission': {
|
|
752
|
+
agents: ['codex'],
|
|
753
|
+
events: ['PostToolUse'],
|
|
754
|
+
script: '10-feed-publish.py',
|
|
755
|
+
timeout: 5,
|
|
756
|
+
},
|
|
667
757
|
'feed-clear-lifecycle': {
|
|
668
|
-
agents: ['claude'],
|
|
758
|
+
agents: ['claude', 'codex'],
|
|
669
759
|
events: ['Stop', 'UserPromptSubmit', 'SessionEnd'],
|
|
670
760
|
script: '10-feed-publish.py',
|
|
671
761
|
timeout: 5,
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -86,6 +86,19 @@ export declare function clonePackage(source: string): Promise<{
|
|
|
86
86
|
}>;
|
|
87
87
|
/** Get the short commit hash (8 chars) of the latest commit in a repo. */
|
|
88
88
|
export declare function getRepoCommit(repoPath: string): Promise<string>;
|
|
89
|
+
/** Compact, self-contained state of a git repo — branch, short HEAD, and a
|
|
90
|
+
* dirty flag — for cross-device comparison (RUSH-2027). Synchronous and
|
|
91
|
+
* best-effort: a non-repo or unreadable path yields `null` fields, never a
|
|
92
|
+
* throw, so a device's `doctor --json` payload always serializes. */
|
|
93
|
+
export interface RepoStateSnapshot {
|
|
94
|
+
branch: string | null;
|
|
95
|
+
head: string | null;
|
|
96
|
+
dirty: boolean;
|
|
97
|
+
}
|
|
98
|
+
/** Read {@link RepoStateSnapshot} for `repoPath` using plumbing commands so the
|
|
99
|
+
* result is stable across git versions and never mutates the tree. Returns null
|
|
100
|
+
* when the path is not a git worktree. */
|
|
101
|
+
export declare function readRepoState(repoPath: string): RepoStateSnapshot | null;
|
|
89
102
|
/**
|
|
90
103
|
* Get the current GitHub username using gh CLI.
|
|
91
104
|
* Returns null if gh is not installed or user is not authenticated.
|
package/dist/lib/git.js
CHANGED
|
@@ -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.
|
|
@@ -813,6 +841,39 @@ export async function commitOwnDeviceMeta(dir, metaAbs = getDeviceMetaPath()) {
|
|
|
813
841
|
export async function pullRepo(dir) {
|
|
814
842
|
try {
|
|
815
843
|
const git = simpleGit(dir);
|
|
844
|
+
// A rebase left in progress by an earlier run must be reported as itself.
|
|
845
|
+
// Without this the dirty-tree guard below claims "Blocked by local changes",
|
|
846
|
+
// which is both wrong and actively harmful advice mid-rebase on a detached
|
|
847
|
+
// HEAD. Checked BEFORE commitOwnDeviceMeta so we never commit into one.
|
|
848
|
+
// Ask git where the state dirs live rather than assuming `<dir>/.git/` is a
|
|
849
|
+
// directory. In a worktree `.git` is a FILE containing `gitdir: <path>`, so
|
|
850
|
+
// path.join(dir, '.git', 'rebase-merge') can never exist and the check would
|
|
851
|
+
// silently never fire. `rev-parse --git-path` resolves both layouts.
|
|
852
|
+
const gitPath = async (name) => {
|
|
853
|
+
try {
|
|
854
|
+
const raw = (await git.raw(['rev-parse', '--git-path', name])).trim();
|
|
855
|
+
return path.isAbsolute(raw) ? raw : path.join(dir, raw);
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
const [rebaseMerge, rebaseApply] = await Promise.all([
|
|
862
|
+
gitPath('rebase-merge'),
|
|
863
|
+
gitPath('rebase-apply'),
|
|
864
|
+
]);
|
|
865
|
+
const rebaseInProgress = (rebaseMerge !== null && fs.existsSync(rebaseMerge)) ||
|
|
866
|
+
(rebaseApply !== null && fs.existsSync(rebaseApply));
|
|
867
|
+
if (rebaseInProgress) {
|
|
868
|
+
return {
|
|
869
|
+
success: false,
|
|
870
|
+
commit: '',
|
|
871
|
+
error: `A previous rebase is still in progress — finish or abort it, then pull again.\n\n` +
|
|
872
|
+
` cd ${displayHomePath(dir)} && git status\n` +
|
|
873
|
+
` git rebase --continue # after resolving\n` +
|
|
874
|
+
` git rebase --abort # to discard the attempt`,
|
|
875
|
+
};
|
|
876
|
+
}
|
|
816
877
|
// Commit this machine's own device-meta first so a per-machine pin change
|
|
817
878
|
// never wedges the pull. Genuine edits elsewhere still block below.
|
|
818
879
|
await commitOwnDeviceMeta(dir);
|
|
@@ -825,12 +886,15 @@ export async function pullRepo(dir) {
|
|
|
825
886
|
};
|
|
826
887
|
}
|
|
827
888
|
const branch = status.current || 'main';
|
|
828
|
-
await git.fetch('origin');
|
|
829
889
|
// Resolve the upstream ref to fast-forward against. Prefer the local
|
|
830
890
|
// branch's tracking config; otherwise ask origin for its default branch.
|
|
831
891
|
let tracking = status.tracking;
|
|
832
892
|
if (!tracking) {
|
|
893
|
+
// No tracking config: fetch origin so its HEAD is known, then ask which
|
|
894
|
+
// branch it points at. This path only ever concerns origin — a branch with
|
|
895
|
+
// no upstream has no other remote to consult.
|
|
833
896
|
try {
|
|
897
|
+
await git.fetch('origin');
|
|
834
898
|
await git.raw(['remote', 'set-head', 'origin', '--auto']);
|
|
835
899
|
const sym = await git.raw(['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']);
|
|
836
900
|
tracking = sym.trim();
|
|
@@ -839,6 +903,22 @@ export async function pullRepo(dir) {
|
|
|
839
903
|
tracking = `origin/${branch}`;
|
|
840
904
|
}
|
|
841
905
|
}
|
|
906
|
+
// Split the remote-tracking ref (<remote>/<branch>) into its parts and pull
|
|
907
|
+
// THOSE. Hardcoding 'origin' while comparing against `tracking` is how a
|
|
908
|
+
// branch tracking e.g. upstream/main silently 'succeeded': revparse saw a
|
|
909
|
+
// difference, the pull fetched origin/main (already current), and pullRepo
|
|
910
|
+
// returned success having moved nothing — the same "reported ok, pulled
|
|
911
|
+
// nothing" failure this change exists to remove. Branch names may contain
|
|
912
|
+
// slashes, so split on the FIRST separator only.
|
|
913
|
+
const sep = tracking.indexOf('/');
|
|
914
|
+
const remoteName = sep > 0 ? tracking.slice(0, sep) : 'origin';
|
|
915
|
+
const remoteBranch = sep > 0 ? tracking.slice(sep + 1) : branch;
|
|
916
|
+
// Bare fetch: updates every remote, so the revparse below sees a fresh ref
|
|
917
|
+
// whichever one the branch tracks. Deliberately argument-less — simple-git's
|
|
918
|
+
// fetchTask only forwards a remote when BOTH remote and branch are passed,
|
|
919
|
+
// so `fetch(remoteName)` would silently drop the argument and do exactly
|
|
920
|
+
// this anyway. Saying so beats an inert argument that reads as targeted.
|
|
921
|
+
await git.fetch();
|
|
842
922
|
const localRef = await git.revparse(['HEAD']);
|
|
843
923
|
const remoteRef = await git.revparse([tracking]).catch(() => null);
|
|
844
924
|
if (!remoteRef) {
|
|
@@ -853,13 +933,31 @@ export async function pullRepo(dir) {
|
|
|
853
933
|
};
|
|
854
934
|
}
|
|
855
935
|
try {
|
|
856
|
-
|
|
936
|
+
// Rebase, not --ff-only. Fast-forward refuses ANY divergence, conflict or
|
|
937
|
+
// not, so a single local commit — including the one commitOwnDeviceMeta
|
|
938
|
+
// makes just above — permanently wedged the pull with nothing actually in
|
|
939
|
+
// conflict. Every device carries its own devices/<host>/ path, so those
|
|
940
|
+
// replay cleanly. Matches syncRepoGit (below) and this function's own doc.
|
|
941
|
+
//
|
|
942
|
+
// Pull the RESOLVED tracking ref, not `branch`: when the local branch has
|
|
943
|
+
// no tracking config the block above falls back to origin's default head,
|
|
944
|
+
// which may be named differently (local `main` vs origin `master`). Using
|
|
945
|
+
// `branch` there asks origin for a ref it does not have.
|
|
946
|
+
assertValidBranchName(remoteBranch);
|
|
947
|
+
await git.pull(remoteName, remoteBranch, { '--rebase': 'true' });
|
|
857
948
|
}
|
|
858
|
-
catch {
|
|
949
|
+
catch (err) {
|
|
950
|
+
// Abort so the tree is restored, matching the atomicity --ff-only gave us.
|
|
951
|
+
// Without this a conflict leaves the repo detached, mid-rebase, with
|
|
952
|
+
// conflict markers written into live config (this repo is ~/.agents —
|
|
953
|
+
// agents.yaml and AGENTS.md are in it), and every later pull misreports
|
|
954
|
+
// the cause. `agents sync` reaches this path unattended across the fleet,
|
|
955
|
+
// so a wedged checkout would be worse than the bug this fixes.
|
|
956
|
+
await git.raw(['rebase', '--abort']).catch(() => { });
|
|
859
957
|
return {
|
|
860
958
|
success: false,
|
|
861
959
|
commit: '',
|
|
862
|
-
error: `
|
|
960
|
+
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
961
|
};
|
|
864
962
|
}
|
|
865
963
|
installGithooksSymlinks(dir);
|
package/dist/lib/heal.d.ts
CHANGED
|
@@ -87,10 +87,13 @@ export declare function healChangedAnything(r: HealResult): boolean;
|
|
|
87
87
|
/** One-line summary of a heal pass for daemon logs. */
|
|
88
88
|
export declare function summarizeHeal(r: HealResult): string;
|
|
89
89
|
/**
|
|
90
|
-
* Fire a
|
|
91
|
-
* noteworthy.
|
|
92
|
-
*
|
|
93
|
-
*
|
|
90
|
+
* Fire a branded desktop notification when a background heal did something
|
|
91
|
+
* noteworthy. Routed through the MenubarHelper companion (notify-desktop.ts) so
|
|
92
|
+
* it carries the agents-cli mark; clicking opens the runs folder
|
|
93
|
+
* (~/.agents/.history/runs, via the `routines:list` action the companion
|
|
94
|
+
* understands). Best-effort — a missing
|
|
95
|
+
* notifier or no display is swallowed. Silent when the pass auto-fixed everything
|
|
96
|
+
* and nothing needs the operator (no point pinging them for routine self-healing).
|
|
94
97
|
*/
|
|
95
98
|
export declare function notifyHeal(r: HealResult): void;
|
|
96
99
|
/**
|
package/dist/lib/heal.js
CHANGED
|
@@ -30,7 +30,7 @@ import { repairPluginManifestFile } from './plugin-marketplace.js';
|
|
|
30
30
|
import * as fs from 'fs';
|
|
31
31
|
import * as path from 'path';
|
|
32
32
|
import * as os from 'os';
|
|
33
|
-
import {
|
|
33
|
+
import { notifyDesktop } from './menubar/notify-desktop.js';
|
|
34
34
|
// ─── diff → selection mapping ────────────────────────────────────────────────
|
|
35
35
|
// Which ResourceSelection key each healable diff kind writes through. `rules`
|
|
36
36
|
// re-syncs via the whole-memory channel (not name-scoped); `promptcuts` is not
|
|
@@ -68,10 +68,13 @@ export function summarizeHeal(r) {
|
|
|
68
68
|
return parts.length > 0 ? parts.join(', ') : 'nothing to heal';
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* Fire a
|
|
72
|
-
* noteworthy.
|
|
73
|
-
*
|
|
74
|
-
*
|
|
71
|
+
* Fire a branded desktop notification when a background heal did something
|
|
72
|
+
* noteworthy. Routed through the MenubarHelper companion (notify-desktop.ts) so
|
|
73
|
+
* it carries the agents-cli mark; clicking opens the runs folder
|
|
74
|
+
* (~/.agents/.history/runs, via the `routines:list` action the companion
|
|
75
|
+
* understands). Best-effort — a missing
|
|
76
|
+
* notifier or no display is swallowed. Silent when the pass auto-fixed everything
|
|
77
|
+
* and nothing needs the operator (no point pinging them for routine self-healing).
|
|
75
78
|
*/
|
|
76
79
|
export function notifyHeal(r) {
|
|
77
80
|
const needsAttention = r.skippedPlugins.length;
|
|
@@ -79,27 +82,12 @@ export function notifyHeal(r) {
|
|
|
79
82
|
if (needsAttention === 0 && healed === 0)
|
|
80
83
|
return;
|
|
81
84
|
const title = needsAttention > 0
|
|
82
|
-
?
|
|
85
|
+
? `${needsAttention} plugin${needsAttention === 1 ? '' : 's'} need attention`
|
|
83
86
|
: 'agents: auto-healed config gaps';
|
|
84
87
|
const body = needsAttention > 0
|
|
85
88
|
? `${summarizeHeal(r)}. Run: agents doctor --fix`
|
|
86
89
|
: summarizeHeal(r);
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
if (platform === 'darwin') {
|
|
90
|
-
const safeTitle = title.replace(/"/g, '\\"');
|
|
91
|
-
const safeBody = body.replace(/"/g, '\\"');
|
|
92
|
-
const child = spawn('osascript', ['-e', `display notification "${safeBody}" with title "${safeTitle}"`], { detached: true, stdio: 'ignore' });
|
|
93
|
-
child.unref();
|
|
94
|
-
}
|
|
95
|
-
else if (platform === 'linux') {
|
|
96
|
-
const child = spawn('notify-send', [title, body], { detached: true, stdio: 'ignore' });
|
|
97
|
-
child.unref();
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
// Notification is best-effort; nothing to do.
|
|
102
|
-
}
|
|
90
|
+
notifyDesktop({ title, body, action: 'routines:list' });
|
|
103
91
|
}
|
|
104
92
|
// ─── central plugin layer (version-independent, runs once per heal) ──────────
|
|
105
93
|
/**
|
|
@@ -20,6 +20,15 @@ import { type HostCredentials } from './credentials.js';
|
|
|
20
20
|
* remainder. Any other path (absolute or relative) is quoted verbatim.
|
|
21
21
|
*/
|
|
22
22
|
export declare function remoteCdPrefix(remoteCwd?: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Merge the resolved actor's provenance env UNDER a caller-supplied env, so every
|
|
25
|
+
* remote `agents …` invocation forwards `AGENTS_ACTOR*` / `GIT_*` across the SSH
|
|
26
|
+
* hop. Without it the remote re-resolves the actor from the ORIGINATING box's
|
|
27
|
+
* `SSH_CONNECTION` (the wrong IP) and mis-credits the run to the shared machine or
|
|
28
|
+
* `UNRESOLVED@<host>` (RUSH-2028). A caller-supplied value wins on any key
|
|
29
|
+
* collision, mirroring `buildExecEnv`'s `...options.env` precedence (exec.ts).
|
|
30
|
+
*/
|
|
31
|
+
export declare function withActorEnv(env?: Record<string, string>): Record<string, string>;
|
|
23
32
|
/**
|
|
24
33
|
* Launch a detached login-shell command in its own Unix session/process group.
|
|
25
34
|
*
|
|
@@ -12,8 +12,9 @@ import { randomUUID } from 'crypto';
|
|
|
12
12
|
import { sshExec, sshStream, shellQuote } from '../ssh-exec.js';
|
|
13
13
|
import { sshTargetFor } from './types.js';
|
|
14
14
|
import { ensureHostReady } from './ready.js';
|
|
15
|
-
import { remoteShellFor } from './remote-cmd.js';
|
|
15
|
+
import { remoteShellFor, posixEnvExports } from './remote-cmd.js';
|
|
16
16
|
import { resolveRemoteOsSync } from './remote-os.js';
|
|
17
|
+
import { resolveActor, actorEnv } from '../actor.js';
|
|
17
18
|
import { saveTask, updateTask, terminalPatch } from './tasks.js';
|
|
18
19
|
import { followHostTask } from './progress.js';
|
|
19
20
|
import { wrapHostCommandWithCredentials } from './credentials.js';
|
|
@@ -56,6 +57,17 @@ export function remoteCdPrefix(remoteCwd) {
|
|
|
56
57
|
return `cd "$HOME"/${shellQuote(rest)} && `;
|
|
57
58
|
return `cd ${shellQuote(remoteCwd)} && `;
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Merge the resolved actor's provenance env UNDER a caller-supplied env, so every
|
|
62
|
+
* remote `agents …` invocation forwards `AGENTS_ACTOR*` / `GIT_*` across the SSH
|
|
63
|
+
* hop. Without it the remote re-resolves the actor from the ORIGINATING box's
|
|
64
|
+
* `SSH_CONNECTION` (the wrong IP) and mis-credits the run to the shared machine or
|
|
65
|
+
* `UNRESOLVED@<host>` (RUSH-2028). A caller-supplied value wins on any key
|
|
66
|
+
* collision, mirroring `buildExecEnv`'s `...options.env` precedence (exec.ts).
|
|
67
|
+
*/
|
|
68
|
+
export function withActorEnv(env) {
|
|
69
|
+
return { ...actorEnv(resolveActor()), ...(env ?? {}) };
|
|
70
|
+
}
|
|
59
71
|
/**
|
|
60
72
|
* Launch a detached login-shell command in its own Unix session/process group.
|
|
61
73
|
*
|
|
@@ -174,10 +186,14 @@ async function launchDetached(host, target, opts) {
|
|
|
174
186
|
const id = randomUUID().slice(0, 8);
|
|
175
187
|
const remoteLog = `${REMOTE_DIR}/${id}.log`;
|
|
176
188
|
const remoteExit = `${REMOTE_DIR}/${id}.exit`;
|
|
177
|
-
// Inner command run under a login shell so PATH resolves `agents`.
|
|
189
|
+
// Inner command run under a login shell so PATH resolves `agents`. Export the
|
|
190
|
+
// resolved actor provenance first so the detached remote run inherits it
|
|
191
|
+
// instead of re-resolving from this box's SSH_CONNECTION (RUSH-2028).
|
|
178
192
|
const invocation = ['agents', ...opts.forwardedArgs].map(shellQuote).join(' ');
|
|
179
193
|
const cwd = remoteCdPrefix(opts.remoteCwd);
|
|
180
|
-
|
|
194
|
+
const actorExports = posixEnvExports(withActorEnv());
|
|
195
|
+
const prelude = actorExports ? `${actorExports}; ` : '';
|
|
196
|
+
let inner = `${prelude}${cwd}${invocation} > ${remoteLog} 2>&1; echo $? > ${remoteExit}`;
|
|
181
197
|
if (opts.copyCreds) {
|
|
182
198
|
inner = wrapHostCommandWithCredentials(inner, opts.copyCreds);
|
|
183
199
|
}
|
|
@@ -370,7 +386,11 @@ export async function runInteractiveOnHost(host, opts) {
|
|
|
370
386
|
process.stderr.write(`[hosts] warning: ${w}\n`);
|
|
371
387
|
const invocation = ['agents', ...buildInteractiveRunForwardedArgs(opts)].map(shellQuote).join(' ');
|
|
372
388
|
const cwd = remoteCdPrefix(opts.remoteCwd);
|
|
373
|
-
|
|
389
|
+
// Forward actor provenance so the interactive remote run inherits it rather
|
|
390
|
+
// than re-resolving from this box's SSH_CONNECTION (RUSH-2028).
|
|
391
|
+
const actorExports = posixEnvExports(withActorEnv());
|
|
392
|
+
const prelude = actorExports ? `${actorExports}; ` : '';
|
|
393
|
+
let remoteCmd = `${prelude}${cwd}${invocation}`;
|
|
374
394
|
if (opts.copyCreds) {
|
|
375
395
|
remoteCmd = wrapHostCommandWithCredentials(remoteCmd, opts.copyCreds);
|
|
376
396
|
}
|
|
@@ -20,7 +20,7 @@ import chalk from 'chalk';
|
|
|
20
20
|
import { assertValidSshTarget, sshStream } from '../ssh-exec.js';
|
|
21
21
|
import { resolveHost, resolveHostByCap } from './registry.js';
|
|
22
22
|
import { sshTargetFor } from './types.js';
|
|
23
|
-
import { dispatchAgentsCommand } from './dispatch.js';
|
|
23
|
+
import { dispatchAgentsCommand, withActorEnv } from './dispatch.js';
|
|
24
24
|
import { stripRoutingFlags, buildRemoteAgentsInvocation, HOST_ROUTING_SPECS, } from './remote-cmd.js';
|
|
25
25
|
import { resolveRemoteOsSync } from './remote-os.js';
|
|
26
26
|
import { machineId } from '../session/sync/config.js';
|
|
@@ -267,9 +267,14 @@ export async function maybeRunOnHost(command, allArgs) {
|
|
|
267
267
|
// profile, and single-quoted env values would not expand $HOME/$PATH.
|
|
268
268
|
const isDoctorCommand = command === 'doctor' || (command === 'teams' && forwarded[1] === 'doctor');
|
|
269
269
|
const remoteOs = resolveRemoteOsSync(host.name);
|
|
270
|
-
const
|
|
270
|
+
const doctorPath = isDoctorCommand && !/^win/i.test((remoteOs ?? '').trim())
|
|
271
271
|
? { PATH: '$HOME/.agents/.cache/shims:$HOME/.local/bin:$PATH' }
|
|
272
272
|
: undefined;
|
|
273
|
+
// Forward actor provenance (AGENTS_ACTOR*/GIT_*) across the SSH hop, merged
|
|
274
|
+
// UNDER the doctor PATH so that PATH still wins — without this the remote
|
|
275
|
+
// re-resolves the actor from THIS box's SSH_CONNECTION and mis-credits it
|
|
276
|
+
// (RUSH-2028). Flows to both POSIX (export) and Windows ($env:) dialects.
|
|
277
|
+
const env = withActorEnv(doctorPath);
|
|
273
278
|
const remoteCmd = buildRemoteAgentsInvocation(forwarded, remoteCwd, remoteOs, env);
|
|
274
279
|
const code = sshStream(target, remoteCmd, { tty: interactive, multiplex: true });
|
|
275
280
|
if (code === 255) {
|
|
@@ -68,6 +68,17 @@ export declare const RUN_OPTION_REJECT_MESSAGES: Record<string, string>;
|
|
|
68
68
|
* POSIX form, so linux/macos are byte-for-byte unchanged.
|
|
69
69
|
*/
|
|
70
70
|
export declare function buildRemoteAgentsInvocation(forwardedArgs: string[], remoteCwd?: string, os?: string, env?: Record<string, string>): string;
|
|
71
|
+
/**
|
|
72
|
+
* Build a POSIX `export K=V; …` prefix from an env map — empty string when the
|
|
73
|
+
* map is missing or empty. Values are rendered as shell LITERALS by default
|
|
74
|
+
* (single-quoted via {@link shellQuote}), so a `$(...)` or backtick in a value
|
|
75
|
+
* can never execute on the SSH target. Only {@link EXPAND_KEYS} (`PATH`) keep the
|
|
76
|
+
* expanding double-quote form (`\` and `"` escaped) so the remote `$HOME`/`$PATH`
|
|
77
|
+
* still resolve. Shared by {@link buildRemoteAgentsInvocation} and the
|
|
78
|
+
* detached/interactive dispatch builders (dispatch.ts) so every remote path
|
|
79
|
+
* exports env identically.
|
|
80
|
+
*/
|
|
81
|
+
export declare function posixEnvExports(env?: Record<string, string>): string;
|
|
71
82
|
/** The two remote shell dialects we build commands for. */
|
|
72
83
|
export type RemoteShell = 'posix' | 'powershell';
|
|
73
84
|
/**
|
|
@@ -116,6 +116,8 @@ export const RUN_OPTION_FORWARDING = {
|
|
|
116
116
|
tailscale: 'local-only', // --tailscale/--no-tailscale gate the lease net mode; never forwarded
|
|
117
117
|
copyCreds: 'local-only', // copies creds TO the host before dispatch — local concern only
|
|
118
118
|
authCheck: 'local-only', // --no-auth-check gates the local interactive login preflight; --host runs skip that preflight entirely
|
|
119
|
+
// Deprecated alias for --device auto; resolved on the launching box before SSH.
|
|
120
|
+
smart: 'local-only',
|
|
119
121
|
};
|
|
120
122
|
/** Actionable messages for value-aware rejections, keyed by attribute name. */
|
|
121
123
|
export const RUN_OPTION_REJECT_MESSAGES = {
|
|
@@ -144,18 +146,41 @@ export function buildRemoteAgentsInvocation(forwardedArgs, remoteCwd, os, env) {
|
|
|
144
146
|
}
|
|
145
147
|
const inner = ['agents', ...forwardedArgs].map(shellQuote).join(' ');
|
|
146
148
|
const withCwd = remoteCwd ? `cd ${shellQuote(remoteCwd)} && ${inner}` : inner;
|
|
147
|
-
if (!env || Object.keys(env).length === 0) {
|
|
148
|
-
return `bash -lc ${shellQuote(withCwd)}`;
|
|
149
|
-
}
|
|
150
149
|
// Prepend env exports so the remote command sees the shims dir even when the
|
|
151
150
|
// login shell hasn't sourced the interactive rc files that usually add it.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
.join('; ');
|
|
151
|
+
const exports = posixEnvExports(env);
|
|
152
|
+
if (!exports) {
|
|
153
|
+
return `bash -lc ${shellQuote(withCwd)}`;
|
|
154
|
+
}
|
|
157
155
|
return `bash -lc ${shellQuote(`${exports}; ${withCwd}`)}`;
|
|
158
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Keys whose values are trusted-static and legitimately need remote shell
|
|
159
|
+
* expansion — `PATH` references the remote `$HOME`/`$PATH`. Every other key is
|
|
160
|
+
* rendered as a shell literal, so an attacker-influenceable value (notably actor
|
|
161
|
+
* provenance, whose name/email can come from a tailnet peer's whois or an
|
|
162
|
+
* unvalidated `AGENTS_ACTOR_*` env var) can never inject shell into a dispatch.
|
|
163
|
+
*/
|
|
164
|
+
const EXPAND_KEYS = new Set(['PATH']);
|
|
165
|
+
/**
|
|
166
|
+
* Build a POSIX `export K=V; …` prefix from an env map — empty string when the
|
|
167
|
+
* map is missing or empty. Values are rendered as shell LITERALS by default
|
|
168
|
+
* (single-quoted via {@link shellQuote}), so a `$(...)` or backtick in a value
|
|
169
|
+
* can never execute on the SSH target. Only {@link EXPAND_KEYS} (`PATH`) keep the
|
|
170
|
+
* expanding double-quote form (`\` and `"` escaped) so the remote `$HOME`/`$PATH`
|
|
171
|
+
* still resolve. Shared by {@link buildRemoteAgentsInvocation} and the
|
|
172
|
+
* detached/interactive dispatch builders (dispatch.ts) so every remote path
|
|
173
|
+
* exports env identically.
|
|
174
|
+
*/
|
|
175
|
+
export function posixEnvExports(env) {
|
|
176
|
+
if (!env || Object.keys(env).length === 0)
|
|
177
|
+
return '';
|
|
178
|
+
return Object.entries(env)
|
|
179
|
+
.map(([k, v]) => EXPAND_KEYS.has(k)
|
|
180
|
+
? `export ${shellQuote(k)}="${v.replace(/[\\"]/g, '\\$&')}"`
|
|
181
|
+
: `export ${shellQuote(k)}=${shellQuote(v)}`)
|
|
182
|
+
.join('; ');
|
|
183
|
+
}
|
|
159
184
|
/**
|
|
160
185
|
* Pick the remote shell dialect from a recorded OS/platform string. A Windows
|
|
161
186
|
* host (device-registry `platform: 'windows'`, or an enrolled `HostEntry.os`
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a remote-created session id back to an interactive `--host` launcher.
|
|
3
|
+
*
|
|
4
|
+
* A headless `--host` run captures the remote-coined id from the followed log
|
|
5
|
+
* (the `--emit-session-id` marker — see session-marker.ts). An INTERACTIVE run
|
|
6
|
+
* has no followed log: the local TTY is wired straight through `sshStream`
|
|
7
|
+
* (stdio:'inherit'), so nothing can tap the stream for a marker without breaking
|
|
8
|
+
* the agent's raw-mode TUI. Instead we correlate by AGENT_LAUNCH_ID.
|
|
9
|
+
*
|
|
10
|
+
* The launcher forwards a launch id it controls (`--env AGENT_LAUNCH_ID=<id>`);
|
|
11
|
+
* the remote `agents run` adopts it (exec.ts `resolveLaunchId`), so the remote
|
|
12
|
+
* SessionStart hook records the agent's REAL session id under that exact key in
|
|
13
|
+
* `~/.agents/.cache/terminals/sessions/<pid>.json`. After the stream returns we
|
|
14
|
+
* do ONE ssh read of that dir and pick the record whose `launch_id` matches —
|
|
15
|
+
* the same launch-id join `agents sessions --active` uses locally
|
|
16
|
+
* (session/hook-sessions.ts), just across the SSH hop.
|
|
17
|
+
*
|
|
18
|
+
* This gives the interactive path a real per-agent id to register in the local
|
|
19
|
+
* session index and to reconnect against, for Codex/Kimi/Grok/Gemini — closing
|
|
20
|
+
* the gap RUSH-2033 left for every non-Claude agent (Claude still forces its own
|
|
21
|
+
* id up front and never reaches here).
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Pick the real session id for `launchId` from the remote hook records.
|
|
25
|
+
*
|
|
26
|
+
* `recordsJson` is the newline-delimited JSON the remote read emits — one record
|
|
27
|
+
* per line (a `cat` of every `sessions/*.json`). Scan for the record whose
|
|
28
|
+
* `launch_id` matches and return its `session_id`; when several match (pid reuse,
|
|
29
|
+
* a lingering file) the NEWEST by `ts` wins, mirroring the local reader's
|
|
30
|
+
* keep-newest tie-break (session/hook-sessions.ts). Pure so the correlation is
|
|
31
|
+
* unit-testable from fixtures with no SSH. Returns undefined when no record
|
|
32
|
+
* carries the launch id (the hook hasn't landed yet, or a hookless harness).
|
|
33
|
+
*/
|
|
34
|
+
export declare function pickRemoteSessionId(recordsJson: string, launchId: string): string | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Read the remote hook-session dir over ssh and resolve the real session id the
|
|
37
|
+
* remote run recorded under `launchId`. One round-trip, best-effort: any ssh
|
|
38
|
+
* failure, a missing dir, or an absent record yields undefined — the caller then
|
|
39
|
+
* keeps the un-mapped run rather than a fabricated id.
|
|
40
|
+
*
|
|
41
|
+
* `$HOME` expands on the REMOTE login shell (never the local box). The glob is a
|
|
42
|
+
* literal path with no user input, so it is injection-safe; a `2>/dev/null` on a
|
|
43
|
+
* globless dir keeps the command quiet when the dir is empty or absent.
|
|
44
|
+
*/
|
|
45
|
+
export declare function resolveRemoteSessionId(target: string, launchId: string, timeoutMs?: number): string | undefined;
|