@phnx-labs/agents-cli 1.22.83 → 1.22.85

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.
Files changed (291) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +80 -53
  3. package/dist/bootstrap.d.ts +7 -6
  4. package/dist/bootstrap.js +20 -27
  5. package/dist/cli/command-registry.js +5 -1
  6. package/dist/commands/accounts.d.ts +6 -13
  7. package/dist/commands/accounts.js +330 -143
  8. package/dist/commands/apply.js +1 -1
  9. package/dist/commands/auth-mint.d.ts +7 -3
  10. package/dist/commands/auth-mint.js +21 -9
  11. package/dist/commands/auth.js +2 -3
  12. package/dist/commands/browser.js +10 -10
  13. package/dist/commands/daemon.d.ts +7 -3
  14. package/dist/commands/daemon.js +40 -25
  15. package/dist/commands/doctor.js +28 -2
  16. package/dist/commands/exec.js +87 -39
  17. package/dist/commands/fleet-capture.js +21 -2
  18. package/dist/commands/harness-wizard.js +2 -2
  19. package/dist/commands/lease.js +7 -12
  20. package/dist/commands/profiles.d.ts +2 -2
  21. package/dist/commands/profiles.js +20 -13
  22. package/dist/commands/repo.js +3 -3
  23. package/dist/commands/run-account-picker.d.ts +3 -3
  24. package/dist/commands/run-account-picker.js +29 -30
  25. package/dist/commands/secrets-passthrough.d.ts +20 -0
  26. package/dist/commands/secrets-passthrough.js +43 -0
  27. package/dist/commands/sessions-trace.d.ts +11 -0
  28. package/dist/commands/sessions-trace.js +71 -0
  29. package/dist/commands/setup-accounts.d.ts +3 -2
  30. package/dist/commands/setup-accounts.js +4 -4
  31. package/dist/commands/setup-secrets.d.ts +16 -21
  32. package/dist/commands/setup-secrets.js +43 -224
  33. package/dist/commands/ssh.js +2 -2
  34. package/dist/commands/sync.js +1 -1
  35. package/dist/commands/update.d.ts +10 -0
  36. package/dist/commands/update.js +43 -40
  37. package/dist/commands/versions.d.ts +11 -11
  38. package/dist/commands/versions.js +131 -83
  39. package/dist/commands/view.d.ts +1 -9
  40. package/dist/commands/view.js +41 -55
  41. package/dist/commands/webhook.js +5 -5
  42. package/dist/commands/workflows.js +2 -1
  43. package/dist/index.d.ts +12 -12
  44. package/dist/index.js +13 -42
  45. package/dist/lib/account-capabilities.d.ts +1 -1
  46. package/dist/lib/account-capabilities.js +1 -1
  47. package/dist/lib/account-catalog.d.ts +121 -6
  48. package/dist/lib/account-catalog.js +421 -7
  49. package/dist/lib/account-registry.d.ts +8 -30
  50. package/dist/lib/account-registry.js +36 -79
  51. package/dist/lib/account-schema.d.ts +1 -1
  52. package/dist/lib/account-schema.js +1 -1
  53. package/dist/lib/accounting/account-pool-collect.js +16 -4
  54. package/dist/lib/accounting/rotate.d.ts +20 -7
  55. package/dist/lib/accounting/rotate.js +89 -13
  56. package/dist/lib/accounting/usage.js +11 -16
  57. package/dist/lib/accounts/add.d.ts +138 -0
  58. package/dist/lib/accounts/add.js +651 -0
  59. package/dist/lib/accounts/migrate.d.ts +117 -0
  60. package/dist/lib/accounts/migrate.js +536 -0
  61. package/dist/lib/accounts/slots.d.ts +13 -0
  62. package/dist/lib/accounts/slots.js +100 -0
  63. package/dist/lib/agent-spec/agents.d.ts +16 -0
  64. package/dist/lib/agent-spec/agents.js +32 -4
  65. package/dist/lib/app-bundle-install.js +5 -4
  66. package/dist/lib/auth-health.d.ts +3 -0
  67. package/dist/lib/auth-health.js +11 -0
  68. package/dist/lib/auth-mint.d.ts +52 -16
  69. package/dist/lib/auth-mint.js +117 -39
  70. package/dist/lib/browser/chrome.d.ts +1 -1
  71. package/dist/lib/browser/chrome.js +5 -5
  72. package/dist/lib/byok-usage.js +3 -3
  73. package/dist/lib/claude-account-token.d.ts +47 -3
  74. package/dist/lib/claude-account-token.js +172 -50
  75. package/dist/lib/cloud/antigravity.js +4 -4
  76. package/dist/lib/cloud/cursor.js +4 -4
  77. package/dist/lib/crabbox/cli.d.ts +1 -1
  78. package/dist/lib/crabbox/cli.js +6 -6
  79. package/dist/lib/crabbox/runtimes.d.ts +3 -3
  80. package/dist/lib/crabbox/runtimes.js +5 -5
  81. package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
  82. package/dist/lib/daemon/account-state-daemon-service.js +199 -3
  83. package/dist/lib/daemon/auth-sync-service.js +28 -1
  84. package/dist/lib/daemon/daemon.d.ts +6 -16
  85. package/dist/lib/daemon/daemon.js +52 -97
  86. package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
  87. package/dist/lib/daemon/daemon.test-fixture.js +22 -40
  88. package/dist/lib/daemon/harness-update-service.d.ts +1 -1
  89. package/dist/lib/daemon/harness-update-service.js +1 -1
  90. package/dist/lib/daemon/runner.d.ts +15 -3
  91. package/dist/lib/daemon/runner.js +49 -24
  92. package/dist/lib/daemon-health.d.ts +1 -2
  93. package/dist/lib/daemon-health.js +7 -6
  94. package/dist/lib/daemon-services.d.ts +1 -1
  95. package/dist/lib/daemon-services.js +1 -11
  96. package/dist/lib/daemon-ticks.js +23 -2
  97. package/dist/lib/daemon-webhooks.d.ts +8 -7
  98. package/dist/lib/daemon-webhooks.js +10 -9
  99. package/dist/lib/device-config.js +1 -1
  100. package/dist/lib/devices/doctor-findings.d.ts +1 -1
  101. package/dist/lib/devices/harness-inventory.d.ts +39 -0
  102. package/dist/lib/devices/harness-inventory.js +126 -4
  103. package/dist/lib/doctor-diff.js +2 -1
  104. package/dist/lib/exec-account-home.d.ts +38 -0
  105. package/dist/lib/exec-account-home.js +164 -0
  106. package/dist/lib/exec.d.ts +30 -0
  107. package/dist/lib/exec.js +101 -31
  108. package/dist/lib/feed/activity-stream.d.ts +60 -0
  109. package/dist/lib/feed/activity-stream.js +271 -0
  110. package/dist/lib/feed/activity.d.ts +7 -0
  111. package/dist/lib/feed/activity.js +10 -4
  112. package/dist/lib/feed/watch.d.ts +9 -3
  113. package/dist/lib/feed/watch.js +72 -36
  114. package/dist/lib/fleet/apply.d.ts +2 -2
  115. package/dist/lib/fleet/apply.js +4 -4
  116. package/dist/lib/fleet/auth-sync.js +4 -3
  117. package/dist/lib/fleet-shared-repo-sync.js +6 -9
  118. package/dist/lib/fleet-shared-state.d.ts +6 -0
  119. package/dist/lib/harness/adapter.d.ts +7 -7
  120. package/dist/lib/harness/adapter.js +10 -6
  121. package/dist/lib/harness/adapters/grok.js +8 -3
  122. package/dist/lib/harness/adapters/muse.js +1 -1
  123. package/dist/lib/harness/adapters/opencode.js +12 -3
  124. package/dist/lib/harness-auth-capabilities.d.ts +61 -0
  125. package/dist/lib/harness-auth-capabilities.js +52 -0
  126. package/dist/lib/helper-versions.d.ts +6 -4
  127. package/dist/lib/helper-versions.js +5 -4
  128. package/dist/lib/hosts/credential-transport.d.ts +10 -0
  129. package/dist/lib/hosts/credential-transport.js +37 -0
  130. package/dist/lib/hosts/dispatch.d.ts +7 -0
  131. package/dist/lib/hosts/dispatch.js +13 -6
  132. package/dist/lib/identity/client.d.ts +3 -3
  133. package/dist/lib/identity/client.js +3 -3
  134. package/dist/lib/installations/index.d.ts +1 -1
  135. package/dist/lib/installations/index.js +1 -1
  136. package/dist/lib/installations/migrate.js +9 -0
  137. package/dist/lib/installations/resolve.js +1 -1
  138. package/dist/lib/installations/shims.d.ts +15 -0
  139. package/dist/lib/installations/shims.js +69 -0
  140. package/dist/lib/installations/store.d.ts +43 -0
  141. package/dist/lib/installations/store.js +75 -7
  142. package/dist/lib/installations/versions.js +1 -1
  143. package/dist/lib/menubar/install-menubar.d.ts +3 -3
  144. package/dist/lib/menubar/install-menubar.js +3 -3
  145. package/dist/lib/native-accounts.d.ts +35 -0
  146. package/dist/lib/native-accounts.js +41 -0
  147. package/dist/lib/net-close.d.ts +11 -0
  148. package/dist/lib/net-close.js +25 -0
  149. package/dist/lib/openclaw-keychain.js +27 -1
  150. package/dist/lib/profiles.js +22 -9
  151. package/dist/lib/project-resources.js +1 -1
  152. package/dist/lib/reserved-stores.d.ts +53 -0
  153. package/dist/lib/reserved-stores.js +120 -0
  154. package/dist/lib/secrets-client.d.ts +191 -0
  155. package/dist/lib/secrets-client.js +710 -0
  156. package/dist/lib/secrets-policy.d.ts +222 -0
  157. package/dist/lib/secrets-policy.js +483 -0
  158. package/dist/lib/secrets-types.d.ts +188 -0
  159. package/dist/lib/secrets-types.js +24 -0
  160. package/dist/lib/service-manifest.js +3 -4
  161. package/dist/lib/session/active.d.ts +38 -4
  162. package/dist/lib/session/active.js +36 -4
  163. package/dist/lib/session/bash-command.js +10 -0
  164. package/dist/lib/session/db.d.ts +65 -2
  165. package/dist/lib/session/db.js +178 -18
  166. package/dist/lib/session/mirror.d.ts +5 -0
  167. package/dist/lib/session/mirror.js +166 -0
  168. package/dist/lib/session/parse.d.ts +49 -0
  169. package/dist/lib/session/parse.js +324 -30
  170. package/dist/lib/session/prompt.d.ts +93 -2
  171. package/dist/lib/session/prompt.js +266 -15
  172. package/dist/lib/session/remote/peer-stream.d.ts +47 -0
  173. package/dist/lib/session/remote/peer-stream.js +142 -0
  174. package/dist/lib/session/remote/watch.d.ts +1 -1
  175. package/dist/lib/session/remote/watch.js +41 -42
  176. package/dist/lib/session/session-cache.d.ts +9 -0
  177. package/dist/lib/session/session-cache.js +40 -2
  178. package/dist/lib/session/sync/config.js +2 -2
  179. package/dist/lib/session/timeline-pass.d.ts +129 -0
  180. package/dist/lib/session/timeline-pass.js +323 -0
  181. package/dist/lib/session/timeline.d.ts +182 -0
  182. package/dist/lib/session/timeline.js +636 -0
  183. package/dist/lib/session/types.d.ts +155 -1
  184. package/dist/lib/sha256-asset.d.ts +11 -16
  185. package/dist/lib/sha256-asset.js +11 -16
  186. package/dist/lib/share/config.js +14 -5
  187. package/dist/lib/signin-badge.d.ts +26 -0
  188. package/dist/lib/signin-badge.js +42 -0
  189. package/dist/lib/staleness/detectors/workflows.js +3 -105
  190. package/dist/lib/staleness/writers/workflows.js +2 -1
  191. package/dist/lib/state.d.ts +7 -4
  192. package/dist/lib/state.js +18 -7
  193. package/dist/lib/summarizer/pass.d.ts +2 -0
  194. package/dist/lib/summarizer/pass.js +14 -1
  195. package/dist/lib/summarizer/summarize.d.ts +7 -0
  196. package/dist/lib/summarizer/summarize.js +3 -0
  197. package/dist/lib/sync-umbrella.js +2 -2
  198. package/dist/lib/teams/agents.js +1 -1
  199. package/dist/lib/types.d.ts +74 -33
  200. package/dist/lib/view-types.d.ts +6 -3
  201. package/dist/lib/workflows-registry.d.ts +71 -0
  202. package/dist/lib/workflows-registry.js +280 -0
  203. package/dist/lib/workflows.d.ts +21 -21
  204. package/dist/lib/workflows.js +12 -327
  205. package/package.json +2 -3
  206. package/scripts/postinstall.js +4 -29
  207. package/dist/commands/secrets-import.d.ts +0 -18
  208. package/dist/commands/secrets-import.js +0 -74
  209. package/dist/commands/secrets-migrate.d.ts +0 -25
  210. package/dist/commands/secrets-migrate.js +0 -334
  211. package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
  212. package/dist/commands/secrets-rotate-passphrase.js +0 -96
  213. package/dist/commands/secrets-sync.d.ts +0 -11
  214. package/dist/commands/secrets-sync.js +0 -153
  215. package/dist/commands/secrets-vault.d.ts +0 -10
  216. package/dist/commands/secrets-vault.js +0 -130
  217. package/dist/commands/secrets.d.ts +0 -212
  218. package/dist/commands/secrets.js +0 -3030
  219. package/dist/lib/accounts/connect.d.ts +0 -170
  220. package/dist/lib/accounts/connect.js +0 -383
  221. package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
  222. package/dist/lib/daemon/keychain-reap-service.js +0 -32
  223. package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
  224. package/dist/lib/daemon/secrets-broker-service.js +0 -51
  225. package/dist/lib/secrets/agent.d.ts +0 -358
  226. package/dist/lib/secrets/agent.js +0 -1291
  227. package/dist/lib/secrets/audit.d.ts +0 -46
  228. package/dist/lib/secrets/audit.js +0 -101
  229. package/dist/lib/secrets/bundles.d.ts +0 -290
  230. package/dist/lib/secrets/bundles.js +0 -1547
  231. package/dist/lib/secrets/download-keychain.d.ts +0 -47
  232. package/dist/lib/secrets/download-keychain.js +0 -70
  233. package/dist/lib/secrets/drivers/rush.d.ts +0 -14
  234. package/dist/lib/secrets/drivers/rush.js +0 -90
  235. package/dist/lib/secrets/fallback.d.ts +0 -48
  236. package/dist/lib/secrets/fallback.js +0 -48
  237. package/dist/lib/secrets/filestore.d.ts +0 -222
  238. package/dist/lib/secrets/filestore.js +0 -1099
  239. package/dist/lib/secrets/headless.d.ts +0 -43
  240. package/dist/lib/secrets/headless.js +0 -56
  241. package/dist/lib/secrets/icloud-import.d.ts +0 -79
  242. package/dist/lib/secrets/icloud-import.js +0 -206
  243. package/dist/lib/secrets/index.d.ts +0 -451
  244. package/dist/lib/secrets/index.js +0 -1568
  245. package/dist/lib/secrets/install-helper.d.ts +0 -72
  246. package/dist/lib/secrets/install-helper.js +0 -245
  247. package/dist/lib/secrets/lease.d.ts +0 -25
  248. package/dist/lib/secrets/lease.js +0 -44
  249. package/dist/lib/secrets/linux.d.ts +0 -77
  250. package/dist/lib/secrets/linux.js +0 -393
  251. package/dist/lib/secrets/list-filter.d.ts +0 -109
  252. package/dist/lib/secrets/list-filter.js +0 -261
  253. package/dist/lib/secrets/mcp.d.ts +0 -93
  254. package/dist/lib/secrets/mcp.js +0 -211
  255. package/dist/lib/secrets/profiles.d.ts +0 -10
  256. package/dist/lib/secrets/profiles.js +0 -13
  257. package/dist/lib/secrets/push.d.ts +0 -133
  258. package/dist/lib/secrets/push.js +0 -273
  259. package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
  260. package/dist/lib/secrets/rc-hygiene.js +0 -143
  261. package/dist/lib/secrets/read-backoff.d.ts +0 -27
  262. package/dist/lib/secrets/read-backoff.js +0 -64
  263. package/dist/lib/secrets/reaper.d.ts +0 -90
  264. package/dist/lib/secrets/reaper.js +0 -243
  265. package/dist/lib/secrets/remote.d.ts +0 -152
  266. package/dist/lib/secrets/remote.js +0 -344
  267. package/dist/lib/secrets/reserved-sync.d.ts +0 -66
  268. package/dist/lib/secrets/reserved-sync.js +0 -147
  269. package/dist/lib/secrets/scope.d.ts +0 -26
  270. package/dist/lib/secrets/scope.js +0 -29
  271. package/dist/lib/secrets/session-store.d.ts +0 -107
  272. package/dist/lib/secrets/session-store.js +0 -342
  273. package/dist/lib/secrets/sync-backend.d.ts +0 -48
  274. package/dist/lib/secrets/sync-backend.js +0 -13
  275. package/dist/lib/secrets/sync-commands.d.ts +0 -21
  276. package/dist/lib/secrets/sync-commands.js +0 -21
  277. package/dist/lib/secrets/sync.d.ts +0 -48
  278. package/dist/lib/secrets/sync.js +0 -237
  279. package/dist/lib/secrets/unlock-hints.d.ts +0 -27
  280. package/dist/lib/secrets/unlock-hints.js +0 -36
  281. package/dist/lib/secrets/usage-db.d.ts +0 -46
  282. package/dist/lib/secrets/usage-db.js +0 -96
  283. package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
  284. package/dist/lib/secrets/vault-age-helper.js +0 -34
  285. package/dist/lib/secrets/vault.d.ts +0 -49
  286. package/dist/lib/secrets/vault.js +0 -399
  287. package/dist/lib/secrets/windows.d.ts +0 -81
  288. package/dist/lib/secrets/windows.js +0 -556
  289. package/scripts/install-helper.js +0 -97
  290. /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
  291. /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
@@ -0,0 +1,117 @@
1
+ import type { AgentId } from '../types.js';
2
+ export declare const ACCOUNT_MIGRATION_SCHEMA = 1;
3
+ export type MigrationActionKind = 'canonical' | 'slot' | 'trash' | 'defer' | 'skip';
4
+ export interface InstallationInventory {
5
+ agent: AgentId;
6
+ label: string;
7
+ release: string;
8
+ dir: string;
9
+ home: string;
10
+ hasBinary: boolean;
11
+ isolated: boolean;
12
+ launchable: boolean;
13
+ signedIn: boolean;
14
+ hasCredential: boolean;
15
+ identityKey: string | null;
16
+ email: string | null;
17
+ accountKey: string | null;
18
+ accountId: string | null;
19
+ sessionCount: number;
20
+ busy: boolean;
21
+ }
22
+ export interface MigrationAction {
23
+ kind: MigrationActionKind;
24
+ label: string;
25
+ release: string;
26
+ reason: string;
27
+ accountId?: string;
28
+ accountName?: string;
29
+ slotDir?: string;
30
+ identityKey?: string | null;
31
+ email?: string | null;
32
+ sessionCount: number;
33
+ /** Planned filesystem remap for sessions.db (from prefix → to prefix). */
34
+ pathMoves: Array<{
35
+ from: string;
36
+ to: string;
37
+ }>;
38
+ }
39
+ export interface HarnessMigrationPlan {
40
+ agent: AgentId;
41
+ canonical: string | null;
42
+ defaultBefore: string | null;
43
+ inventory: InstallationInventory[];
44
+ actions: MigrationAction[];
45
+ counts: {
46
+ installations: number;
47
+ keep: number;
48
+ slots: number;
49
+ trash: number;
50
+ deferred: number;
51
+ skipped: number;
52
+ };
53
+ }
54
+ export interface AccountMigrationPlan {
55
+ at: string;
56
+ harnesses: HarnessMigrationPlan[];
57
+ totals: {
58
+ installations: number;
59
+ keep: number;
60
+ slots: number;
61
+ trash: number;
62
+ deferred: number;
63
+ skipped: number;
64
+ };
65
+ }
66
+ export type AccountMigrationManifestStatus = 'planned' | 'complete';
67
+ export interface AccountMigrationManifest {
68
+ schema: number;
69
+ at: string;
70
+ dryRun: boolean;
71
+ /** `planned` until every irreversible step finishes; `complete` is last. */
72
+ status: AccountMigrationManifestStatus;
73
+ /** Full plan, written before the first move so a crash still names the intent. */
74
+ plan: AccountMigrationPlan;
75
+ harnesses: Record<string, {
76
+ canonical: string | null;
77
+ slots: Array<{
78
+ oldLabel: string;
79
+ accountId: string;
80
+ accountName: string;
81
+ slotDir: string;
82
+ }>;
83
+ trashed: Array<{
84
+ label: string;
85
+ reason: string;
86
+ trashPath: string;
87
+ }>;
88
+ deferred: Array<{
89
+ label: string;
90
+ reason: string;
91
+ }>;
92
+ }>;
93
+ /** old `agent@label` → new slot dir (or trash path). */
94
+ map: Record<string, string>;
95
+ }
96
+ export interface AccountMigrateDeps {
97
+ isActive?: (installation: {
98
+ agent: AgentId;
99
+ label: string;
100
+ }) => Promise<boolean>;
101
+ now?: () => Date;
102
+ }
103
+ export declare function planAccountMigration(agents?: AgentId[], deps?: AccountMigrateDeps): Promise<AccountMigrationPlan>;
104
+ export declare function formatMigrationPlan(plan: AccountMigrationPlan): string;
105
+ export interface ApplyMigrationResult {
106
+ plan: AccountMigrationPlan;
107
+ manifest: AccountMigrationManifest;
108
+ manifestPath: string;
109
+ sessionsReindexed: number;
110
+ }
111
+ export declare function applyAccountMigration(agents?: AgentId[], deps?: AccountMigrateDeps): Promise<ApplyMigrationResult>;
112
+ /**
113
+ * Upgrade hook: print a dry-run report when leftover per-account installations
114
+ * exist. Never applies. `--apply` stays an explicit `agents accounts migrate`
115
+ * flag this release.
116
+ */
117
+ export declare function reportAccountSlotMigrationOnUpgrade(): Promise<void>;
@@ -0,0 +1,536 @@
1
+ /**
2
+ * Fold N per-account installations into 1 harness install + N credential slots
3
+ * (PHNX-3940 T7).
4
+ *
5
+ * Homes are moved, never copied. Empty logged-out homes and duplicate identities
6
+ * go to `agents trash` (restore reverses). Session transcript paths are re-indexed
7
+ * in the same transaction as the moves. A running/leased home is deferred, never
8
+ * moved. `--apply` is explicit; dry-run (and the upgrade hook) touch nothing.
9
+ *
10
+ * Native OAuth files stay inside the moved home on this device. Nothing here
11
+ * reads or writes a reserved store, a setup-token, or a worker bundle.
12
+ */
13
+ import * as fs from 'node:fs';
14
+ import * as path from 'node:path';
15
+ import { ALL_AGENT_IDS, credentialPresence, getAccountInfo } from '../agents.js';
16
+ import { nativeAccountCapability, nativeIdentityKey } from '../account-capabilities.js';
17
+ import { addNativeAccount, bindAccount, findNativeAccountByIdentity, listNativeAccounts, } from '../account-registry.js';
18
+ import { isLaunchableSignedIn } from '../accounting/rotate.js';
19
+ import { compareVersions } from '../agent-spec/primitives.js';
20
+ import { isInstallationLikelyActive } from '../installations/active-check.js';
21
+ import { getGlobalDefault, getVersionDir, getVersionHomePath, invalidateInstalledVersionsCache, isVersionIsolated, listInstalledVersionDirs, readInstallation, setGlobalDefault, softDeleteVersionDir, } from '../installations/versions.js';
22
+ import { removeVersionedAlias } from '../installations/shims.js';
23
+ import { countSessionsWithFilePrefix, reindexMovedSessionPaths } from '../session/db.js';
24
+ import { atomicWriteFileSync } from '../fs-atomic.js';
25
+ import { getHistoryDir, readMeta, updateMeta } from '../state.js';
26
+ import { recordSlot, slotDir } from './slots.js';
27
+ export const ACCOUNT_MIGRATION_SCHEMA = 1;
28
+ const OPAQUE_LABEL = /^(?:latest|main|acct-[0-9a-f]+|\d+\.\d+.*)$/i;
29
+ function defaultIsActive(installation) {
30
+ return isInstallationLikelyActive(installation);
31
+ }
32
+ function readInstallRecord(agent, label) {
33
+ try {
34
+ const rec = readInstallation(agent, label);
35
+ if (rec)
36
+ return { release: rec.releaseVersion };
37
+ return { release: label };
38
+ }
39
+ catch (err) {
40
+ throw new Error(`Unreadable installation.json for ${agent}@${label}: ${err instanceof Error ? err.message : String(err)}`);
41
+ }
42
+ }
43
+ function emailLocalpart(email) {
44
+ if (!email)
45
+ return null;
46
+ const local = email.split('@')[0]?.trim();
47
+ return local || null;
48
+ }
49
+ function isOpaqueInstallLabel(label) {
50
+ return OPAQUE_LABEL.test(label);
51
+ }
52
+ function uniqueAccountName(agent, preferred, taken) {
53
+ const base = preferred.replace(/[^A-Za-z0-9@._+-]/g, '-').replace(/^[^A-Za-z0-9]+/, '') || 'account';
54
+ if (!taken.has(`${agent}:${base.toLowerCase()}`))
55
+ return base;
56
+ for (let i = 2; i < 1000; i++) {
57
+ const candidate = `${base}-${i}`;
58
+ if (!taken.has(`${agent}:${candidate.toLowerCase()}`))
59
+ return candidate;
60
+ }
61
+ throw new Error(`Could not allocate a unique account name for ${agent} starting from '${preferred}'.`);
62
+ }
63
+ function invertHomes(agent, meta) {
64
+ const out = new Map();
65
+ const homes = meta.deviceAccounts?.homes ?? {};
66
+ for (const [accountId, label] of Object.entries(homes)) {
67
+ if (!label)
68
+ continue;
69
+ const row = listNativeAccounts(meta).find((a) => a.id === accountId);
70
+ if (row && row.agent !== agent)
71
+ continue;
72
+ out.set(label, accountId);
73
+ }
74
+ return out;
75
+ }
76
+ function assertIdentityMatch(agent, label, identityKey, row) {
77
+ if (identityKey && row.identityKey !== identityKey) {
78
+ throw new Error(`Identity mismatch for ${agent}@${label}: home is '${identityKey}' but account row '${row.name}' is '${row.identityKey}'.`);
79
+ }
80
+ }
81
+ async function inventoryHarness(agent, deps) {
82
+ const dirs = listInstalledVersionDirs(agent);
83
+ const meta = readMeta();
84
+ const homesByLabel = invertHomes(agent, meta);
85
+ const isActive = deps.isActive ?? defaultIsActive;
86
+ const out = [];
87
+ for (const { version: label, hasBinary } of dirs) {
88
+ const rec = readInstallRecord(agent, label);
89
+ const home = getVersionHomePath(agent, label);
90
+ const info = await getAccountInfo(agent, home);
91
+ const presence = credentialPresence(agent, home);
92
+ const launchable = isLaunchableSignedIn(info.signedIn, presence);
93
+ const identityKey = nativeIdentityKey(info, nativeAccountCapability(agent));
94
+ const accountIdFromHome = homesByLabel.get(label) ?? null;
95
+ if (accountIdFromHome) {
96
+ const row = listNativeAccounts(meta).find((a) => a.id === accountIdFromHome);
97
+ if (row)
98
+ assertIdentityMatch(agent, label, identityKey, row);
99
+ }
100
+ const matched = findNativeAccountByIdentity(meta, agent, info);
101
+ out.push({
102
+ agent,
103
+ label,
104
+ release: rec.release,
105
+ dir: getVersionDir(agent, label),
106
+ home,
107
+ hasBinary,
108
+ isolated: isVersionIsolated(agent, label),
109
+ launchable,
110
+ signedIn: info.signedIn,
111
+ hasCredential: presence.perVersion,
112
+ identityKey,
113
+ email: info.email,
114
+ accountKey: info.accountKey,
115
+ accountId: accountIdFromHome ?? matched?.id ?? null,
116
+ sessionCount: countSessionsWithFilePrefix(home),
117
+ busy: await isActive({ agent, label }),
118
+ });
119
+ }
120
+ return out;
121
+ }
122
+ function newestLaunchable(items) {
123
+ const launchable = items.filter((i) => i.launchable && !i.isolated);
124
+ if (launchable.length === 0)
125
+ return null;
126
+ return [...launchable].sort((a, b) => {
127
+ const rel = compareVersions(a.release, b.release);
128
+ if (rel !== 0)
129
+ return rel;
130
+ return compareVersions(a.label, b.label);
131
+ }).at(-1) ?? null;
132
+ }
133
+ function chooseCanonical(items, defaultLabel, trashLabels) {
134
+ const eligible = items.filter((i) => !i.isolated && !trashLabels.has(i.label));
135
+ if (defaultLabel) {
136
+ const def = eligible.find((i) => i.label === defaultLabel);
137
+ if (def?.launchable)
138
+ return def;
139
+ }
140
+ const newest = newestLaunchable(eligible);
141
+ if (newest)
142
+ return newest;
143
+ const withBinary = eligible.filter((i) => i.hasBinary);
144
+ if (withBinary.length === 0)
145
+ return eligible[0] ?? null;
146
+ return [...withBinary].sort((a, b) => compareVersions(a.release, b.release) || compareVersions(a.label, b.label)).at(-1) ?? null;
147
+ }
148
+ async function duplicateTrashLabels(items) {
149
+ const slotish = items.filter((i) => i.hasCredential && i.identityKey && !i.isolated);
150
+ if (slotish.length < 2)
151
+ return new Set();
152
+ const { planDuplicatePrune } = await import('../../commands/view.js');
153
+ const dups = planDuplicatePrune(slotish.map((i) => ({
154
+ version: i.label,
155
+ release: i.release,
156
+ email: i.email,
157
+ accountKey: i.accountKey,
158
+ signedIn: i.signedIn,
159
+ hasBinary: i.hasBinary,
160
+ })));
161
+ return new Set(dups.map((d) => d.version));
162
+ }
163
+ function plannedTrashPath(agent, label) {
164
+ return path.join(getHistoryDir(), 'trash', 'versions', agent, label, '<stamp>');
165
+ }
166
+ function emptyDir(dir) {
167
+ if (!fs.existsSync(dir))
168
+ return true;
169
+ try {
170
+ return fs.readdirSync(dir).length === 0;
171
+ }
172
+ catch {
173
+ return false;
174
+ }
175
+ }
176
+ function assertSlotAbsent(agent, accountId) {
177
+ const dest = slotDir(agent, accountId);
178
+ if (fs.existsSync(dest) && !emptyDir(dest)) {
179
+ throw new Error(`Slot already exists for ${agent} account ${accountId} at ${dest}.`);
180
+ }
181
+ return dest;
182
+ }
183
+ function takenNames(meta) {
184
+ const taken = new Set();
185
+ for (const row of listNativeAccounts(meta)) {
186
+ taken.add(`${row.agent}:${row.name.toLowerCase()}`);
187
+ }
188
+ return taken;
189
+ }
190
+ function resolveOrRegisterAccount(item, taken) {
191
+ const meta = readMeta();
192
+ if (item.accountId) {
193
+ const row = listNativeAccounts(meta).find((a) => a.id === item.accountId);
194
+ if (!row)
195
+ throw new Error(`${item.agent}@${item.label} points at unknown account id '${item.accountId}'.`);
196
+ assertIdentityMatch(item.agent, item.label, item.identityKey, row);
197
+ return row;
198
+ }
199
+ if (!item.identityKey) {
200
+ throw new Error(`${item.agent}@${item.label} has a credential but no inspectable identity.`);
201
+ }
202
+ const existing = listNativeAccounts(meta).find((a) => a.agent === item.agent && a.identityKey === item.identityKey);
203
+ if (existing)
204
+ return existing;
205
+ const preferred = (!isOpaqueInstallLabel(item.label) ? item.label : null)
206
+ ?? emailLocalpart(item.email)
207
+ ?? 'account';
208
+ const name = uniqueAccountName(item.agent, preferred, taken);
209
+ taken.add(`${item.agent}:${name.toLowerCase()}`);
210
+ const cap = nativeAccountCapability(item.agent);
211
+ const scope = cap.scope === 'device' ? 'device' : 'version';
212
+ return addNativeAccount(name, item.agent, item.identityKey, item.email ?? undefined, scope);
213
+ }
214
+ async function planHarness(agent, deps) {
215
+ const inventory = await inventoryHarness(agent, deps);
216
+ const defaultBefore = getGlobalDefault(agent);
217
+ const trashFromDups = await duplicateTrashLabels(inventory);
218
+ const canonical = chooseCanonical(inventory, defaultBefore, trashFromDups);
219
+ const actions = [];
220
+ for (const item of inventory) {
221
+ if (item.isolated) {
222
+ actions.push({
223
+ kind: 'skip',
224
+ label: item.label,
225
+ release: item.release,
226
+ reason: 'isolated copy — left untouched',
227
+ sessionCount: item.sessionCount,
228
+ pathMoves: [],
229
+ });
230
+ continue;
231
+ }
232
+ if (item.busy) {
233
+ actions.push({
234
+ kind: 'defer',
235
+ label: item.label,
236
+ release: item.release,
237
+ reason: 'installation is busy (live process or launch lease)',
238
+ identityKey: item.identityKey,
239
+ email: item.email,
240
+ sessionCount: item.sessionCount,
241
+ pathMoves: [],
242
+ });
243
+ continue;
244
+ }
245
+ if (trashFromDups.has(item.label)) {
246
+ actions.push({
247
+ kind: 'trash',
248
+ label: item.label,
249
+ release: item.release,
250
+ reason: 'duplicate identity — keeping the better home via planDuplicatePrune',
251
+ identityKey: item.identityKey,
252
+ email: item.email,
253
+ sessionCount: item.sessionCount,
254
+ pathMoves: [{ from: item.dir, to: plannedTrashPath(agent, item.label) }],
255
+ });
256
+ continue;
257
+ }
258
+ if (item.hasCredential && item.identityKey) {
259
+ const dest = item.accountId ? slotDir(agent, item.accountId) : slotDir(agent, '<new-account>');
260
+ actions.push({
261
+ kind: 'slot',
262
+ label: item.label,
263
+ release: item.release,
264
+ reason: item.label === canonical?.label
265
+ ? 'credential-bearing canonical home → slot (binary stays)'
266
+ : 'credential-bearing home → slot; binary trashed',
267
+ accountId: item.accountId ?? undefined,
268
+ identityKey: item.identityKey,
269
+ email: item.email,
270
+ slotDir: dest,
271
+ sessionCount: item.sessionCount,
272
+ pathMoves: [
273
+ { from: item.home, to: dest },
274
+ ...(item.label === canonical?.label ? [] : [{ from: item.dir, to: plannedTrashPath(agent, item.label) }]),
275
+ ],
276
+ });
277
+ continue;
278
+ }
279
+ if (canonical && item.label === canonical.label) {
280
+ actions.push({
281
+ kind: 'canonical',
282
+ label: item.label,
283
+ release: item.release,
284
+ reason: 'canonical install (binary kept; no credential to move)',
285
+ sessionCount: item.sessionCount,
286
+ pathMoves: [],
287
+ });
288
+ continue;
289
+ }
290
+ actions.push({
291
+ kind: 'trash',
292
+ label: item.label,
293
+ release: item.release,
294
+ reason: item.hasBinary ? 'empty logged-out home' : 'home-only leftover (no binary)',
295
+ sessionCount: item.sessionCount,
296
+ pathMoves: [{ from: item.dir, to: plannedTrashPath(agent, item.label) }],
297
+ });
298
+ }
299
+ const counts = {
300
+ installations: inventory.length,
301
+ // A busy canonical is still the kept install — we did not move it, we
302
+ // deferred the fold. Counting it as 0 install is a lie.
303
+ keep: actions.filter((a) => a.kind === 'canonical'
304
+ || ((a.kind === 'slot' || a.kind === 'defer') && a.label === canonical?.label)).length,
305
+ slots: actions.filter((a) => a.kind === 'slot').length,
306
+ trash: actions.filter((a) => a.kind === 'trash').length,
307
+ deferred: actions.filter((a) => a.kind === 'defer').length,
308
+ skipped: actions.filter((a) => a.kind === 'skip').length,
309
+ };
310
+ return { agent, canonical: canonical?.label ?? null, defaultBefore, inventory, actions, counts };
311
+ }
312
+ export async function planAccountMigration(agents = [...ALL_AGENT_IDS], deps = {}) {
313
+ const harnesses = [];
314
+ for (const agent of agents) {
315
+ const plan = await planHarness(agent, deps);
316
+ if (plan.inventory.length === 0)
317
+ continue;
318
+ harnesses.push(plan);
319
+ }
320
+ const totals = harnesses.reduce((acc, h) => ({
321
+ installations: acc.installations + h.counts.installations,
322
+ keep: acc.keep + h.counts.keep,
323
+ slots: acc.slots + h.counts.slots,
324
+ trash: acc.trash + h.counts.trash,
325
+ deferred: acc.deferred + h.counts.deferred,
326
+ skipped: acc.skipped + h.counts.skipped,
327
+ }), { installations: 0, keep: 0, slots: 0, trash: 0, deferred: 0, skipped: 0 });
328
+ return { at: (deps.now ?? (() => new Date()))().toISOString(), harnesses, totals };
329
+ }
330
+ function canonicalBusy(h) {
331
+ return !!h.canonical && h.actions.some((a) => a.kind === 'defer' && a.label === h.canonical);
332
+ }
333
+ export function formatMigrationPlan(plan) {
334
+ const lines = [];
335
+ if (plan.harnesses.length === 0 || plan.totals.installations === 0) {
336
+ return 'No per-account installations to fold.';
337
+ }
338
+ for (const h of plan.harnesses) {
339
+ if (h.counts.installations === 0)
340
+ continue;
341
+ lines.push(`${h.agent}: ${h.counts.installations} installation${h.counts.installations === 1 ? '' : 's'}`
342
+ + ` → ${h.counts.keep} install + ${h.counts.slots} slot${h.counts.slots === 1 ? '' : 's'}`
343
+ + ` + ${h.counts.trash} trashed`
344
+ + (h.counts.deferred ? ` + ${h.counts.deferred} deferred` : '')
345
+ + (h.canonical ? ` (canonical ${h.agent}@${h.canonical}${canonicalBusy(h) ? ' busy' : ''})` : ''));
346
+ if (h.defaultBefore && h.defaultBefore !== h.canonical) {
347
+ lines.push(` default ${h.agent}@${h.defaultBefore} is not launchable → repoint to ${h.canonical ?? '(none)'}`);
348
+ }
349
+ for (const a of h.actions) {
350
+ const who = a.email ? ` ${a.email}` : '';
351
+ lines.push(` ${a.kind.padEnd(9)} ${h.agent}@${a.label}${who} ${a.reason}`);
352
+ }
353
+ }
354
+ lines.push(`totals: ${plan.totals.installations} installations → ${plan.totals.keep} install + ${plan.totals.slots} slots + ${plan.totals.trash} trashed`
355
+ + (plan.totals.deferred ? ` + ${plan.totals.deferred} deferred` : ''));
356
+ return lines.join('\n');
357
+ }
358
+ function rewriteBindings(agent, labelToAccount, stillPresent) {
359
+ const meta = readMeta();
360
+ const targets = new Set([
361
+ ...Object.keys(meta.accounts?.bindings ?? {}),
362
+ ...Object.keys(meta.deviceAccounts?.bindings ?? {}),
363
+ ]);
364
+ const prefix = `${agent}@`;
365
+ for (const target of targets) {
366
+ if (!target.startsWith(prefix))
367
+ continue;
368
+ const label = target.slice(prefix.length);
369
+ const accountId = labelToAccount.get(label);
370
+ if (!accountId) {
371
+ if (stillPresent.has(label))
372
+ continue;
373
+ throw new Error(`Cannot rewrite binding '${target}': ${agent}@${label} was removed and has no kept account of the same identity.`);
374
+ }
375
+ const row = listNativeAccounts(readMeta()).find((a) => a.id === accountId);
376
+ if (!row) {
377
+ throw new Error(`Cannot rewrite binding '${target}': account '${accountId}' is missing.`);
378
+ }
379
+ // Keep the per-target key (`agent@label` → account id). Collapsing every
380
+ // label onto one bare-agent binding would drop every account after the first.
381
+ bindAccount(row.id, target, agent);
382
+ }
383
+ }
384
+ function clearHomes(accountIds) {
385
+ if (accountIds.length === 0)
386
+ return;
387
+ updateMeta((current) => {
388
+ const homes = { ...current.deviceAccounts?.homes };
389
+ for (const id of accountIds)
390
+ delete homes[id];
391
+ return { ...current, deviceAccounts: { ...current.deviceAccounts, homes } };
392
+ });
393
+ }
394
+ function moveHomeToSlot(item, dest) {
395
+ if (fs.existsSync(dest) && !emptyDir(dest)) {
396
+ throw new Error(`Slot already exists for ${item.agent} account at ${dest}.`);
397
+ }
398
+ if (fs.existsSync(dest))
399
+ fs.rmdirSync(dest);
400
+ fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
401
+ if (!fs.existsSync(item.home)) {
402
+ throw new Error(`Home ${item.home} is missing; cannot move it into a slot.`);
403
+ }
404
+ fs.renameSync(item.home, dest);
405
+ }
406
+ function recordMovedSlot(agent, account, dest) {
407
+ const cap = nativeAccountCapability(agent);
408
+ const slot = {
409
+ accountId: account.id,
410
+ slotDir: dest,
411
+ authMode: cap.scope === 'device' ? 'per-device' : 'native',
412
+ verdict: 'unverified',
413
+ };
414
+ recordSlot(account.id, slot);
415
+ }
416
+ function persistManifest(manifestPath, manifest) {
417
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true, mode: 0o700 });
418
+ atomicWriteFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
419
+ }
420
+ function harnessManifest(manifest, agent, canonical) {
421
+ const existing = manifest.harnesses[agent];
422
+ if (existing)
423
+ return existing;
424
+ const created = { canonical, slots: [], trashed: [], deferred: [] };
425
+ manifest.harnesses[agent] = created;
426
+ return created;
427
+ }
428
+ export async function applyAccountMigration(agents = [...ALL_AGENT_IDS], deps = {}) {
429
+ const plan = await planAccountMigration(agents, deps);
430
+ const now = deps.now ?? (() => new Date());
431
+ const at = now();
432
+ const stamp = at.toISOString();
433
+ const manifestPath = path.join(getHistoryDir(), 'accounts', `migration-${stamp.replace(/[:.]/g, '-')}.json`);
434
+ const manifest = {
435
+ schema: ACCOUNT_MIGRATION_SCHEMA,
436
+ at: stamp,
437
+ dryRun: false,
438
+ status: 'planned',
439
+ plan,
440
+ harnesses: {},
441
+ map: {},
442
+ };
443
+ persistManifest(manifestPath, manifest);
444
+ const remaps = [];
445
+ const taken = takenNames(readMeta());
446
+ for (const h of plan.harnesses) {
447
+ const entry = harnessManifest(manifest, h.agent, h.canonical);
448
+ const labelToAccount = new Map();
449
+ const identityToAccount = new Map();
450
+ const movedAccountIds = [];
451
+ const stillPresent = new Set(h.inventory.map((i) => i.label));
452
+ const byLabel = new Map(h.inventory.map((i) => [i.label, i]));
453
+ const slotActions = h.actions.filter((a) => a.kind === 'slot');
454
+ const trashActions = h.actions.filter((a) => a.kind === 'trash');
455
+ for (const action of h.actions.filter((a) => a.kind === 'defer')) {
456
+ entry.deferred.push({ label: action.label, reason: action.reason });
457
+ }
458
+ persistManifest(manifestPath, manifest);
459
+ for (const action of slotActions) {
460
+ const item = byLabel.get(action.label);
461
+ if (!item)
462
+ throw new Error(`Plan named ${h.agent}@${action.label} but inventory has no such install.`);
463
+ const account = resolveOrRegisterAccount(item, taken);
464
+ const dest = assertSlotAbsent(h.agent, account.id);
465
+ moveHomeToSlot(item, dest);
466
+ recordMovedSlot(h.agent, account, dest);
467
+ remaps.push({ from: item.home, to: dest });
468
+ labelToAccount.set(item.label, account.id);
469
+ if (item.identityKey)
470
+ identityToAccount.set(item.identityKey, account.id);
471
+ movedAccountIds.push(account.id);
472
+ entry.slots.push({ oldLabel: item.label, accountId: account.id, accountName: account.name, slotDir: dest });
473
+ manifest.map[`${h.agent}@${item.label}`] = dest;
474
+ persistManifest(manifestPath, manifest);
475
+ if (item.label === h.canonical) {
476
+ fs.mkdirSync(item.home, { recursive: true, mode: 0o700 });
477
+ }
478
+ else {
479
+ const trashPath = softDeleteVersionDir(h.agent, item.label);
480
+ if (!trashPath)
481
+ throw new Error(`Failed to trash binary leftover ${h.agent}@${item.label}.`);
482
+ removeVersionedAlias(h.agent, item.label);
483
+ remaps.push({ from: item.dir, to: trashPath });
484
+ stillPresent.delete(item.label);
485
+ entry.trashed.push({ label: item.label, reason: 'binary leftover after home moved to slot', trashPath });
486
+ manifest.map[`${h.agent}@${item.label}#binary`] = trashPath;
487
+ persistManifest(manifestPath, manifest);
488
+ }
489
+ }
490
+ for (const item of h.inventory) {
491
+ if (!item.identityKey)
492
+ continue;
493
+ const kept = identityToAccount.get(item.identityKey);
494
+ if (kept)
495
+ labelToAccount.set(item.label, kept);
496
+ }
497
+ for (const action of trashActions) {
498
+ const item = byLabel.get(action.label);
499
+ if (!item)
500
+ throw new Error(`Plan named ${h.agent}@${action.label} but inventory has no such install.`);
501
+ const trashPath = softDeleteVersionDir(h.agent, item.label);
502
+ if (!trashPath)
503
+ throw new Error(`Failed to trash ${h.agent}@${item.label}.`);
504
+ removeVersionedAlias(h.agent, item.label);
505
+ remaps.push({ from: item.dir, to: trashPath });
506
+ stillPresent.delete(item.label);
507
+ entry.trashed.push({ label: item.label, reason: action.reason, trashPath });
508
+ manifest.map[`${h.agent}@${item.label}`] = trashPath;
509
+ persistManifest(manifestPath, manifest);
510
+ }
511
+ if (h.canonical)
512
+ setGlobalDefault(h.agent, h.canonical);
513
+ rewriteBindings(h.agent, labelToAccount, stillPresent);
514
+ clearHomes(movedAccountIds);
515
+ invalidateInstalledVersionsCache(h.agent);
516
+ }
517
+ const sessionsReindexed = reindexMovedSessionPaths(remaps);
518
+ manifest.status = 'complete';
519
+ persistManifest(manifestPath, manifest);
520
+ return { plan, manifest, manifestPath, sessionsReindexed };
521
+ }
522
+ /**
523
+ * Upgrade hook: print a dry-run report when leftover per-account installations
524
+ * exist. Never applies. `--apply` stays an explicit `agents accounts migrate`
525
+ * flag this release.
526
+ */
527
+ export async function reportAccountSlotMigrationOnUpgrade() {
528
+ const plan = await planAccountMigration();
529
+ const work = plan.totals.slots + plan.totals.trash + plan.totals.deferred;
530
+ if (work === 0)
531
+ return;
532
+ const summary = formatMigrationPlan(plan);
533
+ process.stderr.write(`[agents] Account model v2: ${plan.totals.installations} leftover per-account installation(s) can fold into slots.\n`
534
+ + `${summary}\n`
535
+ + `Dry-run (this upgrade does not move anything). Apply with: agents accounts migrate --apply\n`);
536
+ }
@@ -0,0 +1,13 @@
1
+ import type { AgentId, DeviceAccountSlot, Meta } from '../types.js';
2
+ export type { DeviceAccountSlot };
3
+ export declare function slotDir(harness: AgentId, accountId: string): string;
4
+ export declare function readSlots(meta: Pick<Meta, 'deviceAccounts'>): Record<string, DeviceAccountSlot>;
5
+ export declare function recordSlot(accountId: string, slot: DeviceAccountSlot): void;
6
+ /**
7
+ * Create the HOME-shaped slot dir and project settings/resources from the
8
+ * managed install. Does not copy credentials (carryForwardSettings excludes
9
+ * them; resource writers write skills/hooks/commands, never OAuth files).
10
+ * Does not persist the slot record — call {@link recordSlot} after identity
11
+ * is captured.
12
+ */
13
+ export declare function ensureSlot(harness: AgentId, accountId: string): DeviceAccountSlot;