@phnx-labs/agents-cli 1.22.52 → 1.22.54
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 +336 -0
- package/README.md +42 -9
- package/dist/bootstrap.js +55 -154
- package/dist/cli/command-registry.d.ts +5 -0
- package/dist/cli/command-registry.js +8 -1
- package/dist/commands/accounts.js +220 -174
- package/dist/commands/apply.js +6 -3
- package/dist/commands/auth-mint.d.ts +8 -0
- package/dist/commands/auth-mint.js +96 -0
- package/dist/commands/auth.js +5 -1
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cost.js +8 -2
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/doctor.js +6 -1
- package/dist/commands/exec.js +26 -17
- package/dist/commands/fleet-capture.js +7 -0
- package/dist/commands/focus.d.ts +1 -0
- package/dist/commands/focus.js +4 -2
- package/dist/commands/go.d.ts +5 -4
- package/dist/commands/go.js +8 -7
- package/dist/commands/insights.js +9 -0
- package/dist/commands/monitors.js +85 -30
- package/dist/commands/output.js +8 -2
- package/dist/commands/repo.js +18 -0
- package/dist/commands/secrets.js +33 -14
- package/dist/commands/sessions-inject.js +8 -3
- package/dist/commands/sessions-picker.js +2 -1
- package/dist/commands/sessions.d.ts +20 -12
- package/dist/commands/sessions.js +94 -40
- package/dist/commands/setup-accounts.d.ts +8 -0
- package/dist/commands/setup-accounts.js +47 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +11 -2
- package/dist/commands/share.d.ts +52 -3
- package/dist/commands/share.js +262 -18
- package/dist/commands/ssh.d.ts +7 -0
- package/dist/commands/ssh.js +53 -14
- package/dist/commands/status.js +14 -0
- package/dist/commands/sync.js +44 -0
- package/dist/commands/view.d.ts +3 -1
- package/dist/commands/view.js +5 -4
- package/dist/lib/account-registry.d.ts +15 -5
- package/dist/lib/account-registry.js +165 -53
- package/dist/lib/accounting/rotate.d.ts +20 -6
- package/dist/lib/accounting/rotate.js +38 -7
- package/dist/lib/accounting/usage.d.ts +37 -1
- package/dist/lib/accounting/usage.js +71 -6
- package/dist/lib/agent-spec/agents.d.ts +5 -2
- package/dist/lib/agent-spec/agents.js +25 -7
- package/dist/lib/analytics/mix-commands.js +12 -6
- package/dist/lib/answer-router.js +2 -1
- package/dist/lib/auth-mint.d.ts +150 -0
- package/dist/lib/auth-mint.js +434 -0
- package/dist/lib/browser/profiles.d.ts +18 -0
- package/dist/lib/browser/profiles.js +26 -1
- package/dist/lib/browser/registry.d.ts +44 -14
- package/dist/lib/browser/registry.js +141 -45
- package/dist/lib/browser/remote-control.d.ts +9 -7
- package/dist/lib/browser/remote-control.js +9 -7
- package/dist/lib/claude-account-token.d.ts +10 -0
- package/dist/lib/claude-account-token.js +14 -4
- package/dist/lib/config-drift.d.ts +37 -0
- package/dist/lib/config-drift.js +72 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
- package/dist/lib/daemon/auth-sync-service.js +34 -0
- package/dist/lib/daemon/daemon.js +30 -4
- package/dist/lib/daemon/runner.js +10 -2
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.d.ts +3 -3
- package/dist/lib/device-config.js +8 -7
- package/dist/lib/devices/config-migration.js +147 -1
- package/dist/lib/devices/connect.d.ts +26 -0
- package/dist/lib/devices/connect.js +48 -1
- package/dist/lib/devices/device-docs.d.ts +35 -0
- package/dist/lib/devices/device-docs.js +163 -0
- package/dist/lib/devices/discovery-policy.d.ts +14 -2
- package/dist/lib/devices/discovery-policy.js +31 -21
- package/dist/lib/devices/doctor-findings.d.ts +5 -1
- package/dist/lib/devices/doctor-findings.js +19 -1
- package/dist/lib/devices/registry.d.ts +11 -5
- package/dist/lib/devices/registry.js +46 -18
- package/dist/lib/exec.d.ts +88 -30
- package/dist/lib/exec.js +138 -34
- package/dist/lib/feed/feed.d.ts +10 -2
- package/dist/lib/feed/feed.js +35 -2
- package/dist/lib/feed-broadcast.js +1 -1
- package/dist/lib/fleet/apply.d.ts +11 -0
- package/dist/lib/fleet/apply.js +23 -3
- package/dist/lib/fleet/auth-sync.js +5 -3
- package/dist/lib/help.d.ts +9 -0
- package/dist/lib/help.js +29 -1
- package/dist/lib/hosts/dispatch.d.ts +4 -3
- package/dist/lib/hosts/dispatch.js +12 -8
- package/dist/lib/hosts/passthrough.d.ts +1 -10
- package/dist/lib/hosts/passthrough.js +1 -13
- package/dist/lib/hosts/providers/local.d.ts +9 -3
- package/dist/lib/hosts/providers/local.js +23 -12
- package/dist/lib/hosts/reconnect.d.ts +7 -4
- package/dist/lib/hosts/reconnect.js +29 -25
- package/dist/lib/hosts/registry.js +4 -1
- package/dist/lib/hosts/remote-os.js +3 -1
- package/dist/lib/installations/versions.js +9 -1
- package/dist/lib/linux-userns.d.ts +58 -0
- package/dist/lib/linux-userns.js +116 -0
- package/dist/lib/memory.d.ts +26 -0
- package/dist/lib/memory.js +80 -1
- package/dist/lib/monitors/config.d.ts +11 -0
- package/dist/lib/monitors/config.js +8 -0
- package/dist/lib/monitors/engine.js +8 -1
- package/dist/lib/monitors/state.d.ts +37 -1
- package/dist/lib/monitors/state.js +79 -4
- package/dist/lib/permissions-registry.d.ts +2 -0
- package/dist/lib/permissions-registry.js +116 -14
- package/dist/lib/permissions.d.ts +5 -3
- package/dist/lib/permissions.js +25 -27
- package/dist/lib/profiles.d.ts +8 -7
- package/dist/lib/profiles.js +12 -0
- package/dist/lib/project-key.d.ts +9 -0
- package/dist/lib/project-key.js +11 -0
- package/dist/lib/secrets/bundles.d.ts +35 -0
- package/dist/lib/secrets/bundles.js +78 -1
- package/dist/lib/secrets/push.d.ts +3 -8
- package/dist/lib/secrets/push.js +18 -14
- package/dist/lib/secrets/remote.d.ts +9 -18
- package/dist/lib/secrets/remote.js +11 -26
- package/dist/lib/secrets/reserved-sync.d.ts +65 -0
- package/dist/lib/secrets/reserved-sync.js +129 -0
- package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
- package/dist/lib/self-heal/registry.js +4 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/active.d.ts +10 -1
- package/dist/lib/session/active.js +8 -5
- package/dist/lib/session/actor-sidecar.d.ts +7 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +39 -4
- package/dist/lib/session/db.js +168 -31
- package/dist/lib/session/discover.d.ts +32 -4
- package/dist/lib/session/discover.js +126 -37
- package/dist/lib/session/insights.d.ts +14 -0
- package/dist/lib/session/insights.js +25 -2
- package/dist/lib/session/linear.js +1 -1
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/pid-registry.d.ts +7 -0
- package/dist/lib/session/prompt.d.ts +15 -0
- package/dist/lib/session/prompt.js +21 -0
- package/dist/lib/session/shell-programs.d.ts +17 -0
- package/dist/lib/session/shell-programs.js +21 -0
- package/dist/lib/session/state.js +2 -1
- package/dist/lib/session/stream-render.js +2 -1
- package/dist/lib/session/tool-calls.js +2 -5
- package/dist/lib/session/trajectory-html.js +2 -1
- package/dist/lib/session/trajectory.js +3 -12
- package/dist/lib/session/types.d.ts +25 -0
- package/dist/lib/session/types.js +10 -0
- package/dist/lib/share/publish.d.ts +53 -5
- package/dist/lib/share/publish.js +99 -17
- package/dist/lib/share/worker-template.js +594 -64
- package/dist/lib/startup/root-command.js +2 -1
- package/dist/lib/state.d.ts +24 -0
- package/dist/lib/state.js +318 -54
- package/dist/lib/sync-status.d.ts +4 -0
- package/dist/lib/sync-status.js +3 -0
- package/dist/lib/terminal/resolve.d.ts +7 -0
- package/dist/lib/terminal/resolve.js +41 -2
- package/dist/lib/traces/classify.js +24 -19
- package/dist/lib/traces/insights.d.ts +67 -0
- package/dist/lib/traces/insights.js +178 -0
- package/dist/lib/traces/phenotype.d.ts +67 -0
- package/dist/lib/traces/phenotype.js +437 -0
- package/dist/lib/traces/segments.d.ts +133 -0
- package/dist/lib/traces/segments.js +301 -0
- package/dist/lib/traces/sync.d.ts +33 -0
- package/dist/lib/traces/sync.js +11 -2
- package/dist/lib/types.d.ts +47 -1
- package/dist/lib/usage-refresh.js +2 -1
- package/dist/lib/view-types.d.ts +2 -0
- package/dist/lib/watchdog/runner.js +18 -4
- package/package.json +2 -1
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
export function configureRootCommand(program, name, version) {
|
|
20
20
|
return program
|
|
21
21
|
.name(name)
|
|
22
|
-
.description('
|
|
22
|
+
.description('Install, configure, run, and dispatch AI coding agents from one place.\n' +
|
|
23
|
+
'Works with Claude, Codex, Antigravity, Cursor, OpenCode, OpenClaw, and Droid.')
|
|
23
24
|
.version(version)
|
|
24
25
|
.option('--verbose', 'Show startup self-heal details on stderr')
|
|
25
26
|
.helpOption('-h, --help', 'Show help')
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -391,6 +391,30 @@ export declare function getDeviceMetaPath(): string;
|
|
|
391
391
|
*/
|
|
392
392
|
export declare function getVersionResourcesPath(): string;
|
|
393
393
|
export declare function withMetaLock<T>(fn: () => T): T;
|
|
394
|
+
/**
|
|
395
|
+
* True when the top-level `~/.agents/agents.yaml` carries a metadata header that
|
|
396
|
+
* is NOT the canonical {@link META_HEADER} — the P1 frozen-header case a box only
|
|
397
|
+
* heals on its next central write (serializeCentral). An absent or headerless
|
|
398
|
+
* file is NOT stale (a headerless central file is deliberately left alone).
|
|
399
|
+
* Surfaced as config drift by `agents sync status` (PHNX-3315).
|
|
400
|
+
*/
|
|
401
|
+
export declare function hasStaleMetaHeader(): boolean;
|
|
402
|
+
/**
|
|
403
|
+
* Parse the top-level user `agents.yaml` (this box's synced central file) WITHOUT
|
|
404
|
+
* the system-repo merge, machine-local overlay, or cache — the raw on-disk central
|
|
405
|
+
* map, or null when the file is absent/unparseable. Used by config-drift detection
|
|
406
|
+
* to see the central blocks that should have folded into the device doc, without
|
|
407
|
+
* mis-attributing a system-repo default as this box's own leak (PHNX-3315).
|
|
408
|
+
*/
|
|
409
|
+
export declare function readTopLevelUserMeta(): Record<string, unknown> | null;
|
|
410
|
+
/**
|
|
411
|
+
* Write `meta` to disk (central + device docs + pins) WITHOUT taking the meta
|
|
412
|
+
* lock — the caller must already hold it via {@link withMetaLock}. Exported so a
|
|
413
|
+
* writer that needs to read fresh state, decide, and commit within a SINGLE lock
|
|
414
|
+
* acquisition (e.g. browser tombstone eviction) can do so without the
|
|
415
|
+
* read-snapshot-then-separately-lock race that {@link updateMeta} would impose.
|
|
416
|
+
*/
|
|
417
|
+
export declare function writeMetaUnlocked(meta: Meta): void;
|
|
394
418
|
/**
|
|
395
419
|
* Read and cache ~/.agents/agents.yaml, migrating from legacy locations if needed.
|
|
396
420
|
*
|
package/dist/lib/state.js
CHANGED
|
@@ -858,49 +858,99 @@ function writeIfChanged(filePath, content) {
|
|
|
858
858
|
* `agents:` / `versions:` are not written (no empty committed files).
|
|
859
859
|
*/
|
|
860
860
|
/**
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
*
|
|
867
|
-
*
|
|
868
|
-
* agents.yaml key-loss (beta flags, `notify.owner`, and `feed:` all vanished
|
|
869
|
-
* this way when an older-versioned CLI on the fleet rewrote the file).
|
|
861
|
+
* Fleet-shared (`central`) Meta keys — the OPT-IN allowlist. A key listed here is
|
|
862
|
+
* written to the synced `~/.agents/agents.yaml`. Device-scope is the DEFAULT:
|
|
863
|
+
* {@link metaKeyScope} returns `'device'` for every Meta key NOT listed here, so a
|
|
864
|
+
* newly-added key can never SILENTLY churn the shared file the way a
|
|
865
|
+
* central-by-default would — the very trap behind the recurring agents.yaml churn.
|
|
866
|
+
* Making a key fleet-shared is now a deliberate edit HERE, not the path of least
|
|
867
|
+
* resistance (PHNX-3315).
|
|
870
868
|
*/
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
869
|
+
const CENTRAL_META_KEYS = [
|
|
870
|
+
'accounts',
|
|
871
|
+
'run',
|
|
872
|
+
'model',
|
|
873
|
+
'watchdog',
|
|
874
|
+
'lease',
|
|
875
|
+
'secrets',
|
|
876
|
+
'budget',
|
|
877
|
+
'feed',
|
|
878
|
+
'beta',
|
|
879
|
+
'registries',
|
|
880
|
+
'profiles',
|
|
881
|
+
'source',
|
|
882
|
+
'extraRepos',
|
|
883
|
+
'brands',
|
|
884
|
+
'actors',
|
|
885
|
+
'seededPresets',
|
|
886
|
+
'hooks',
|
|
887
|
+
'config',
|
|
888
|
+
'hosts',
|
|
889
|
+
'fleet',
|
|
890
|
+
'share',
|
|
891
|
+
'notify',
|
|
892
|
+
];
|
|
893
|
+
/**
|
|
894
|
+
* Device-scoped Meta keys with BESPOKE routing — each lands in a special file
|
|
895
|
+
* (pins JSON / version-resources JSON) or a REMAPPED device-doc sub-block
|
|
896
|
+
* (`deviceHosts`->`hosts:`, `deviceFleet`->`fleet:`, ...), so their handling is
|
|
897
|
+
* hand-written in {@link writeMetaUnlocked} / {@link overlayMachineLocal} and kept
|
|
898
|
+
* behavior-identical. A device-scoped key NOT listed here is a GENERIC device key:
|
|
899
|
+
* it round-trips through `devices/<host>/agents.yaml` under its OWN name with no
|
|
900
|
+
* bespoke wiring (see the generic loops in those two functions). The `browser`
|
|
901
|
+
* tombstone is bespoke too — drained by lib/browser/registry.ts.
|
|
902
|
+
*/
|
|
903
|
+
const BESPOKE_DEVICE_KEYS = [
|
|
904
|
+
'agents',
|
|
905
|
+
'isolatedAgents',
|
|
906
|
+
'versions',
|
|
907
|
+
'deviceRoutines',
|
|
908
|
+
'deviceConfig',
|
|
909
|
+
'deviceBrowser',
|
|
910
|
+
'deviceFleet',
|
|
911
|
+
'deviceHosts',
|
|
912
|
+
'deviceAccounts',
|
|
913
|
+
'projectRoot',
|
|
914
|
+
];
|
|
915
|
+
const CENTRAL_KEY_SET = new Set(CENTRAL_META_KEYS);
|
|
916
|
+
const _metaKeysAreExhaustive = true;
|
|
917
|
+
void _metaKeysAreExhaustive;
|
|
918
|
+
/**
|
|
919
|
+
* Sync-domain of a Meta key. `'central'` ONLY for the opt-in allowlist; `'device'`
|
|
920
|
+
* by DEFAULT for everything else — so the classification is authoritative (it
|
|
921
|
+
* DRIVES the generic device-doc router below) rather than a decorative string map,
|
|
922
|
+
* and forgetting to classify a new key routes it to the SAFE per-box file.
|
|
923
|
+
*/
|
|
924
|
+
function metaKeyScope(key) {
|
|
925
|
+
return CENTRAL_KEY_SET.has(key) ? 'central' : 'device';
|
|
926
|
+
}
|
|
927
|
+
/** Bespoke device keys as a runtime Set (the generic router skips these). */
|
|
928
|
+
const BESPOKE_DEVICE_KEY_SET = new Set([
|
|
929
|
+
...BESPOKE_DEVICE_KEYS,
|
|
930
|
+
// The `browser` tombstone is device-scoped but bespoke: lib/browser/registry.ts
|
|
931
|
+
// drains it (collision-checked) into deviceBrowser, so the generic router must
|
|
932
|
+
// never blindly relocate it.
|
|
933
|
+
'browser',
|
|
934
|
+
]);
|
|
935
|
+
/**
|
|
936
|
+
* Device-doc sub-block names the read/write paths handle BESPOKE-ly (each maps to
|
|
937
|
+
* a different `device*` Meta key, or lives in a separate file). The generic
|
|
938
|
+
* device-doc overlay skips these so it only surfaces genuine generic keys.
|
|
939
|
+
*/
|
|
940
|
+
const BESPOKE_DEVICE_DOC_KEYS = new Set([
|
|
941
|
+
'agents',
|
|
942
|
+
'isolatedAgents',
|
|
943
|
+
'routines',
|
|
944
|
+
'config',
|
|
945
|
+
'browser',
|
|
946
|
+
'fleet',
|
|
947
|
+
'hosts',
|
|
948
|
+
'accounts',
|
|
949
|
+
'projectRoot',
|
|
950
|
+
// Legacy top-level doc key the config migration folds into `config:`; never
|
|
951
|
+
// surfaced onto Meta before, so keep the generic overlay from doing so.
|
|
952
|
+
'defaultBrowserProfile',
|
|
953
|
+
]);
|
|
904
954
|
/**
|
|
905
955
|
* Every key this version models (central + device). serializeCentral deletes an
|
|
906
956
|
* on-disk key only when it is KNOWN and absent from the write's in-memory object:
|
|
@@ -910,11 +960,91 @@ const META_KEY_SCOPE = {
|
|
|
910
960
|
* a newer CLI version added — is preserved verbatim, never dropped + synced away.
|
|
911
961
|
*/
|
|
912
962
|
const KNOWN_META_KEYS = new Set([
|
|
913
|
-
...
|
|
963
|
+
...CENTRAL_META_KEYS,
|
|
964
|
+
...BESPOKE_DEVICE_KEYS,
|
|
914
965
|
// Removed browser-profile store. Kept only as a serializer tombstone so the
|
|
915
966
|
// first registry read can migrate it into this device's file and delete it.
|
|
916
967
|
'browser',
|
|
917
968
|
]);
|
|
969
|
+
/**
|
|
970
|
+
* Rewrite a frozen `agents.yaml` header to the current {@link META_HEADER}.
|
|
971
|
+
*
|
|
972
|
+
* `serializeCentral` parses the existing file to preserve its hand-written body
|
|
973
|
+
* comments, but that also preserves the leading metadata header verbatim, so a
|
|
974
|
+
* top-level file written before the agi-cli rename (or before the `$schema` line
|
|
975
|
+
* existed) keeps its stale header forever — every freshly-written device doc gets
|
|
976
|
+
* the current header while the shared file is left behind (PHNX-3315).
|
|
977
|
+
*
|
|
978
|
+
* The header is healed TEXTUALLY, on the already-serialized string, rather than
|
|
979
|
+
* via `doc.commentBefore`: the `yaml` library folds the whole leading comment
|
|
980
|
+
* block onto the FIRST key's `commentBefore` when that key already carries a
|
|
981
|
+
* hand-written comment, so the header is not reliably the document comment — but
|
|
982
|
+
* it is always the top block of the output. Strip a leading `agents-cli metadata`
|
|
983
|
+
* header (any pre-rename variant, with or without the `$schema` line — the URL
|
|
984
|
+
* and schema lines are matched specifically so a hand-written body comment is
|
|
985
|
+
* never mistaken for a header line) and prepend the canonical header. A file with
|
|
986
|
+
* no recognizable header simply gains one. Body comments, which sit below the
|
|
987
|
+
* blank line that terminates the header, are untouched.
|
|
988
|
+
*/
|
|
989
|
+
function healMetaHeader(serialized) {
|
|
990
|
+
const headerBlock = /^# agents-cli metadata\n# Auto-generated - do not edit manually\n(?:# (?:https:\/\/github\.com\/phnx-labs\/[^\n]*|yaml-language-server: \$schema=[^\n]*)\n)*\n?/;
|
|
991
|
+
const stripped = serialized.replace(headerBlock, '');
|
|
992
|
+
// No metadata header present (replace was a no-op) → leave the file exactly as
|
|
993
|
+
// it is. We heal a STALE header; we never prepend one to a file that never had
|
|
994
|
+
// it (that would rewrite a hand-authored, headerless central file on the first
|
|
995
|
+
// real central change). A current header round-trips to the identical bytes.
|
|
996
|
+
return stripped === serialized ? serialized : META_HEADER + stripped;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* True when the top-level `~/.agents/agents.yaml` carries a metadata header that
|
|
1000
|
+
* is NOT the canonical {@link META_HEADER} — the P1 frozen-header case a box only
|
|
1001
|
+
* heals on its next central write (serializeCentral). An absent or headerless
|
|
1002
|
+
* file is NOT stale (a headerless central file is deliberately left alone).
|
|
1003
|
+
* Surfaced as config drift by `agents sync status` (PHNX-3315).
|
|
1004
|
+
*/
|
|
1005
|
+
export function hasStaleMetaHeader() {
|
|
1006
|
+
let content;
|
|
1007
|
+
try {
|
|
1008
|
+
content = fs.readFileSync(META_FILE, 'utf-8');
|
|
1009
|
+
}
|
|
1010
|
+
catch {
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
return healMetaHeader(content) !== content;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Parse the top-level user `agents.yaml` (this box's synced central file) WITHOUT
|
|
1017
|
+
* the system-repo merge, machine-local overlay, or cache — the raw on-disk central
|
|
1018
|
+
* map, or null when the file is absent/unparseable. Used by config-drift detection
|
|
1019
|
+
* to see the central blocks that should have folded into the device doc, without
|
|
1020
|
+
* mis-attributing a system-repo default as this box's own leak (PHNX-3315).
|
|
1021
|
+
*/
|
|
1022
|
+
export function readTopLevelUserMeta() {
|
|
1023
|
+
let content;
|
|
1024
|
+
try {
|
|
1025
|
+
content = fs.readFileSync(META_FILE, 'utf-8');
|
|
1026
|
+
}
|
|
1027
|
+
catch {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
try {
|
|
1031
|
+
const parsed = yaml.parse(content);
|
|
1032
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1033
|
+
return parsed;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
catch { /* malformed — nothing to report */ }
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Top-level keys currently on disk in the central `agents.yaml` ({} when the file
|
|
1041
|
+
* is absent or unparseable). The generic device-doc router uses this to leave a
|
|
1042
|
+
* FOREIGN key — one this version does not model, already written to central by a
|
|
1043
|
+
* newer CLI — in place instead of relocating it to this box's device doc.
|
|
1044
|
+
*/
|
|
1045
|
+
function readCentralKeys() {
|
|
1046
|
+
return new Set(Object.keys(readTopLevelUserMeta() ?? {}));
|
|
1047
|
+
}
|
|
918
1048
|
/**
|
|
919
1049
|
* Serialize the central (synced) meta to `agents.yaml` WITHOUT destroying the
|
|
920
1050
|
* hand-written comments in the committed file.
|
|
@@ -973,20 +1103,41 @@ function serializeCentral(central) {
|
|
|
973
1103
|
}
|
|
974
1104
|
}
|
|
975
1105
|
// No central field changed → keep the file byte-identical (comments intact), so
|
|
976
|
-
// writeIfChanged skips it and the churn loop never starts.
|
|
1106
|
+
// writeIfChanged skips it and the churn loop never starts. A device-only write
|
|
1107
|
+
// (pins/routines/etc. routed elsewhere) reaches here with changed=false and
|
|
1108
|
+
// MUST NOT rewrite the shared file — header healing waits for a genuine central
|
|
1109
|
+
// change below rather than dirtying agents.yaml on an unrelated write, which is
|
|
1110
|
+
// the very churn that wedges `agents sync` and blocks fleet pulls.
|
|
977
1111
|
if (!changed)
|
|
978
1112
|
return existing;
|
|
979
|
-
// Everything cleared → header only (never leave a flow `{}` behind).
|
|
980
|
-
//
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
//
|
|
984
|
-
//
|
|
985
|
-
// (
|
|
986
|
-
|
|
1113
|
+
// Everything cleared → header only (never leave a flow `{}` behind). Byte-stable
|
|
1114
|
+
// when the file is already exactly the current header.
|
|
1115
|
+
if (isEmpty)
|
|
1116
|
+
return existing === META_HEADER ? existing : META_HEADER;
|
|
1117
|
+
// A central key changed: serialize the edited doc and heal a frozen header on
|
|
1118
|
+
// the result. stringifyDoc still normalizes a legacy flow root (`{}`) to block,
|
|
1119
|
+
// so edited nodes do not render flow (`disabledCommands: [ teams ]` instead of a
|
|
1120
|
+
// `- teams` block list), but it no longer forces block on a normal document —
|
|
1121
|
+
// that flattened committed flow sequences and made this writer disagree with
|
|
1122
|
+
// feed.ts/activity.ts/migrate.ts on the same file (RUSH-2505). parseDocument
|
|
1123
|
+
// still preserves body comments + key ordering; healMetaHeader is a no-op unless
|
|
1124
|
+
// a stale metadata header is actually present, so a headerless central file is
|
|
1125
|
+
// updated in place without gaining one.
|
|
1126
|
+
return healMetaHeader(stringifyDoc(doc));
|
|
987
1127
|
}
|
|
988
|
-
|
|
989
|
-
|
|
1128
|
+
/**
|
|
1129
|
+
* Write `meta` to disk (central + device docs + pins) WITHOUT taking the meta
|
|
1130
|
+
* lock — the caller must already hold it via {@link withMetaLock}. Exported so a
|
|
1131
|
+
* writer that needs to read fresh state, decide, and commit within a SINGLE lock
|
|
1132
|
+
* acquisition (e.g. browser tombstone eviction) can do so without the
|
|
1133
|
+
* read-snapshot-then-separately-lock race that {@link updateMeta} would impose.
|
|
1134
|
+
*/
|
|
1135
|
+
export function writeMetaUnlocked(meta) {
|
|
1136
|
+
// INVARIANT: every key destructured here must also be in BESPOKE_DEVICE_KEYS (and
|
|
1137
|
+
// vice versa) — a bespoke device key that is classified but NOT pulled out here
|
|
1138
|
+
// would fall into `central`, and the generic router skips it (BESPOKE_DEVICE_KEY_SET),
|
|
1139
|
+
// so it would silently sync to the shared file. Keep the two lists in lockstep.
|
|
1140
|
+
const { agents, isolatedAgents, versions, deviceRoutines, deviceConfig, deviceBrowser, deviceFleet, deviceHosts, deviceAccounts, projectRoot, ...central } = meta;
|
|
990
1141
|
// Write the machine-local files FIRST, then strip central — so a crash mid-write
|
|
991
1142
|
// never removes pins/versions from central before they're persisted elsewhere.
|
|
992
1143
|
const hasAgents = !!agents && Object.keys(agents).length > 0;
|
|
@@ -1049,11 +1200,69 @@ function writeMetaUnlocked(meta) {
|
|
|
1049
1200
|
doc.browser = deviceBrowser;
|
|
1050
1201
|
else
|
|
1051
1202
|
delete doc.browser;
|
|
1203
|
+
// PHNX-3315 device-scoped fleet/hosts/accounts blocks. Each is this box's OWN
|
|
1204
|
+
// slice; the effective fleet view is unioned across every device doc at read
|
|
1205
|
+
// time (lib/devices/device-docs.ts). Empty slices are dropped so a box that
|
|
1206
|
+
// has made no decision leaves no key behind (no committed empty maps).
|
|
1207
|
+
const fleetDiscovery = deviceFleet?.discovery && Object.keys(deviceFleet.discovery).length > 0
|
|
1208
|
+
? deviceFleet.discovery : undefined;
|
|
1209
|
+
const fleetIgnored = deviceFleet?.ignored && deviceFleet.ignored.length > 0
|
|
1210
|
+
? deviceFleet.ignored : undefined;
|
|
1211
|
+
if (fleetDiscovery || fleetIgnored) {
|
|
1212
|
+
const df = {};
|
|
1213
|
+
if (fleetDiscovery)
|
|
1214
|
+
df.discovery = fleetDiscovery;
|
|
1215
|
+
if (fleetIgnored)
|
|
1216
|
+
df.ignored = fleetIgnored;
|
|
1217
|
+
doc.fleet = df;
|
|
1218
|
+
}
|
|
1219
|
+
else
|
|
1220
|
+
delete doc.fleet;
|
|
1221
|
+
const hasDeviceHosts = !!deviceHosts && Object.keys(deviceHosts).length > 0;
|
|
1222
|
+
if (hasDeviceHosts)
|
|
1223
|
+
doc.hosts = deviceHosts;
|
|
1224
|
+
else
|
|
1225
|
+
delete doc.hosts;
|
|
1226
|
+
const accountsNative = deviceAccounts?.native && Object.keys(deviceAccounts.native).length > 0
|
|
1227
|
+
? deviceAccounts.native : undefined;
|
|
1228
|
+
const accountsBindings = deviceAccounts?.bindings && Object.keys(deviceAccounts.bindings).length > 0
|
|
1229
|
+
? deviceAccounts.bindings : undefined;
|
|
1230
|
+
if (accountsNative || accountsBindings) {
|
|
1231
|
+
const da = {};
|
|
1232
|
+
if (accountsNative)
|
|
1233
|
+
da.native = accountsNative;
|
|
1234
|
+
if (accountsBindings)
|
|
1235
|
+
da.bindings = accountsBindings;
|
|
1236
|
+
doc.accounts = da;
|
|
1237
|
+
}
|
|
1238
|
+
else
|
|
1239
|
+
delete doc.accounts;
|
|
1052
1240
|
const hasProjectRoot = typeof projectRoot === 'string' && projectRoot.length > 0;
|
|
1053
1241
|
if (hasProjectRoot)
|
|
1054
1242
|
doc.projectRoot = projectRoot;
|
|
1055
1243
|
else
|
|
1056
1244
|
delete doc.projectRoot;
|
|
1245
|
+
// Generic device-scoped keys (PHNX-3315): any key left in `central` that this
|
|
1246
|
+
// version classifies as device but does NOT bespoke-route round-trips through
|
|
1247
|
+
// this box's device doc under its OWN name — so a NEWLY DECLARED device-scoped
|
|
1248
|
+
// key lands per-box BY DEFAULT, with no bespoke wiring, and never reaches the
|
|
1249
|
+
// synced central file. Today the bespoke set covers every device key, so this
|
|
1250
|
+
// loop moves nothing (behavior-identical). A key that is UNKNOWN to this version
|
|
1251
|
+
// AND already present in the on-disk central file is a FOREIGN key a newer CLI
|
|
1252
|
+
// wrote as central — left in `central` and preserved verbatim by serializeCentral
|
|
1253
|
+
// (the fleet-wide config-loss guard), never relocated to this box's doc.
|
|
1254
|
+
const centralRecord = central;
|
|
1255
|
+
const onDiskCentralKeys = readCentralKeys();
|
|
1256
|
+
for (const k of Object.keys(centralRecord)) {
|
|
1257
|
+
if (metaKeyScope(k) !== 'device')
|
|
1258
|
+
continue; // fleet-shared: stays central
|
|
1259
|
+
if (BESPOKE_DEVICE_KEY_SET.has(k))
|
|
1260
|
+
continue; // bespoke (incl. browser tombstone)
|
|
1261
|
+
if (!KNOWN_META_KEYS.has(k) && onDiskCentralKeys.has(k))
|
|
1262
|
+
continue; // foreign — preserve
|
|
1263
|
+
doc[k] = centralRecord[k];
|
|
1264
|
+
delete centralRecord[k];
|
|
1265
|
+
}
|
|
1057
1266
|
if (Object.keys(doc).length > 0) {
|
|
1058
1267
|
fs.mkdirSync(path.dirname(devicePath), { recursive: true });
|
|
1059
1268
|
writeIfChanged(devicePath, META_HEADER + yaml.stringify(doc));
|
|
@@ -1121,12 +1330,67 @@ function overlayMachineLocal(meta) {
|
|
|
1121
1330
|
if (dm?.config && typeof dm.config === 'object' && !Array.isArray(dm.config)) {
|
|
1122
1331
|
meta.deviceConfig = { ...meta.deviceConfig, ...dm.config };
|
|
1123
1332
|
}
|
|
1333
|
+
// PHNX-3315: this box's own device-scoped fleet/hosts/accounts slices.
|
|
1334
|
+
// These populate the `device*` keys the writers read-modify-write; the
|
|
1335
|
+
// effective UNION across all boxes is computed separately by
|
|
1336
|
+
// lib/devices/device-docs.ts, not here (this overlay is this box only).
|
|
1337
|
+
// A malformed block on THIS box's own doc is a HARD error, exactly like
|
|
1338
|
+
// `routines` below and the cross-box union readers (device-docs.ts): a
|
|
1339
|
+
// silent drop would let the next writeMetaUnlocked round-trip overwrite the
|
|
1340
|
+
// whole block with just the new entry, discarding the rest on this box's
|
|
1341
|
+
// tracked file (PHNX-3315).
|
|
1342
|
+
const isMap = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
1343
|
+
const dmRaw = dm;
|
|
1344
|
+
if (dmRaw.fleet !== undefined) {
|
|
1345
|
+
if (!isMap(dmRaw.fleet))
|
|
1346
|
+
throw new Error(`Device config corrupted at ${devicePath}: fleet must be a map.`);
|
|
1347
|
+
const df = dmRaw.fleet;
|
|
1348
|
+
if (df.discovery !== undefined && !isMap(df.discovery)) {
|
|
1349
|
+
throw new Error(`Device config corrupted at ${devicePath}: fleet.discovery must be a map.`);
|
|
1350
|
+
}
|
|
1351
|
+
if (df.ignored !== undefined && !Array.isArray(df.ignored)) {
|
|
1352
|
+
throw new Error(`Device config corrupted at ${devicePath}: fleet.ignored must be a list.`);
|
|
1353
|
+
}
|
|
1354
|
+
const discovery = df.discovery;
|
|
1355
|
+
const ignored = df.ignored;
|
|
1356
|
+
if (discovery || ignored)
|
|
1357
|
+
meta.deviceFleet = { ...(discovery ? { discovery } : {}), ...(ignored ? { ignored } : {}) };
|
|
1358
|
+
}
|
|
1359
|
+
if (dmRaw.hosts !== undefined) {
|
|
1360
|
+
if (!isMap(dmRaw.hosts))
|
|
1361
|
+
throw new Error(`Device config corrupted at ${devicePath}: hosts must be a map.`);
|
|
1362
|
+
meta.deviceHosts = { ...meta.deviceHosts, ...dmRaw.hosts };
|
|
1363
|
+
}
|
|
1364
|
+
if (dmRaw.accounts !== undefined) {
|
|
1365
|
+
if (!isMap(dmRaw.accounts))
|
|
1366
|
+
throw new Error(`Device config corrupted at ${devicePath}: accounts must be a map.`);
|
|
1367
|
+
const acc = dmRaw.accounts;
|
|
1368
|
+
if (acc.native !== undefined && !isMap(acc.native)) {
|
|
1369
|
+
throw new Error(`Device config corrupted at ${devicePath}: accounts.native must be a map.`);
|
|
1370
|
+
}
|
|
1371
|
+
if (acc.bindings !== undefined && !isMap(acc.bindings)) {
|
|
1372
|
+
throw new Error(`Device config corrupted at ${devicePath}: accounts.bindings must be a map.`);
|
|
1373
|
+
}
|
|
1374
|
+
const native = acc.native;
|
|
1375
|
+
const bindings = acc.bindings;
|
|
1376
|
+
if (native || bindings)
|
|
1377
|
+
meta.deviceAccounts = { ...(native ? { native } : {}), ...(bindings ? { bindings } : {}) };
|
|
1378
|
+
}
|
|
1124
1379
|
if (Object.prototype.hasOwnProperty.call(dm, 'routines')) {
|
|
1125
1380
|
if (!Array.isArray(dm.routines) || dm.routines.some((name) => typeof name !== 'string')) {
|
|
1126
1381
|
throw new Error(`Device config corrupted at ${devicePath}: routines must be a string list.`);
|
|
1127
1382
|
}
|
|
1128
1383
|
meta.deviceRoutines = dm.routines;
|
|
1129
1384
|
}
|
|
1385
|
+
// Generic device-scoped keys (PHNX-3315): device-doc keys this overlay does
|
|
1386
|
+
// NOT bespoke-handle map straight back onto Meta under their own name —
|
|
1387
|
+
// symmetry with the generic write in writeMetaUnlocked. The bespoke sub-blocks
|
|
1388
|
+
// are handled above and skipped, so a stock device doc surfaces nothing extra.
|
|
1389
|
+
for (const [k, v] of Object.entries(dm)) {
|
|
1390
|
+
if (BESPOKE_DEVICE_DOC_KEYS.has(k))
|
|
1391
|
+
continue;
|
|
1392
|
+
meta[k] = v;
|
|
1393
|
+
}
|
|
1130
1394
|
}
|
|
1131
1395
|
}
|
|
1132
1396
|
const vrPath = getVersionResourcesPath();
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { AgentId } from './types.js';
|
|
22
22
|
import { type DoctorKind } from './doctor-diff.js';
|
|
23
|
+
import { type ConfigDrift } from './config-drift.js';
|
|
23
24
|
/**
|
|
24
25
|
* One stable status per resource, unified across every surface.
|
|
25
26
|
* - `synced` — installed copy matches the resolved source (DiffStatus 'ok').
|
|
@@ -76,6 +77,9 @@ export interface UserRepoStatus {
|
|
|
76
77
|
export interface UnifiedSyncStatus {
|
|
77
78
|
system: SystemRepoStatus;
|
|
78
79
|
user: UserRepoStatus;
|
|
80
|
+
/** Config drift: has this box drained its device-scoped state, or is it still
|
|
81
|
+
* carrying per-box state in the shared top-level agents.yaml? (PHNX-3315) */
|
|
82
|
+
config: ConfigDrift;
|
|
79
83
|
agents: AgentVersionStatus[];
|
|
80
84
|
totals: {
|
|
81
85
|
drifted: number;
|
package/dist/lib/sync-status.js
CHANGED
|
@@ -26,6 +26,7 @@ import { loadManifest } from './staleness/index.js';
|
|
|
26
26
|
import { getSystemAgentsDir, getUserAgentsDir } from './state.js';
|
|
27
27
|
import * as fs from 'fs';
|
|
28
28
|
import { isGitRepo, readOriginUrl } from './git.js';
|
|
29
|
+
import { detectConfigDrift } from './config-drift.js';
|
|
29
30
|
const STATUS_MAP = {
|
|
30
31
|
ok: 'synced',
|
|
31
32
|
diff: 'drifted',
|
|
@@ -125,6 +126,7 @@ export async function computeSyncStatus(options = {}) {
|
|
|
125
126
|
}
|
|
126
127
|
const system = await getSystemRepoStatus();
|
|
127
128
|
const user = await getUserRepoStatus();
|
|
129
|
+
const config = detectConfigDrift();
|
|
128
130
|
const agentsNeedingSync = new Set();
|
|
129
131
|
let drifted = 0, missing = 0, orphan = 0, versionsNeedingSync = 0, versionsNeverSynced = 0;
|
|
130
132
|
for (const v of agents) {
|
|
@@ -141,6 +143,7 @@ export async function computeSyncStatus(options = {}) {
|
|
|
141
143
|
return {
|
|
142
144
|
system,
|
|
143
145
|
user,
|
|
146
|
+
config,
|
|
144
147
|
agents,
|
|
145
148
|
totals: {
|
|
146
149
|
drifted,
|
|
@@ -34,6 +34,7 @@ export type InjectResolution = {
|
|
|
34
34
|
} | {
|
|
35
35
|
addressable: false;
|
|
36
36
|
reason: string;
|
|
37
|
+
hint?: string;
|
|
37
38
|
};
|
|
38
39
|
export interface ResolveOptions {
|
|
39
40
|
/**
|
|
@@ -49,6 +50,12 @@ export interface ResolveOptions {
|
|
|
49
50
|
*/
|
|
50
51
|
ptyId?: string;
|
|
51
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Human-facing recovery hint for a session the resolver judged un-addressable.
|
|
55
|
+
* Tells the user both how to continue THIS session and how to make FUTURE runs
|
|
56
|
+
* addressable, so the failure is not silent and the fix is actionable.
|
|
57
|
+
*/
|
|
58
|
+
export declare function addressabilityRecoveryHint(session: ActiveSession, fallbackId?: string): string;
|
|
52
59
|
/**
|
|
53
60
|
* Resolve a target from an already-fetched ActiveSession. Pure — no I/O — so the
|
|
54
61
|
* precedence logic is unit-testable without the process table. This is where the
|
|
@@ -1,10 +1,40 @@
|
|
|
1
1
|
import { getActiveSessions } from '../session/active.js';
|
|
2
|
+
import { machineId } from '../machine-id.js';
|
|
2
3
|
/** The editor CLIs that speak the swarm-ext URI protocol, keyed by the host detectHost() reports. */
|
|
3
4
|
const IDE_INJECT_VARIANTS = {
|
|
4
5
|
codium: { cli: 'codium', scheme: 'vscodium' },
|
|
5
6
|
cursor: { cli: 'cursor', scheme: 'cursor' },
|
|
6
7
|
code: { cli: 'code', scheme: 'vscode' },
|
|
7
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* Human-facing recovery hint for a session the resolver judged un-addressable.
|
|
11
|
+
* Tells the user both how to continue THIS session and how to make FUTURE runs
|
|
12
|
+
* addressable, so the failure is not silent and the fix is actionable.
|
|
13
|
+
*/
|
|
14
|
+
export function addressabilityRecoveryHint(session, fallbackId) {
|
|
15
|
+
const sid = session.sessionId;
|
|
16
|
+
// Branch on the live sessionId (an IDE terminal that has not registered one
|
|
17
|
+
// yet is a distinct message), but render the resume command with the real id
|
|
18
|
+
// when the caller can supply it — e.g. `focus` has meta.id even though the
|
|
19
|
+
// live row's sessionId is falsy, so without this the hint printed a useless
|
|
20
|
+
// `agents sessions resume <id>` placeholder in exactly that case (PHNX-3070).
|
|
21
|
+
const resumeId = sid ?? fallbackId;
|
|
22
|
+
const shortId = resumeId ? resumeId.slice(0, 8) : '<id>';
|
|
23
|
+
const device = session.machine ?? machineId();
|
|
24
|
+
const resumeCmd = resumeId ? `agents sessions resume ${shortId}` : 'agents sessions resume <id>';
|
|
25
|
+
const tmuxCmd = `agents config set devices.${device}.tmux on`;
|
|
26
|
+
const interactive = session.context === 'terminal' || !!session.tty;
|
|
27
|
+
if (session.host === 'ghostty') {
|
|
28
|
+
return `Ghostty has no per-split addressing. ${interactive ? `Enable tmux wrapping with \`${tmuxCmd}\` and re-launch, or ` : ''}use \`${resumeCmd}\` to continue this session.`;
|
|
29
|
+
}
|
|
30
|
+
if (session.host && session.host in IDE_INJECT_VARIANTS && !sid) {
|
|
31
|
+
return `This IDE terminal has not registered a session id yet. Wait a moment and retry, or use \`${resumeCmd}\` to continue.`;
|
|
32
|
+
}
|
|
33
|
+
if (session.host) {
|
|
34
|
+
return `Host '${session.host}' has no addressable rail here. ${interactive ? `Enable tmux wrapping with \`${tmuxCmd}\` and re-launch, or ` : ''}use \`${resumeCmd}\` to continue this session.`;
|
|
35
|
+
}
|
|
36
|
+
return `This session has no addressable terminal rail (not tmux, iTerm, an IDE terminal, or a pty sidecar). ${interactive ? `Enable tmux wrapping with \`${tmuxCmd}\` and re-launch, or ` : ''}use \`${resumeCmd}\` to continue this session.`;
|
|
37
|
+
}
|
|
8
38
|
/**
|
|
9
39
|
* Resolve a target from an already-fetched ActiveSession. Pure — no I/O — so the
|
|
10
40
|
* precedence logic is unit-testable without the process table. This is where the
|
|
@@ -35,7 +65,11 @@ export function resolveInjectTargetForSession(session, opts = {}) {
|
|
|
35
65
|
const variant = session.host ? IDE_INJECT_VARIANTS[session.host] : undefined;
|
|
36
66
|
if (variant) {
|
|
37
67
|
if (!session.sessionId) {
|
|
38
|
-
return {
|
|
68
|
+
return {
|
|
69
|
+
addressable: false,
|
|
70
|
+
reason: `IDE terminal (${session.host}) has no session id to address`,
|
|
71
|
+
hint: addressabilityRecoveryHint(session),
|
|
72
|
+
};
|
|
39
73
|
}
|
|
40
74
|
return {
|
|
41
75
|
addressable: true,
|
|
@@ -58,13 +92,18 @@ export function resolveInjectTargetForSession(session, opts = {}) {
|
|
|
58
92
|
note: 'coarse Ghostty window path (opt-in): raises a window and types into the FOCUSED split — not split-precise',
|
|
59
93
|
};
|
|
60
94
|
}
|
|
61
|
-
return {
|
|
95
|
+
return {
|
|
96
|
+
addressable: false,
|
|
97
|
+
reason: 'un-addressable (ghostty, no tmux): no per-split addressing; watchdog skips',
|
|
98
|
+
hint: addressabilityRecoveryHint(session),
|
|
99
|
+
};
|
|
62
100
|
}
|
|
63
101
|
return {
|
|
64
102
|
addressable: false,
|
|
65
103
|
reason: session.host
|
|
66
104
|
? `no precise inject rail for host '${session.host}' (no tmux/iterm/IDE terminal detected)`
|
|
67
105
|
: 'no inject rail: session is not inside tmux, iTerm, or an IDE terminal',
|
|
106
|
+
hint: addressabilityRecoveryHint(session),
|
|
68
107
|
};
|
|
69
108
|
}
|
|
70
109
|
/**
|