@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
package/dist/lib/help.js
CHANGED
|
@@ -7,6 +7,27 @@ const commandGroupRegistry = new WeakMap();
|
|
|
7
7
|
export function registerCommandGroups(parent, groups) {
|
|
8
8
|
commandGroupRegistry.set(parent, groups);
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Front-door command groups shown on `agents --help`. Derived from measured
|
|
12
|
+
* reach: tier 1 = setup/run/sessions/view, tier 2 = teams/browser/secrets/
|
|
13
|
+
* devices/accounts/add. The remaining groups stay discoverable through the
|
|
14
|
+
* pointer rendered below these groups.
|
|
15
|
+
*/
|
|
16
|
+
export const FRONT_DOOR_COMMAND_GROUPS = [
|
|
17
|
+
{
|
|
18
|
+
title: 'Quick start',
|
|
19
|
+
names: ['setup', 'view', 'run', 'sessions'],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
title: 'Most-used',
|
|
23
|
+
names: ['teams', 'browser', 'secrets', 'devices', 'accounts', 'add'],
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
const compactRootHelp = new WeakMap();
|
|
27
|
+
/** Mark the root program so its help only renders front-door groups + a pointer. */
|
|
28
|
+
export function setCompactRootHelp(program) {
|
|
29
|
+
compactRootHelp.set(program, true);
|
|
30
|
+
}
|
|
10
31
|
const helpSectionRegistry = new WeakMap();
|
|
11
32
|
/**
|
|
12
33
|
* Attach an Examples block (rendered between the description and Arguments)
|
|
@@ -136,7 +157,14 @@ function formatHelpCommandsFirst(cmd, helper) {
|
|
|
136
157
|
output = output.concat([`${title}:`, formatList(subs.map(renderCommand)), '']);
|
|
137
158
|
}
|
|
138
159
|
const remaining = visibleCommands.filter((s) => !placed.has(s.name()));
|
|
139
|
-
if (
|
|
160
|
+
if (compactRootHelp.get(cmd)) {
|
|
161
|
+
output = output.concat([
|
|
162
|
+
'Commands:',
|
|
163
|
+
` See "${cmd.name()} --help-all" for every command.`,
|
|
164
|
+
'',
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
else if (remaining.length > 0) {
|
|
140
168
|
output = output.concat(['Commands:', formatList(remaining.map(renderCommand)), '']);
|
|
141
169
|
}
|
|
142
170
|
}
|
|
@@ -40,9 +40,10 @@ export declare function withActorEnv(env?: Record<string, string>): Record<strin
|
|
|
40
40
|
*
|
|
41
41
|
* `extra` carries the markers that are true of ONE path rather than both — the
|
|
42
42
|
* interactive dispatch adds REMOTE_INTERACTIVE_ENV, which the remote CLI reads
|
|
43
|
-
* to
|
|
44
|
-
*
|
|
45
|
-
* same way, in one
|
|
43
|
+
* to know its stdio is an ssh link (reconnect target, `--no-follow` pane
|
|
44
|
+
* requirement). It goes through this builder rather than being concatenated on
|
|
45
|
+
* at the call site so every remote env marker is exported the same way, in one
|
|
46
|
+
* place.
|
|
46
47
|
*/
|
|
47
48
|
export declare function remoteRunShellPrelude(agent: string, extra?: Record<string, string>): string;
|
|
48
49
|
/**
|
|
@@ -78,9 +78,10 @@ export function withActorEnv(env) {
|
|
|
78
78
|
*
|
|
79
79
|
* `extra` carries the markers that are true of ONE path rather than both — the
|
|
80
80
|
* interactive dispatch adds REMOTE_INTERACTIVE_ENV, which the remote CLI reads
|
|
81
|
-
* to
|
|
82
|
-
*
|
|
83
|
-
* same way, in one
|
|
81
|
+
* to know its stdio is an ssh link (reconnect target, `--no-follow` pane
|
|
82
|
+
* requirement). It goes through this builder rather than being concatenated on
|
|
83
|
+
* at the call site so every remote env marker is exported the same way, in one
|
|
84
|
+
* place.
|
|
84
85
|
*/
|
|
85
86
|
export function remoteRunShellPrelude(agent, extra = {}) {
|
|
86
87
|
const guard = agent === RUN_AUTO_KEYWORD ? { [RUN_AUTO_HOST_RESOLVED_ENV]: '1' } : {};
|
|
@@ -497,11 +498,14 @@ export async function runInteractiveOnHost(host, opts) {
|
|
|
497
498
|
// than re-resolving from this box's SSH_CONNECTION (RUSH-2028); a `run auto`
|
|
498
499
|
// dispatch also gets the chain-hop guard (remoteRunShellPrelude).
|
|
499
500
|
//
|
|
500
|
-
// REMOTE_INTERACTIVE_ENV rides the same prelude and
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
501
|
+
// REMOTE_INTERACTIVE_ENV rides the same prelude and tells the remote CLI its
|
|
502
|
+
// stdio is this ssh link: it marks the run as reconnect-managed (a drop is
|
|
503
|
+
// answered by reconnect.ts, which rejoins the live pane or resumes the
|
|
504
|
+
// session in place) and, when the launcher has no TTY (CI, scripts), requires
|
|
505
|
+
// the detached pane that is the run's only interface. Since PHNX-3316 it no
|
|
506
|
+
// longer forces the tmux wrap on a TTY-followed run — the peer's
|
|
507
|
+
// tmux.enabled decides that. Set here, on the interactive path only:
|
|
508
|
+
// `launchDetached` already setsids the headless one.
|
|
505
509
|
const prelude = remoteRunShellPrelude(opts.agent, { [REMOTE_INTERACTIVE_ENV]: '1' });
|
|
506
510
|
let remoteCmd = `${prelude}${cwd}${invocation}`;
|
|
507
511
|
if (opts.copyCreds) {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* "not supported" message — never commander's raw `unknown option`.
|
|
18
18
|
*/
|
|
19
19
|
import { type Host } from './types.js';
|
|
20
|
-
import { type
|
|
20
|
+
import { type DeviceRegistry } from '../devices/registry.js';
|
|
21
21
|
import { runLocalCommand, runOnDevice } from '../devices/fleet.js';
|
|
22
22
|
/** Re-export for callers that historically imported flagValue from this module. */
|
|
23
23
|
export { flagValue, hasHostRoutingFlag } from './routing-flag.js';
|
|
@@ -69,15 +69,6 @@ export interface FleetPassthroughOptions {
|
|
|
69
69
|
/** Override this machine's id (tests). Defaults to `machineId()`. */
|
|
70
70
|
self?: string;
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
73
|
-
* Prefix a fan-out remote command so the far side sees AGENTS_FLEET_REMOTE=1 —
|
|
74
|
-
* the same marker the single-target dispatch sets via env. `wrapRemoteCommand`
|
|
75
|
-
* joins the argv with spaces (POSIX) or base64-encodes it for PowerShell, so a
|
|
76
|
-
* shell-appropriate leading token rides through both: `env VAR=1 …` on POSIX,
|
|
77
|
-
* `$env:VAR='1'; …` on PowerShell. Only remote (non-self) targets get it; the
|
|
78
|
-
* self target runs locally and must stay ungated.
|
|
79
|
-
*/
|
|
80
|
-
export declare function markFleetRemote(cmd: string[], device: DeviceProfile): string[];
|
|
81
72
|
/** Run `agents <command> …` across every registered device and render the roster. */
|
|
82
73
|
export declare function runFleetPassthrough(command: string, allArgs: string[], spec: RemoteSpec, opts?: FleetPassthroughOptions): Promise<boolean>;
|
|
83
74
|
/**
|
|
@@ -32,6 +32,7 @@ import { isDeviceInteractive, resolveInteractiveDevice, interactiveUnsetError, }
|
|
|
32
32
|
import { flagValue, hasHostRoutingFlag } from './routing-flag.js';
|
|
33
33
|
import { loadDevices } from '../devices/registry.js';
|
|
34
34
|
import { isSelfHost } from '../devices/self-host.js';
|
|
35
|
+
import { markFleetRemote } from '../devices/connect.js';
|
|
35
36
|
import { fanOutDevices, planFleetTargets, runLocalCommand, runOnDevice, } from '../devices/fleet.js';
|
|
36
37
|
import { platformGroupLabel } from '../devices/health-report.js';
|
|
37
38
|
import { isKnownTopLevelCommand } from '../startup/command-registry.js';
|
|
@@ -368,19 +369,6 @@ function renderFleetRoster(command, forwarded, results, self) {
|
|
|
368
369
|
console.log(chalk.gray(summaryParts.join(' · ')));
|
|
369
370
|
}
|
|
370
371
|
}
|
|
371
|
-
/**
|
|
372
|
-
* Prefix a fan-out remote command so the far side sees AGENTS_FLEET_REMOTE=1 —
|
|
373
|
-
* the same marker the single-target dispatch sets via env. `wrapRemoteCommand`
|
|
374
|
-
* joins the argv with spaces (POSIX) or base64-encodes it for PowerShell, so a
|
|
375
|
-
* shell-appropriate leading token rides through both: `env VAR=1 …` on POSIX,
|
|
376
|
-
* `$env:VAR='1'; …` on PowerShell. Only remote (non-self) targets get it; the
|
|
377
|
-
* self target runs locally and must stay ungated.
|
|
378
|
-
*/
|
|
379
|
-
export function markFleetRemote(cmd, device) {
|
|
380
|
-
return device.shell === 'powershell'
|
|
381
|
-
? [`$env:AGENTS_FLEET_REMOTE='1';`, ...cmd]
|
|
382
|
-
: ['env', 'AGENTS_FLEET_REMOTE=1', ...cmd];
|
|
383
|
-
}
|
|
384
372
|
/** Run `agents <command> …` across every registered device and render the roster. */
|
|
385
373
|
export async function runFleetPassthrough(command, allArgs, spec, opts = {}) {
|
|
386
374
|
const self = opts.self ?? machineId();
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
* Local host provider: the v1 directory.
|
|
3
3
|
*
|
|
4
4
|
* `list()` is the union of ssh-config `Host` stanzas (read-only, connection
|
|
5
|
-
* details owned by ssh) and inline entries the user registered
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* details owned by ssh) and inline entries the user registered. The host
|
|
6
|
+
* overlay (caps/os, keyed by name) is merged onto both. We never copy or
|
|
7
|
+
* rewrite ssh config.
|
|
8
|
+
*
|
|
9
|
+
* PHNX-3315: registrations are DEVICE-SCOPED — each box writes only its own
|
|
10
|
+
* `hosts:` block in `devices/<machine>/agents.yaml` (via `Meta.deviceHosts`),
|
|
11
|
+
* so N boxes no longer rewrite one shared `hosts:` map (the pull conflict).
|
|
12
|
+
* Reads are the cross-box UNION of every device doc, plus any lingering central
|
|
13
|
+
* legacy entries drained by the migration.
|
|
8
14
|
*/
|
|
9
15
|
import type { Host, HostProvider, HostProviderCapabilities } from '../types.js';
|
|
10
16
|
export declare class LocalHostProvider implements HostProvider {
|
|
@@ -2,14 +2,28 @@
|
|
|
2
2
|
* Local host provider: the v1 directory.
|
|
3
3
|
*
|
|
4
4
|
* `list()` is the union of ssh-config `Host` stanzas (read-only, connection
|
|
5
|
-
* details owned by ssh) and inline entries the user registered
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* details owned by ssh) and inline entries the user registered. The host
|
|
6
|
+
* overlay (caps/os, keyed by name) is merged onto both. We never copy or
|
|
7
|
+
* rewrite ssh config.
|
|
8
|
+
*
|
|
9
|
+
* PHNX-3315: registrations are DEVICE-SCOPED — each box writes only its own
|
|
10
|
+
* `hosts:` block in `devices/<machine>/agents.yaml` (via `Meta.deviceHosts`),
|
|
11
|
+
* so N boxes no longer rewrite one shared `hosts:` map (the pull conflict).
|
|
12
|
+
* Reads are the cross-box UNION of every device doc, plus any lingering central
|
|
13
|
+
* legacy entries drained by the migration.
|
|
8
14
|
*/
|
|
9
15
|
import { readMeta, updateMeta } from '../../state.js';
|
|
16
|
+
import { unionDeviceHosts } from '../../devices/device-docs.js';
|
|
10
17
|
import { listSshConfigHosts, isSshConfigHost } from '../ssh-config.js';
|
|
18
|
+
/** The EFFECTIVE host overlay: the cross-box union of every device doc's
|
|
19
|
+
* `hosts:` block, with any lingering central-legacy `hosts:` entries as a base
|
|
20
|
+
* (a device doc wins on a name collision). Newest `addedAt` wins across boxes. */
|
|
11
21
|
function entries() {
|
|
12
|
-
return readMeta().hosts
|
|
22
|
+
return { ...readMeta().hosts, ...unionDeviceHosts() };
|
|
23
|
+
}
|
|
24
|
+
/** THIS box's OWN host registrations (the writable slice in the device doc). */
|
|
25
|
+
function ownEntries(meta = readMeta()) {
|
|
26
|
+
return meta.deviceHosts ?? {};
|
|
13
27
|
}
|
|
14
28
|
function toHost(name, entry, enrolled) {
|
|
15
29
|
return {
|
|
@@ -63,19 +77,16 @@ export class LocalHostProvider {
|
|
|
63
77
|
...(spec.caps && spec.caps.length ? { caps: spec.caps } : {}),
|
|
64
78
|
addedAt: spec.addedAt ?? new Date().toISOString(),
|
|
65
79
|
};
|
|
66
|
-
|
|
80
|
+
// Device-scoped: land in THIS box's device doc, never the shared central map.
|
|
81
|
+
updateMeta((meta) => ({ ...meta, deviceHosts: { ...ownEntries(meta), [spec.name]: entry } }));
|
|
67
82
|
return toHost(spec.name, entry, true);
|
|
68
83
|
}
|
|
69
84
|
async remove(name) {
|
|
85
|
+
// A box only owns the registrations in its own device doc; drop from there.
|
|
70
86
|
updateMeta((meta) => {
|
|
71
|
-
const hosts = { ...(meta
|
|
87
|
+
const hosts = { ...ownEntries(meta) };
|
|
72
88
|
delete hosts[name];
|
|
73
|
-
|
|
74
|
-
if (Object.keys(hosts).length === 0) {
|
|
75
|
-
const { hosts: _omit, ...rest } = meta;
|
|
76
|
-
return rest;
|
|
77
|
-
}
|
|
78
|
-
return { ...meta, hosts };
|
|
89
|
+
return { ...meta, deviceHosts: hosts };
|
|
79
90
|
});
|
|
80
91
|
}
|
|
81
92
|
}
|
|
@@ -211,10 +211,13 @@ export declare function startConnectionTarget(opts: {
|
|
|
211
211
|
/**
|
|
212
212
|
* Decide what happens after an interactive `--device` stream returns.
|
|
213
213
|
*
|
|
214
|
-
* Auto-reconnect
|
|
215
|
-
* (`willReconnect`)
|
|
216
|
-
*
|
|
217
|
-
*
|
|
214
|
+
* Auto-reconnect fires whenever the link dropped (255) and the run did not opt
|
|
215
|
+
* out with `--raw` (`willReconnect`) — wrap or no wrap. A wrapped run's pane
|
|
216
|
+
* survived, so the reattach rejoins it; a bare run's agent died with the link,
|
|
217
|
+
* so the same verb resumes the session in place from disk (PHNX-3316). `--raw`
|
|
218
|
+
* never reconnects — but it still prints the session id: the user is at a
|
|
219
|
+
* shell, and EXEC-55 does not exempt raw (RUSH-3227). Bundling the notice
|
|
220
|
+
* behind `!isRaw` was the miss.
|
|
218
221
|
*/
|
|
219
222
|
export declare function afterInteractiveRemoteExit(opts: {
|
|
220
223
|
target?: ReconnectTarget;
|
|
@@ -2,34 +2,35 @@
|
|
|
2
2
|
* Auto-reconnect for an interactive `agents run --device` session whose
|
|
3
3
|
* SSH link dropped.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
5
|
+
* What the reconnect lands on depends on the peer's `tmux.enabled`
|
|
6
|
+
* (PHNX-3316). With the wrap opted in, the agent runs in a DETACHED tmux
|
|
7
|
+
* session on the peer (see lib/exec.ts `runInTmux`), so a network blink kills
|
|
8
|
+
* only the local ssh client and the reattach below rejoins the live pane.
|
|
9
|
+
* With the wrap off — the fleet default — the remote agent is a child of the
|
|
10
|
+
* sshd session and a blink SIGHUPs it: the in-flight turn is lost, and the
|
|
11
|
+
* reattach RESUMES the harness session from disk instead. Both outcomes go
|
|
12
|
+
* through the same verb below; what changed in PHNX-3316 is that "resumed"
|
|
13
|
+
* is once again an honest, expected result rather than a corpse this file
|
|
14
|
+
* pretended was alive (the pre-RUSH-3125 bug), and RUSH-3125's forced wrap —
|
|
15
|
+
* which made the pane a guarantee by overriding the operator's tmux.enabled —
|
|
16
|
+
* is gone with it.
|
|
17
17
|
*
|
|
18
18
|
* `sshStream` reports that drop as exit code 255 (ssh's
|
|
19
19
|
* own connection-layer failure; see ssh-exec.ts). Without this, exec.ts would
|
|
20
20
|
* `process.exit(255)` and the user would have to notice, find the session id, and
|
|
21
|
-
* `agents sessions focus` by hand. Instead we re-attach
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* `agents sessions focus` by hand. Instead we re-attach over SSH automatically,
|
|
22
|
+
* with bounded backoff, until the user detaches cleanly (the remote returns 0),
|
|
23
|
+
* the agent exits (a non-255 code), or the user interrupts the wait with
|
|
24
|
+
* Ctrl-C ({@link waitOrInterrupt} → 130).
|
|
25
25
|
*
|
|
26
26
|
* The re-attach reuses the peer's OWN recovery verb — `agents sessions focus <id>
|
|
27
27
|
* --local` — which JOINS the live local tmux pane there (a second client, no fork)
|
|
28
|
-
* when it
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* fall through to resume so the user is put back
|
|
32
|
-
* re-attach implementation (the peer's focus) to
|
|
28
|
+
* when it exists, and RESUMES the session in place when there is no pane — which
|
|
29
|
+
* is every drop on a default (wrap-off) box. Dropping `--attach-only` is
|
|
30
|
+
* deliberate: a reattach that finds no pane must not dead-end at a bare shell
|
|
31
|
+
* (the RUSH-2085 bug), it must fall through to resume so the user is put back
|
|
32
|
+
* into the agent. There is one re-attach implementation (the peer's focus) to
|
|
33
|
+
* keep in sync.
|
|
33
34
|
*
|
|
34
35
|
* **What it takes to refill the budget: reached the host AND held the pane.** ssh
|
|
35
36
|
* returns 255 for BOTH "couldn't connect at all" and "connected, then the link
|
|
@@ -314,10 +315,13 @@ export function startConnectionTarget(opts) {
|
|
|
314
315
|
/**
|
|
315
316
|
* Decide what happens after an interactive `--device` stream returns.
|
|
316
317
|
*
|
|
317
|
-
* Auto-reconnect
|
|
318
|
-
* (`willReconnect`)
|
|
319
|
-
*
|
|
320
|
-
*
|
|
318
|
+
* Auto-reconnect fires whenever the link dropped (255) and the run did not opt
|
|
319
|
+
* out with `--raw` (`willReconnect`) — wrap or no wrap. A wrapped run's pane
|
|
320
|
+
* survived, so the reattach rejoins it; a bare run's agent died with the link,
|
|
321
|
+
* so the same verb resumes the session in place from disk (PHNX-3316). `--raw`
|
|
322
|
+
* never reconnects — but it still prints the session id: the user is at a
|
|
323
|
+
* shell, and EXEC-55 does not exempt raw (RUSH-3227). Bundling the notice
|
|
324
|
+
* behind `!isRaw` was the miss.
|
|
321
325
|
*/
|
|
322
326
|
export function afterInteractiveRemoteExit(opts) {
|
|
323
327
|
if (!opts.target)
|
|
@@ -22,6 +22,7 @@ import { DevicesHostProvider } from './providers/devices.js';
|
|
|
22
22
|
import { assertValidSshTarget } from '../ssh-exec.js';
|
|
23
23
|
import { normalizeHost } from '../machine-id.js';
|
|
24
24
|
import { readMeta } from '../state.js';
|
|
25
|
+
import { unionDeviceHosts } from '../devices/device-docs.js';
|
|
25
26
|
import { isSshConfigHost } from './ssh-config.js';
|
|
26
27
|
import { resolveRemoteOsSync } from './remote-os.js';
|
|
27
28
|
import { loadDevices } from '../devices/registry.js';
|
|
@@ -187,7 +188,9 @@ export async function matchHost(name, opts = {}) {
|
|
|
187
188
|
catch {
|
|
188
189
|
reg = {};
|
|
189
190
|
}
|
|
190
|
-
|
|
191
|
+
// The effective host overlay is the cross-box union of every device doc's
|
|
192
|
+
// `hosts:` block (PHNX-3315), plus any lingering central-legacy entry.
|
|
193
|
+
const overlay = { ...readMeta().hosts, ...unionDeviceHosts() }[host];
|
|
191
194
|
// 1. A registered device (normalized match) — its live address/OS/presence win.
|
|
192
195
|
const device = matchDevice(host, reg);
|
|
193
196
|
if (device)
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { loadDevicesSync } from '../devices/registry.js';
|
|
20
20
|
import { readDeviceConfigValues } from '../device-config.js';
|
|
21
21
|
import { readMeta } from '../state.js';
|
|
22
|
+
import { unionDeviceHosts } from '../devices/device-docs.js';
|
|
22
23
|
/** Resolve the OS/platform string for a host name, or undefined if unknown. */
|
|
23
24
|
export function resolveRemoteOsSync(name) {
|
|
24
25
|
try {
|
|
@@ -33,5 +34,6 @@ export function resolveRemoteOsSync(name) {
|
|
|
33
34
|
// A corrupt/unreadable device registry must never break command building —
|
|
34
35
|
// fall through to the host overlay and ultimately the POSIX default.
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
+
// Cross-box union of the device-scoped host overlays, central legacy as base.
|
|
38
|
+
return ({ ...readMeta().hosts, ...unionDeviceHosts() }[name])?.os;
|
|
37
39
|
}
|
|
@@ -49,7 +49,7 @@ import { emit } from '../feed/events.js';
|
|
|
49
49
|
import { safeJoin } from '../paths.js';
|
|
50
50
|
import { readSkillSourceCommandMarker, shouldAlsoInstallCommandAsSkill, shouldInstallCommandAsSkill, } from '../command-skills.js';
|
|
51
51
|
import { getWriter, getDetector } from '../staleness/registry.js';
|
|
52
|
-
import { syncMemoryToVersionHome } from '../memory.js';
|
|
52
|
+
import { syncMemoryToVersionHome, syncClaudeProjectMemoryDir } from '../memory.js';
|
|
53
53
|
import { listPluginSkillNames, resolveCommandSource, resolveSkillSource } from '../staleness/writers/sources.js';
|
|
54
54
|
import { syncProjectResourcesToAgent } from '../project-resources.js';
|
|
55
55
|
import { installClaudeStatusLine } from '../claude-statusline.js';
|
|
@@ -2860,6 +2860,14 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2860
2860
|
if (supports(agent, 'memory', version).ok) {
|
|
2861
2861
|
syncMemoryToVersionHome(agent, versionHome, cwd);
|
|
2862
2862
|
}
|
|
2863
|
+
// Claude Code's own NATIVE per-project auto-memory (.claude/projects/<key>/memory/,
|
|
2864
|
+
// PHNX-2817) is a separate, unmanaged directory Claude writes into itself — make it
|
|
2865
|
+
// version-independent the same way project-level rules already are, via a shared
|
|
2866
|
+
// symlink, so a note survives an agent version upgrade instead of vanishing into a
|
|
2867
|
+
// fresh, empty version home.
|
|
2868
|
+
if (agent === 'claude') {
|
|
2869
|
+
syncClaudeProjectMemoryDir(versionHome, cwd);
|
|
2870
|
+
}
|
|
2863
2871
|
// Prune resources deleted from source (RUSH-2438). Runs only on a repo-scope
|
|
2864
2872
|
// reconcile (`options.prune`) with a caller selection — a full sync
|
|
2865
2873
|
// (`!userPassedSelection`) already sweeps orphans above, and an additive
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Path of the AppArmor knob that gates unprivileged userns on Ubuntu 23.10+. */
|
|
2
|
+
export declare const APPARMOR_USERNS_SYSCTL_PATH = "/proc/sys/kernel/apparmor_restrict_unprivileged_userns";
|
|
3
|
+
export type UsernsState =
|
|
4
|
+
/** A new user namespace with a uid map can be created — Codex's sandbox works. */
|
|
5
|
+
'ok'
|
|
6
|
+
/** Unprivileged userns is restricted — Codex's bwrap sandbox cannot start. */
|
|
7
|
+
| 'blocked'
|
|
8
|
+
/** Could not determine (non-Linux, or the probe could not run). */
|
|
9
|
+
| 'unknown';
|
|
10
|
+
export interface UsernsStatus {
|
|
11
|
+
state: UsernsState;
|
|
12
|
+
/** One-line human reason, present when `blocked` or `unknown`. */
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
/** The raw signals the pure interpreter reasons over. */
|
|
16
|
+
export interface UsernsInputs {
|
|
17
|
+
platform: NodeJS.Platform;
|
|
18
|
+
/**
|
|
19
|
+
* Contents of {@link APPARMOR_USERNS_SYSCTL_PATH} trimmed, or null when the
|
|
20
|
+
* file is absent (older kernels / no AppArmor userns mediation).
|
|
21
|
+
*/
|
|
22
|
+
apparmorRestrict: string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Result of actually attempting to create a user namespace with a uid map:
|
|
25
|
+
* - 'ok' → the probe created the namespace and mapped root.
|
|
26
|
+
* - 'denied' → the kernel refused the uid_map write (the restricted case).
|
|
27
|
+
* - 'no-tool' → the probe binary (`unshare`) was missing or failed to spawn.
|
|
28
|
+
*/
|
|
29
|
+
unshareProbe: 'ok' | 'denied' | 'no-tool';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Decide userns availability from raw signals. Pure — no I/O.
|
|
33
|
+
*
|
|
34
|
+
* The definitive signal is the actual probe: if we successfully created a userns
|
|
35
|
+
* and wrote a uid map, the sandbox works regardless of the sysctl (an AppArmor
|
|
36
|
+
* profile may grant a specific binary `userns` even while the global knob is 1).
|
|
37
|
+
* A denied probe is a hard `blocked`. When the probe tool is missing we fall back
|
|
38
|
+
* to the sysctl: `1` → `blocked`, `0`/absent → `unknown` (we could not prove it,
|
|
39
|
+
* and refuse to claim `ok` we did not observe).
|
|
40
|
+
*/
|
|
41
|
+
export declare function interpretUsernsInputs(inputs: UsernsInputs): UsernsStatus;
|
|
42
|
+
/** Read the AppArmor userns sysctl, or null when the file is absent. */
|
|
43
|
+
export declare function readApparmorRestrict(sysctlPath?: string): string | null;
|
|
44
|
+
/**
|
|
45
|
+
* Actually try to create a user namespace and map root inside it — the same
|
|
46
|
+
* operation bwrap performs (`unshare --user --map-root-user`). This is the ground
|
|
47
|
+
* truth: it observes exactly what the kernel/AppArmor policy permits for *this*
|
|
48
|
+
* process, rather than inferring from the sysctl alone.
|
|
49
|
+
*/
|
|
50
|
+
export declare function probeUnshare(): 'ok' | 'denied' | 'no-tool';
|
|
51
|
+
/**
|
|
52
|
+
* Resolve whether an unprivileged user namespace can be created on this host,
|
|
53
|
+
* cached for the process (the answer is a stable property of the box). Non-Linux
|
|
54
|
+
* short-circuits to `ok` without spawning anything.
|
|
55
|
+
*/
|
|
56
|
+
export declare function probeUnprivilegedUserns(platform?: NodeJS.Platform): UsernsStatus;
|
|
57
|
+
/** Test-only: drop the process cache so a test can re-probe. */
|
|
58
|
+
export declare function resetUsernsCacheForTests(): void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unprivileged user-namespace availability on Linux — the capability Codex's
|
|
3
|
+
* Linux sandbox needs, and the one Ubuntu 23.10+ restricts by default.
|
|
4
|
+
*
|
|
5
|
+
* Codex ≥0.146 implements its `read-only` and `workspace-write` sandbox modes on
|
|
6
|
+
* Linux with a bundled **bubblewrap** (`bwrap`), extracted per-run to
|
|
7
|
+
* `$CODEX_HOME/tmp/arg0/codex-XXXX/` and exec'd from a memfd. bwrap sets up its
|
|
8
|
+
* mounts inside a fresh **unprivileged user namespace** (`--unshare-user`, then a
|
|
9
|
+
* write to `/proc/self/uid_map`). Ubuntu 24.04 ships
|
|
10
|
+
* `kernel.apparmor_restrict_unprivileged_userns=1`, which denies that to an
|
|
11
|
+
* unconfined binary — so bwrap dies with `bwrap: setting up uid map: Permission
|
|
12
|
+
* denied` and a headless Codex run lands zero tools (no file writes, no shell).
|
|
13
|
+
* `danger-full-access` (our `skip` mode) drops the sandbox and is the only mode
|
|
14
|
+
* that avoids bwrap; the legacy Landlock backend is gone (`use_linux_sandbox_bwrap`
|
|
15
|
+
* is `removed`, `use_legacy_landlock` panics under the permission-profile model).
|
|
16
|
+
*
|
|
17
|
+
* This module is the single detector. It is pure at its core
|
|
18
|
+
* ({@link interpretUsernsInputs}) so the decision is unit-testable without a
|
|
19
|
+
* shell, and {@link probeUnprivilegedUserns} gathers the real inputs once per
|
|
20
|
+
* process. See PHNX-3285.
|
|
21
|
+
*/
|
|
22
|
+
import { execFileSync } from 'child_process';
|
|
23
|
+
import * as fs from 'fs';
|
|
24
|
+
/** Path of the AppArmor knob that gates unprivileged userns on Ubuntu 23.10+. */
|
|
25
|
+
export const APPARMOR_USERNS_SYSCTL_PATH = '/proc/sys/kernel/apparmor_restrict_unprivileged_userns';
|
|
26
|
+
/**
|
|
27
|
+
* Decide userns availability from raw signals. Pure — no I/O.
|
|
28
|
+
*
|
|
29
|
+
* The definitive signal is the actual probe: if we successfully created a userns
|
|
30
|
+
* and wrote a uid map, the sandbox works regardless of the sysctl (an AppArmor
|
|
31
|
+
* profile may grant a specific binary `userns` even while the global knob is 1).
|
|
32
|
+
* A denied probe is a hard `blocked`. When the probe tool is missing we fall back
|
|
33
|
+
* to the sysctl: `1` → `blocked`, `0`/absent → `unknown` (we could not prove it,
|
|
34
|
+
* and refuse to claim `ok` we did not observe).
|
|
35
|
+
*/
|
|
36
|
+
export function interpretUsernsInputs(inputs) {
|
|
37
|
+
if (inputs.platform !== 'linux')
|
|
38
|
+
return { state: 'ok' };
|
|
39
|
+
if (inputs.unshareProbe === 'ok')
|
|
40
|
+
return { state: 'ok' };
|
|
41
|
+
if (inputs.unshareProbe === 'denied') {
|
|
42
|
+
const via = inputs.apparmorRestrict === '1'
|
|
43
|
+
? ' (kernel.apparmor_restrict_unprivileged_userns=1)'
|
|
44
|
+
: '';
|
|
45
|
+
return {
|
|
46
|
+
state: 'blocked',
|
|
47
|
+
reason: `the kernel denied creating an unprivileged user namespace${via}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// Probe tool unavailable — lean on the AppArmor knob.
|
|
51
|
+
if (inputs.apparmorRestrict === '1') {
|
|
52
|
+
return {
|
|
53
|
+
state: 'blocked',
|
|
54
|
+
reason: 'unprivileged user namespaces are AppArmor-restricted ' +
|
|
55
|
+
'(kernel.apparmor_restrict_unprivileged_userns=1) and `unshare` was not available to confirm',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
state: 'unknown',
|
|
60
|
+
reason: '`unshare` was not available to probe user-namespace support',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Read the AppArmor userns sysctl, or null when the file is absent. */
|
|
64
|
+
export function readApparmorRestrict(sysctlPath = APPARMOR_USERNS_SYSCTL_PATH) {
|
|
65
|
+
try {
|
|
66
|
+
return fs.readFileSync(sysctlPath, 'utf8').trim();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Actually try to create a user namespace and map root inside it — the same
|
|
74
|
+
* operation bwrap performs (`unshare --user --map-root-user`). This is the ground
|
|
75
|
+
* truth: it observes exactly what the kernel/AppArmor policy permits for *this*
|
|
76
|
+
* process, rather than inferring from the sysctl alone.
|
|
77
|
+
*/
|
|
78
|
+
export function probeUnshare() {
|
|
79
|
+
try {
|
|
80
|
+
execFileSync('unshare', ['--user', '--map-root-user', 'true'], {
|
|
81
|
+
stdio: 'ignore',
|
|
82
|
+
timeout: 5000,
|
|
83
|
+
});
|
|
84
|
+
return 'ok';
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
// ENOENT / spawn failure → the tool isn't here; anything else (nonzero exit
|
|
88
|
+
// from the denied uid_map write) is the restricted case.
|
|
89
|
+
const code = err?.code;
|
|
90
|
+
if (code === 'ENOENT')
|
|
91
|
+
return 'no-tool';
|
|
92
|
+
return 'denied';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
let cached = null;
|
|
96
|
+
/**
|
|
97
|
+
* Resolve whether an unprivileged user namespace can be created on this host,
|
|
98
|
+
* cached for the process (the answer is a stable property of the box). Non-Linux
|
|
99
|
+
* short-circuits to `ok` without spawning anything.
|
|
100
|
+
*/
|
|
101
|
+
export function probeUnprivilegedUserns(platform = process.platform) {
|
|
102
|
+
if (platform !== 'linux')
|
|
103
|
+
return { state: 'ok' };
|
|
104
|
+
if (cached)
|
|
105
|
+
return cached;
|
|
106
|
+
cached = interpretUsernsInputs({
|
|
107
|
+
platform,
|
|
108
|
+
apparmorRestrict: readApparmorRestrict(),
|
|
109
|
+
unshareProbe: probeUnshare(),
|
|
110
|
+
});
|
|
111
|
+
return cached;
|
|
112
|
+
}
|
|
113
|
+
/** Test-only: drop the process cache so a test can re-probe. */
|
|
114
|
+
export function resetUsernsCacheForTests() {
|
|
115
|
+
cached = null;
|
|
116
|
+
}
|
package/dist/lib/memory.d.ts
CHANGED
|
@@ -53,3 +53,29 @@ export declare function rebuildMemoryIndex(dir: string): void;
|
|
|
53
53
|
export declare function memoryTargetDir(agent: AgentId): string;
|
|
54
54
|
/** Copy canonical layered memory into one version home. Returns fact names written. */
|
|
55
55
|
export declare function syncMemoryToVersionHome(agent: AgentId, versionHome: string, cwd?: string): string[];
|
|
56
|
+
/**
|
|
57
|
+
* Canonical shared dir for Claude Code's NATIVE per-project auto-memory —
|
|
58
|
+
* `<versionHome>/.claude/projects/<project-key>/memory/*.md`, the freeform
|
|
59
|
+
* notes Claude writes for itself during a session. Distinct from the layered
|
|
60
|
+
* `memory` resource above (~/.agents/memory/ facts synced into
|
|
61
|
+
* `.claude/memory/`): this dir is keyed by project (via
|
|
62
|
+
* {@link claudeProjectDirName}), not by agent version, and Claude Code itself
|
|
63
|
+
* decides what goes in it — agents-cli only makes the directory
|
|
64
|
+
* version-independent, never writes into it.
|
|
65
|
+
*/
|
|
66
|
+
export declare function getClaudeProjectMemoryDir(cwd: string): string;
|
|
67
|
+
/**
|
|
68
|
+
* Make Claude Code's native per-project memory dir version-independent by
|
|
69
|
+
* symlinking `<versionHome>/.claude/projects/<project-key>/memory/` into the
|
|
70
|
+
* one canonical dir every installed Claude version's home shares for this
|
|
71
|
+
* project (PHNX-2817). Without this, `getVersionHomePath` gives every
|
|
72
|
+
* installed version its own isolated HOME, so a note written under one
|
|
73
|
+
* version is invisible under another — the directory is just empty there.
|
|
74
|
+
*
|
|
75
|
+
* Idempotent and safe to call on every sync: a dir already linked to the
|
|
76
|
+
* canonical target is left alone; a PRE-EXISTING real directory with content
|
|
77
|
+
* (the common case today, since this bug has always left one behind) has its
|
|
78
|
+
* files migrated into the canonical dir first — never discarded — before
|
|
79
|
+
* being replaced by the symlink.
|
|
80
|
+
*/
|
|
81
|
+
export declare function syncClaudeProjectMemoryDir(versionHome: string, cwd?: string): void;
|