@cleocode/core 2026.9.16 → 2026.9.17

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 (216) hide show
  1. package/dist/changesets/parser.d.ts.map +1 -1
  2. package/dist/changesets/parser.js +67 -0
  3. package/dist/changesets/parser.js.map +1 -1
  4. package/dist/crypto/credentials.d.ts +128 -26
  5. package/dist/crypto/credentials.d.ts.map +1 -1
  6. package/dist/crypto/credentials.js +183 -142
  7. package/dist/crypto/credentials.js.map +1 -1
  8. package/dist/docs/export-document.js +14705 -12949
  9. package/dist/docs/export-document.js.map +3 -3
  10. package/dist/doctor/index.d.ts +1 -1
  11. package/dist/doctor/index.d.ts.map +1 -1
  12. package/dist/doctor/index.js +1 -1
  13. package/dist/doctor/index.js.map +1 -1
  14. package/dist/doctor/nexus-schema-residency.d.ts +1 -1
  15. package/dist/doctor/nexus-schema-residency.d.ts.map +1 -1
  16. package/dist/doctor/nexus-schema-residency.js +1 -0
  17. package/dist/doctor/nexus-schema-residency.js.map +1 -1
  18. package/dist/doctor/project-identity.d.ts +124 -0
  19. package/dist/doctor/project-identity.d.ts.map +1 -0
  20. package/dist/doctor/project-identity.js +307 -0
  21. package/dist/doctor/project-identity.js.map +1 -0
  22. package/dist/doctor/superseded-store.d.ts +8 -0
  23. package/dist/doctor/superseded-store.d.ts.map +1 -1
  24. package/dist/doctor/superseded-store.js +48 -3
  25. package/dist/doctor/superseded-store.js.map +1 -1
  26. package/dist/init.d.ts +7 -0
  27. package/dist/init.d.ts.map +1 -1
  28. package/dist/init.js +17 -2
  29. package/dist/init.js.map +1 -1
  30. package/dist/internal.d.ts +5 -2
  31. package/dist/internal.d.ts.map +1 -1
  32. package/dist/internal.js +4 -2
  33. package/dist/internal.js.map +1 -1
  34. package/dist/llm/credentials-store.d.ts +10 -0
  35. package/dist/llm/credentials-store.d.ts.map +1 -1
  36. package/dist/llm/credentials-store.js +7 -1
  37. package/dist/llm/credentials-store.js.map +1 -1
  38. package/dist/llm/plugin-facade.js +13368 -1550
  39. package/dist/llm/plugin-facade.js.map +3 -3
  40. package/dist/nexus/analyze-orchestrator.d.ts +20 -3
  41. package/dist/nexus/analyze-orchestrator.d.ts.map +1 -1
  42. package/dist/nexus/analyze-orchestrator.js +159 -10
  43. package/dist/nexus/analyze-orchestrator.js.map +1 -1
  44. package/dist/nexus/assessment-store.d.ts +57 -0
  45. package/dist/nexus/assessment-store.d.ts.map +1 -0
  46. package/dist/nexus/assessment-store.js +73 -0
  47. package/dist/nexus/assessment-store.js.map +1 -0
  48. package/dist/nexus/freshness.d.ts +131 -0
  49. package/dist/nexus/freshness.d.ts.map +1 -0
  50. package/dist/nexus/freshness.js +391 -0
  51. package/dist/nexus/freshness.js.map +1 -0
  52. package/dist/nexus/graph-manifest.d.ts +88 -0
  53. package/dist/nexus/graph-manifest.d.ts.map +1 -0
  54. package/dist/nexus/graph-manifest.js +114 -0
  55. package/dist/nexus/graph-manifest.js.map +1 -0
  56. package/dist/nexus/knowledge.d.ts +21 -1
  57. package/dist/nexus/knowledge.d.ts.map +1 -1
  58. package/dist/nexus/knowledge.js +76 -37
  59. package/dist/nexus/knowledge.js.map +1 -1
  60. package/dist/nexus/path-map.d.ts +55 -0
  61. package/dist/nexus/path-map.d.ts.map +1 -0
  62. package/dist/nexus/path-map.js +87 -0
  63. package/dist/nexus/path-map.js.map +1 -0
  64. package/dist/nexus/projects-clean.d.ts +21 -28
  65. package/dist/nexus/projects-clean.d.ts.map +1 -1
  66. package/dist/nexus/projects-clean.js +168 -85
  67. package/dist/nexus/projects-clean.js.map +1 -1
  68. package/dist/nexus/query.d.ts.map +1 -1
  69. package/dist/nexus/query.js +3 -2
  70. package/dist/nexus/query.js.map +1 -1
  71. package/dist/nexus/registry-hygiene.d.ts +70 -0
  72. package/dist/nexus/registry-hygiene.d.ts.map +1 -0
  73. package/dist/nexus/registry-hygiene.js +113 -0
  74. package/dist/nexus/registry-hygiene.js.map +1 -0
  75. package/dist/nexus/registry.d.ts +2 -2
  76. package/dist/nexus/registry.d.ts.map +1 -1
  77. package/dist/nexus/registry.js +48 -12
  78. package/dist/nexus/registry.js.map +1 -1
  79. package/dist/paths.d.ts +26 -0
  80. package/dist/paths.d.ts.map +1 -1
  81. package/dist/paths.js +99 -6
  82. package/dist/paths.js.map +1 -1
  83. package/dist/render/nexus/graph/index.d.ts.map +1 -1
  84. package/dist/render/nexus/graph/index.js +8 -0
  85. package/dist/render/nexus/graph/index.js.map +1 -1
  86. package/dist/scaffold/ensure-config.d.ts +12 -3
  87. package/dist/scaffold/ensure-config.d.ts.map +1 -1
  88. package/dist/scaffold/ensure-config.js +61 -38
  89. package/dist/scaffold/ensure-config.js.map +1 -1
  90. package/dist/scaffold/project-identity.d.ts +136 -0
  91. package/dist/scaffold/project-identity.d.ts.map +1 -0
  92. package/dist/scaffold/project-identity.js +225 -0
  93. package/dist/scaffold/project-identity.js.map +1 -0
  94. package/dist/selfimprove/fix-gen.js +13319 -1556
  95. package/dist/selfimprove/fix-gen.js.map +3 -3
  96. package/dist/store/agent-api-key.d.ts +114 -0
  97. package/dist/store/agent-api-key.d.ts.map +1 -0
  98. package/dist/store/agent-api-key.js +136 -0
  99. package/dist/store/agent-api-key.js.map +1 -0
  100. package/dist/store/agent-registry-accessor.d.ts +4 -2
  101. package/dist/store/agent-registry-accessor.d.ts.map +1 -1
  102. package/dist/store/agent-registry-accessor.js +39 -89
  103. package/dist/store/agent-registry-accessor.js.map +1 -1
  104. package/dist/store/api-key-kdf.d.ts +7 -0
  105. package/dist/store/api-key-kdf.d.ts.map +1 -1
  106. package/dist/store/api-key-kdf.js +7 -0
  107. package/dist/store/api-key-kdf.js.map +1 -1
  108. package/dist/store/backup-crypto.d.ts +49 -0
  109. package/dist/store/backup-crypto.d.ts.map +1 -1
  110. package/dist/store/backup-crypto.js +154 -0
  111. package/dist/store/backup-crypto.js.map +1 -1
  112. package/dist/store/credential-transfer.d.ts +303 -0
  113. package/dist/store/credential-transfer.d.ts.map +1 -0
  114. package/dist/store/credential-transfer.js +774 -0
  115. package/dist/store/credential-transfer.js.map +1 -0
  116. package/dist/store/exodus/column-transforms.d.ts +79 -4
  117. package/dist/store/exodus/column-transforms.d.ts.map +1 -1
  118. package/dist/store/exodus/column-transforms.js +121 -4
  119. package/dist/store/exodus/column-transforms.js.map +1 -1
  120. package/dist/store/exodus/index.d.ts +1 -0
  121. package/dist/store/exodus/index.d.ts.map +1 -1
  122. package/dist/store/exodus/index.js +1 -0
  123. package/dist/store/exodus/index.js.map +1 -1
  124. package/dist/store/exodus/migrate.d.ts +17 -1
  125. package/dist/store/exodus/migrate.d.ts.map +1 -1
  126. package/dist/store/exodus/migrate.js +155 -33
  127. package/dist/store/exodus/migrate.js.map +1 -1
  128. package/dist/store/exodus/on-open.d.ts +14 -0
  129. package/dist/store/exodus/on-open.d.ts.map +1 -1
  130. package/dist/store/exodus/on-open.js +115 -6
  131. package/dist/store/exodus/on-open.js.map +1 -1
  132. package/dist/store/exodus/reconcile.d.ts +103 -0
  133. package/dist/store/exodus/reconcile.d.ts.map +1 -0
  134. package/dist/store/exodus/reconcile.js +728 -0
  135. package/dist/store/exodus/reconcile.js.map +1 -0
  136. package/dist/store/exodus/recovery.d.ts.map +1 -1
  137. package/dist/store/exodus/recovery.js +14 -2
  138. package/dist/store/exodus/recovery.js.map +1 -1
  139. package/dist/store/exodus/runtime-targets.d.ts +39 -0
  140. package/dist/store/exodus/runtime-targets.d.ts.map +1 -0
  141. package/dist/store/exodus/runtime-targets.js +63 -0
  142. package/dist/store/exodus/runtime-targets.js.map +1 -0
  143. package/dist/store/exodus/table-name-map.d.ts.map +1 -1
  144. package/dist/store/exodus/table-name-map.js +5 -0
  145. package/dist/store/exodus/table-name-map.js.map +1 -1
  146. package/dist/store/exodus/table-order.d.ts +35 -0
  147. package/dist/store/exodus/table-order.d.ts.map +1 -0
  148. package/dist/store/exodus/table-order.js +62 -0
  149. package/dist/store/exodus/table-order.js.map +1 -0
  150. package/dist/store/exodus/verify-migration.d.ts +4 -1
  151. package/dist/store/exodus/verify-migration.d.ts.map +1 -1
  152. package/dist/store/exodus/verify-migration.js +142 -10
  153. package/dist/store/exodus/verify-migration.js.map +1 -1
  154. package/dist/store/global-salt.d.ts +16 -0
  155. package/dist/store/global-salt.d.ts.map +1 -1
  156. package/dist/store/global-salt.js +22 -7
  157. package/dist/store/global-salt.js.map +1 -1
  158. package/dist/store/legacy-tasks-lineage.d.ts +86 -0
  159. package/dist/store/legacy-tasks-lineage.d.ts.map +1 -0
  160. package/dist/store/legacy-tasks-lineage.js +285 -0
  161. package/dist/store/legacy-tasks-lineage.js.map +1 -0
  162. package/dist/store/migration-manager.d.ts +27 -0
  163. package/dist/store/migration-manager.d.ts.map +1 -1
  164. package/dist/store/migration-manager.js +63 -0
  165. package/dist/store/migration-manager.js.map +1 -1
  166. package/dist/store/portable-bundle-import.d.ts +117 -0
  167. package/dist/store/portable-bundle-import.d.ts.map +1 -0
  168. package/dist/store/portable-bundle-import.js +589 -0
  169. package/dist/store/portable-bundle-import.js.map +1 -0
  170. package/dist/store/portable-bundle-relocate.d.ts +66 -0
  171. package/dist/store/portable-bundle-relocate.d.ts.map +1 -0
  172. package/dist/store/portable-bundle-relocate.js +304 -0
  173. package/dist/store/portable-bundle-relocate.js.map +1 -0
  174. package/dist/store/portable-bundle-scan.d.ts +167 -0
  175. package/dist/store/portable-bundle-scan.d.ts.map +1 -0
  176. package/dist/store/portable-bundle-scan.js +569 -0
  177. package/dist/store/portable-bundle-scan.js.map +1 -0
  178. package/dist/store/portable-bundle.d.ts +146 -0
  179. package/dist/store/portable-bundle.d.ts.map +1 -0
  180. package/dist/store/portable-bundle.js +674 -0
  181. package/dist/store/portable-bundle.js.map +1 -0
  182. package/dist/store/regenerators.d.ts +3 -1
  183. package/dist/store/regenerators.d.ts.map +1 -1
  184. package/dist/store/regenerators.js +6 -2
  185. package/dist/store/regenerators.js.map +1 -1
  186. package/dist/store/schema/cleo-global/nexus.d.ts +98 -0
  187. package/dist/store/schema/cleo-global/nexus.d.ts.map +1 -1
  188. package/dist/store/schema/cleo-global/nexus.js +19 -0
  189. package/dist/store/schema/cleo-global/nexus.js.map +1 -1
  190. package/dist/store/schema/nexus-schema.d.ts +102 -0
  191. package/dist/store/schema/nexus-schema.d.ts.map +1 -1
  192. package/dist/store/schema/nexus-schema.js +24 -0
  193. package/dist/store/schema/nexus-schema.js.map +1 -1
  194. package/dist/store/split-brain-import.d.ts +176 -0
  195. package/dist/store/split-brain-import.d.ts.map +1 -0
  196. package/dist/store/split-brain-import.js +680 -0
  197. package/dist/store/split-brain-import.js.map +1 -0
  198. package/dist/store/sqlite.d.ts +29 -0
  199. package/dist/store/sqlite.d.ts.map +1 -1
  200. package/dist/store/sqlite.js +57 -5
  201. package/dist/store/sqlite.js.map +1 -1
  202. package/dist/system/health.d.ts.map +1 -1
  203. package/dist/system/health.js +30 -5
  204. package/dist/system/health.js.map +1 -1
  205. package/dist/upgrade.d.ts.map +1 -1
  206. package/dist/upgrade.js +60 -0
  207. package/dist/upgrade.js.map +1 -1
  208. package/dist/validation/doctor/checks.d.ts +14 -0
  209. package/dist/validation/doctor/checks.d.ts.map +1 -1
  210. package/dist/validation/doctor/checks.js +33 -0
  211. package/dist/validation/doctor/checks.js.map +1 -1
  212. package/migrations/drizzle-cleo-global/20260924000000_t12354-project-paths/migration.sql +40 -0
  213. package/migrations/drizzle-nexus/20260924000000_t12315-nexus-parse-cache/migration.sql +21 -0
  214. package/package.json +13 -13
  215. package/templates/CLEO-INJECTION.md +3 -3
  216. package/templates/cleo-gitignore +7 -0
@@ -0,0 +1,774 @@
1
+ /**
2
+ * credential-transfer.ts — move stored credentials between devices without
3
+ * moving the device key.
4
+ *
5
+ * Every stored credential is encrypted under a key derived from the local
6
+ * `machine-key` (`crypto/credentials.ts`). Copying that key to another device
7
+ * would make the two devices one trust domain, so it is never exported. A
8
+ * credential crosses devices one of two ways instead:
9
+ *
10
+ * - **Sealed** (encrypted backups) — {@link sealCredentials} decrypts every
11
+ * local credential and seals the plaintexts under a passphrase-derived key
12
+ * (scrypt + AES-256-GCM, the same envelope as encrypted bundles).
13
+ * {@link unsealCredentials} on the target device opens that payload and
14
+ * re-encrypts each credential under the TARGET machine-key.
15
+ * - **Re-entered** (unencrypted backups) — {@link redactCredentialCiphertexts}
16
+ * blanks the device-bound ciphertexts in the STAGED snapshot (they are
17
+ * useless elsewhere) and {@link listCredentialsForReentry} names every
18
+ * credential with the one command that re-enters it.
19
+ *
20
+ * This module also migrates project credentials from the legacy path-bound
21
+ * KDF to the project-identity KDF ({@link migrateProjectCredentials}), so a
22
+ * moved project directory keeps its credentials.
23
+ *
24
+ * Stores covered:
25
+ *
26
+ * | Store | Location | Encryption |
27
+ * |-------|----------|------------|
28
+ * | `project-agent` | project `cleo.db` → `tasks_agent_credentials.api_key_encrypted` | project KDF |
29
+ * | `service-connection` | global `cleo.db` → `service_connections.credentials_enc` | global KDF, id `service:<provider>:<label>` |
30
+ * | `llm-pool` | `<cleoHome>/llm-credentials.json` | none (0600 plaintext file) |
31
+ * | `agent-registry` | global `cleo.db` → `agent_registry_agents.api_key_encrypted` | global KDF, id `agent:<agentId>` (T12352) |
32
+ *
33
+ * The LLM pool stays a plaintext 0600 file on purpose (T12326 decision).
34
+ * Encrypting it under the machine-key would add little on-device protection,
35
+ * because the key sits in the same directory with the same mode. It would also
36
+ * force a key read into the synchronous resolver hot path
37
+ * (`pickCredentialForProviderSync`) and break the tools that read the file
38
+ * directly. What matters is that the pool never enters an unencrypted bundle,
39
+ * and this module guarantees that: the pool is either sealed or listed for
40
+ * re-entry.
41
+ *
42
+ * Callers pass FILE PATHS (live stores, or a backup's staged snapshots); the
43
+ * module opens short-lived raw handles and closes them before returning.
44
+ *
45
+ * @task T12326
46
+ * @module store/credential-transfer
47
+ */
48
+ import fs from 'node:fs';
49
+ import { createRequire } from 'node:module';
50
+ import path from 'node:path';
51
+ import { decryptGlobal, decryptProjectSecret, encryptGlobal, encryptProjectSecret, } from '../crypto/credentials.js';
52
+ import { addCredential, credentialsStorePath, isStoredCredential, } from '../llm/credentials-store.js';
53
+ import { getCleoHome } from '../paths.js';
54
+ import { openAgentApiKey, sealAgentApiKey } from './agent-api-key.js';
55
+ import { decryptBundle, encryptBundle } from './backup-crypto.js';
56
+ // ---------------------------------------------------------------------------
57
+ // node:sqlite interop (createRequire — Vitest strips `node:` prefix)
58
+ // ---------------------------------------------------------------------------
59
+ const _require = createRequire(import.meta.url);
60
+ const { DatabaseSync } = _require('node:sqlite');
61
+ /**
62
+ * Error thrown by credential transfer. Raised BEFORE any write, so a caller
63
+ * that catches it knows no store was modified.
64
+ *
65
+ * @task T12326
66
+ */
67
+ export class CredentialTransferError extends Error {
68
+ /** Machine-readable failure code. */
69
+ code;
70
+ /**
71
+ * @param code - Failure code.
72
+ * @param message - Human-readable explanation, including the remedy.
73
+ */
74
+ constructor(code, message) {
75
+ super(message);
76
+ this.name = 'CredentialTransferError';
77
+ this.code = code;
78
+ }
79
+ }
80
+ // ---------------------------------------------------------------------------
81
+ // Sealed payload shape
82
+ // ---------------------------------------------------------------------------
83
+ /** Discriminator written into every sealed payload. */
84
+ const PAYLOAD_FORMAT = 'cleo-credential-transfer';
85
+ /** Payload schema version. */
86
+ const PAYLOAD_VERSION = 1;
87
+ // ---------------------------------------------------------------------------
88
+ // Re-entry commands
89
+ // ---------------------------------------------------------------------------
90
+ /** Quote a shell argument only when it needs quoting. */
91
+ function shellArg(value) {
92
+ return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
93
+ }
94
+ /**
95
+ * Build the re-entry record for a credential: its identity, why it must be
96
+ * re-entered, and the one command that re-enters it.
97
+ *
98
+ * @param descriptor - The credential.
99
+ * @param reason - Why it could not be carried over.
100
+ * @returns The re-entry record.
101
+ * @task T12326
102
+ */
103
+ export function reentryFor(descriptor, reason) {
104
+ return { ...descriptor, reason, reentryCommand: reentryCommandFor(descriptor) };
105
+ }
106
+ /**
107
+ * The one command that re-enters a credential. Secrets appear as a
108
+ * placeholder, never a value.
109
+ */
110
+ function reentryCommandFor(descriptor) {
111
+ switch (descriptor.store) {
112
+ case 'project-agent':
113
+ case 'agent-registry':
114
+ return `cleo agent register --id ${shellArg(descriptor.id)} --name ${shellArg(descriptor.label)} --api-key <API_KEY>`;
115
+ case 'service-connection': {
116
+ const { provider, label } = splitProviderLabel(descriptor.id);
117
+ return `cleo service connect ${shellArg(provider)} --label ${shellArg(label)} --token <TOKEN>`;
118
+ }
119
+ case 'llm-pool': {
120
+ const { provider, label } = splitProviderLabel(descriptor.id);
121
+ return `printf '%s' "$API_KEY" | cleo llm add ${shellArg(provider)} --label ${shellArg(label)} --api-key-stdin`;
122
+ }
123
+ }
124
+ }
125
+ /** Split a `<provider>:<label>` id (labels may contain `:`). */
126
+ function splitProviderLabel(id) {
127
+ const idx = id.indexOf(':');
128
+ return idx < 0
129
+ ? { provider: id, label: 'default' }
130
+ : { provider: id.slice(0, idx), label: id.slice(idx + 1) };
131
+ }
132
+ /** Open a database file, run `fn`, always close. Returns `fallback` when the file is absent. */
133
+ function withDb(dbPath, fallback, fn) {
134
+ if (dbPath === undefined || !fs.existsSync(dbPath))
135
+ return fallback;
136
+ const db = new DatabaseSync(dbPath);
137
+ try {
138
+ return fn(db);
139
+ }
140
+ finally {
141
+ db.close();
142
+ }
143
+ }
144
+ /**
145
+ * True when `table` exists in `db` and has every one of `columns`. Stores in
146
+ * the wild include older or partial schemas; a table without the columns this
147
+ * module reads is treated as holding no credentials rather than failing the
148
+ * caller (a backup must not abort on it).
149
+ */
150
+ function hasTable(db, table, columns = []) {
151
+ if (db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) ===
152
+ undefined) {
153
+ return false;
154
+ }
155
+ if (columns.length === 0)
156
+ return true;
157
+ const present = new Set(db.prepare(`PRAGMA table_info("${table}")`).all().map((c) => c.name));
158
+ return columns.every((c) => present.has(c));
159
+ }
160
+ /** Read `tasks_agent_credentials` rows that carry ciphertext. */
161
+ function readProjectAgentRows(dbPath) {
162
+ return withDb(dbPath, [], (db) => {
163
+ if (!hasTable(db, 'tasks_agent_credentials', ['agent_id', 'display_name', 'api_key_encrypted'])) {
164
+ return [];
165
+ }
166
+ const rows = db
167
+ .prepare("SELECT agent_id, display_name, api_key_encrypted FROM tasks_agent_credentials WHERE api_key_encrypted <> '' ORDER BY agent_id")
168
+ .all();
169
+ return rows.map((r) => ({
170
+ descriptor: { store: 'project-agent', id: r.agent_id, label: r.display_name },
171
+ material: r.api_key_encrypted,
172
+ }));
173
+ });
174
+ }
175
+ /** Read `service_connections` rows that carry ciphertext. */
176
+ function readServiceConnectionRows(dbPath) {
177
+ return withDb(dbPath, [], (db) => {
178
+ if (!hasTable(db, 'service_connections', ['provider', 'label', 'credentials_enc']))
179
+ return [];
180
+ const rows = db
181
+ .prepare("SELECT provider, label, credentials_enc FROM service_connections WHERE credentials_enc IS NOT NULL AND credentials_enc <> '' ORDER BY provider, label")
182
+ .all();
183
+ return rows.map((r) => ({
184
+ descriptor: {
185
+ store: 'service-connection',
186
+ id: `${r.provider}:${r.label}`,
187
+ label: `${r.provider} service connection "${r.label}"`,
188
+ },
189
+ material: r.credentials_enc,
190
+ }));
191
+ });
192
+ }
193
+ /** Read `agent_registry_agents` rows that carry a stored key (any format). */
194
+ function readAgentRegistryRows(dbPath) {
195
+ return withDb(dbPath, [], (db) => {
196
+ if (!hasTable(db, 'agent_registry_agents', ['agent_id', 'name', 'api_key_encrypted'])) {
197
+ return [];
198
+ }
199
+ const rows = db
200
+ .prepare("SELECT agent_id, name, api_key_encrypted FROM agent_registry_agents WHERE api_key_encrypted IS NOT NULL AND api_key_encrypted <> '' ORDER BY agent_id")
201
+ .all();
202
+ return rows.map((r) => ({
203
+ descriptor: { store: 'agent-registry', id: r.agent_id, label: r.name },
204
+ material: r.api_key_encrypted,
205
+ }));
206
+ });
207
+ }
208
+ /** Read the LLM pool file's entries. A missing or malformed file yields none. */
209
+ function readLlmPoolRows(storePath) {
210
+ if (storePath === undefined || !fs.existsSync(storePath))
211
+ return [];
212
+ let parsed;
213
+ try {
214
+ parsed = JSON.parse(fs.readFileSync(storePath, 'utf-8'));
215
+ }
216
+ catch {
217
+ return [];
218
+ }
219
+ if (typeof parsed !== 'object' || parsed === null || !('credentials' in parsed))
220
+ return [];
221
+ const credentials = parsed.credentials;
222
+ if (!Array.isArray(credentials))
223
+ return [];
224
+ return credentials.filter(isStoredCredential).map((c) => ({
225
+ descriptor: {
226
+ store: 'llm-pool',
227
+ id: `${c.provider}:${c.label}`,
228
+ label: `${c.provider} LLM credential "${c.label}"`,
229
+ },
230
+ material: JSON.stringify(c),
231
+ }));
232
+ }
233
+ /** Every credential row across the given sources, still encrypted. */
234
+ function readAllRows(sources) {
235
+ return [
236
+ ...readProjectAgentRows(sources.projectDbPath),
237
+ ...readServiceConnectionRows(sources.globalDbPath),
238
+ ...readAgentRegistryRows(sources.globalDbPath),
239
+ ...readLlmPoolRows(sources.llmStorePath),
240
+ ];
241
+ }
242
+ // ---------------------------------------------------------------------------
243
+ // Unencrypted-bundle path: list + redact
244
+ // ---------------------------------------------------------------------------
245
+ /**
246
+ * List every credential in `sources` with the command that re-enters it.
247
+ * Reads identities only — nothing is decrypted.
248
+ *
249
+ * Use for an UNENCRYPTED backup: its import cannot carry credentials, so the
250
+ * import report prints this list.
251
+ *
252
+ * @param sources - Stores to enumerate.
253
+ * @param reason - Why the credentials must be re-entered (shown per entry).
254
+ * @returns One re-entry record per credential.
255
+ * @task T12326
256
+ */
257
+ export function listCredentialsForReentry(sources, reason = 'unencrypted backups do not carry credentials') {
258
+ return readAllRows(sources).map((row) => reentryFor(row.descriptor, reason));
259
+ }
260
+ /** Resolve a path for comparison, following symlinks when the file exists. */
261
+ function canonicalPath(p) {
262
+ try {
263
+ return fs.realpathSync(p);
264
+ }
265
+ catch {
266
+ return path.resolve(p);
267
+ }
268
+ }
269
+ /**
270
+ * Refuse to redact anything that looks like a live store: the global
271
+ * `cleo.db`, or any database directly inside a `.cleo/` directory.
272
+ */
273
+ function assertStagedCopy(dbPath) {
274
+ const resolved = canonicalPath(dbPath);
275
+ const liveGlobal = canonicalPath(path.join(getCleoHome(), 'cleo.db'));
276
+ if (resolved === liveGlobal || path.basename(path.dirname(resolved)) === '.cleo') {
277
+ throw new CredentialTransferError('E_CREDENTIAL_LIVE_STORE', `Refusing to redact credentials in a live store (${resolved}). ` +
278
+ 'Redaction applies only to staged backup snapshots.');
279
+ }
280
+ }
281
+ /**
282
+ * Blank the device-bound ciphertexts in STAGED snapshot databases and report
283
+ * what was removed.
284
+ *
285
+ * An unencrypted bundle must not carry credential ciphertext: it cannot be
286
+ * decrypted on another device, and it would be decryptable by anyone who
287
+ * later obtains this device's machine-key. Sets
288
+ * `tasks_agent_credentials.api_key_encrypted = ''` (the column is NOT NULL)
289
+ * and `service_connections.credentials_enc = NULL`; every non-secret column
290
+ * is kept. The LLM pool FILE is not touched — the bundle writer must exclude
291
+ * it, and can take its entries from the returned list.
292
+ *
293
+ * @param sources - Staged snapshot paths (`projectDbPath` / `globalDbPath`);
294
+ * `llmStorePath` is listed, not modified.
295
+ * @returns Re-entry records for every redacted or excluded credential.
296
+ * @throws {CredentialTransferError} `E_CREDENTIAL_LIVE_STORE` when a path is
297
+ * a live store.
298
+ * @task T12326
299
+ */
300
+ export function redactCredentialCiphertexts(sources) {
301
+ if (sources.projectDbPath !== undefined)
302
+ assertStagedCopy(sources.projectDbPath);
303
+ if (sources.globalDbPath !== undefined)
304
+ assertStagedCopy(sources.globalDbPath);
305
+ const listed = listCredentialsForReentry(sources);
306
+ withDb(sources.projectDbPath, undefined, (db) => {
307
+ if (hasTable(db, 'tasks_agent_credentials', ['api_key_encrypted'])) {
308
+ db.prepare("UPDATE tasks_agent_credentials SET api_key_encrypted = ''").run();
309
+ }
310
+ });
311
+ withDb(sources.globalDbPath, undefined, (db) => {
312
+ if (hasTable(db, 'service_connections', ['credentials_enc'])) {
313
+ db.prepare('UPDATE service_connections SET credentials_enc = NULL WHERE credentials_enc IS NOT NULL').run();
314
+ }
315
+ if (hasTable(db, 'agent_registry_agents', ['api_key_encrypted'])) {
316
+ db.prepare('UPDATE agent_registry_agents SET api_key_encrypted = NULL WHERE api_key_encrypted IS NOT NULL').run();
317
+ }
318
+ });
319
+ return listed;
320
+ }
321
+ // ---------------------------------------------------------------------------
322
+ // Encrypted-bundle path: seal + unseal
323
+ // ---------------------------------------------------------------------------
324
+ /** Require a project id when project credentials are present. */
325
+ function requireProjectId(projectId, action) {
326
+ if (projectId === undefined || projectId.length === 0) {
327
+ throw new CredentialTransferError('E_CREDENTIAL_PROJECT_ID', `Cannot ${action} project credentials without the project's id ` +
328
+ '(the projectId in .cleo/project-info.json).');
329
+ }
330
+ return projectId;
331
+ }
332
+ /** Decrypt one stored row with the LOCAL keys. */
333
+ async function openRow(row, sources) {
334
+ switch (row.descriptor.store) {
335
+ case 'project-agent': {
336
+ const projectId = requireProjectId(sources.projectId, 'seal');
337
+ const result = await decryptProjectSecret(row.material, {
338
+ projectId,
339
+ legacyProjectPaths: sources.legacyProjectPaths,
340
+ cleoHome: sources.cleoHome,
341
+ });
342
+ return result.plaintext;
343
+ }
344
+ case 'service-connection':
345
+ return decryptGlobal(row.material, `service:${row.descriptor.id}`, {
346
+ cleoHome: sources.cleoHome,
347
+ });
348
+ case 'agent-registry': {
349
+ const key = await openAgentApiKey(row.material, row.descriptor.id, {
350
+ cleoHome: sources.cleoHome,
351
+ });
352
+ if (key.requiresReauth)
353
+ throw new Error(key.reason ?? 'the stored key is not recoverable');
354
+ return key.apiKey;
355
+ }
356
+ case 'llm-pool':
357
+ return row.material;
358
+ }
359
+ }
360
+ /**
361
+ * Seal every credential in `sources` under a passphrase.
362
+ *
363
+ * Each credential is decrypted with THIS device's keys and the plaintexts are
364
+ * sealed together (scrypt-derived key, AES-256-GCM — {@link encryptBundle}).
365
+ * The payload carries credentials, never key material: the machine-key and
366
+ * global-salt stay on this device. A credential this device cannot decrypt is
367
+ * not sealed; it is reported in `reentry` (and carried into the payload so
368
+ * the importing device reports it too).
369
+ *
370
+ * @param sources - Stores to read (live stores or staged snapshots).
371
+ * @param passphrase - User passphrase; the same one opens the payload.
372
+ * @returns Sealed bytes plus what was sealed and what needs re-entry. A
373
+ * project credential with no `sources.projectId` is reported for re-entry.
374
+ * @task T12326
375
+ */
376
+ export async function sealCredentials(sources, passphrase) {
377
+ const rows = readAllRows(sources);
378
+ const entries = [];
379
+ const reentry = [];
380
+ for (const row of rows) {
381
+ try {
382
+ entries.push({ ...row.descriptor, secret: await openRow(row, sources) });
383
+ }
384
+ catch (err) {
385
+ reentry.push(reentryFor(row.descriptor, `not decryptable on the exporting device: ${err instanceof Error ? err.message : String(err)}`));
386
+ }
387
+ }
388
+ const payload = {
389
+ format: PAYLOAD_FORMAT,
390
+ version: PAYLOAD_VERSION,
391
+ createdAt: new Date().toISOString(),
392
+ entries,
393
+ reentry,
394
+ };
395
+ const plaintext = Buffer.from(JSON.stringify(payload), 'utf8');
396
+ try {
397
+ return {
398
+ sealed: encryptBundle(plaintext, passphrase),
399
+ sealedCredentials: entries.map(({ store, id, label }) => ({ store, id, label })),
400
+ reentry,
401
+ };
402
+ }
403
+ finally {
404
+ plaintext.fill(0);
405
+ }
406
+ }
407
+ /** Type guard for a credential descriptor field set. */
408
+ function isDescriptor(value) {
409
+ if (typeof value !== 'object' || value === null)
410
+ return false;
411
+ const store = 'store' in value ? value.store : undefined;
412
+ return ((store === 'project-agent' ||
413
+ store === 'service-connection' ||
414
+ store === 'llm-pool' ||
415
+ store === 'agent-registry') &&
416
+ 'id' in value &&
417
+ typeof value.id === 'string' &&
418
+ 'label' in value &&
419
+ typeof value.label === 'string');
420
+ }
421
+ /** Type guard for a sealed entry. */
422
+ function isSealedEntry(value) {
423
+ return isDescriptor(value) && 'secret' in value && typeof value.secret === 'string';
424
+ }
425
+ /** Type guard for a carried re-entry record. */
426
+ function isReentry(value) {
427
+ return (isDescriptor(value) &&
428
+ 'reason' in value &&
429
+ typeof value.reason === 'string' &&
430
+ 'reentryCommand' in value &&
431
+ typeof value.reentryCommand === 'string');
432
+ }
433
+ /** Parse and validate a decrypted payload. */
434
+ function parsePayload(bytes) {
435
+ let parsed;
436
+ try {
437
+ parsed = JSON.parse(bytes.toString('utf8'));
438
+ }
439
+ catch {
440
+ parsed = undefined;
441
+ }
442
+ if (typeof parsed !== 'object' ||
443
+ parsed === null ||
444
+ !('format' in parsed) ||
445
+ parsed.format !== PAYLOAD_FORMAT ||
446
+ !('version' in parsed) ||
447
+ parsed.version !== PAYLOAD_VERSION ||
448
+ !('entries' in parsed) ||
449
+ !Array.isArray(parsed.entries) ||
450
+ !parsed.entries.every(isSealedEntry) ||
451
+ !('reentry' in parsed) ||
452
+ !Array.isArray(parsed.reentry) ||
453
+ !parsed.reentry.every(isReentry)) {
454
+ throw new CredentialTransferError('E_CREDENTIAL_PAYLOAD', 'The sealed credential payload opened but is not a supported credential payload.');
455
+ }
456
+ return {
457
+ format: PAYLOAD_FORMAT,
458
+ version: PAYLOAD_VERSION,
459
+ createdAt: 'createdAt' in parsed && typeof parsed.createdAt === 'string' ? parsed.createdAt : '',
460
+ entries: parsed.entries,
461
+ reentry: parsed.reentry,
462
+ };
463
+ }
464
+ /**
465
+ * Update one ciphertext cell in `table`; returns whether a row matched. A
466
+ * table missing any of `columns` matches nothing (reported, never thrown).
467
+ */
468
+ function updateCell(dbPath, table, columns, sql, params) {
469
+ return withDb(dbPath, false, (db) => {
470
+ if (!hasTable(db, table, columns))
471
+ return false;
472
+ return Number(db.prepare(sql).run(...params).changes) > 0;
473
+ });
474
+ }
475
+ /**
476
+ * Open a sealed payload and write every credential into the local stores,
477
+ * re-encrypted under THIS device's machine-key.
478
+ *
479
+ * Order of operations: the passphrase is checked and the payload validated
480
+ * BEFORE anything is written — a wrong passphrase leaves every store
481
+ * untouched. Database rows are updated in place (the restore must already
482
+ * have put the rows there); a credential whose row is absent, or whose store
483
+ * has no target, is reported for re-entry instead of being dropped silently.
484
+ *
485
+ * Call after the restored database files and any restored `global-salt` are in
486
+ * place: the global KDF reads the salt. Pass `targets.cleoHome` so the salt is
487
+ * read fresh from that home rather than from the process memo.
488
+ *
489
+ * @param sealed - Bytes from {@link sealCredentials}.
490
+ * @param passphrase - The passphrase used to seal.
491
+ * @param targets - Restored stores to write into.
492
+ * @returns What was restored and what must be re-entered (with commands).
493
+ * @throws {CredentialTransferError} `E_CREDENTIAL_PASSPHRASE` (wrong
494
+ * passphrase or corrupted payload) or `E_CREDENTIAL_PAYLOAD` — nothing written.
495
+ * @task T12326
496
+ */
497
+ export async function unsealCredentials(sealed, passphrase, targets) {
498
+ let opened;
499
+ try {
500
+ opened = decryptBundle(Buffer.from(sealed), passphrase);
501
+ }
502
+ catch {
503
+ throw new CredentialTransferError('E_CREDENTIAL_PASSPHRASE', 'Cannot open the sealed credentials: wrong passphrase, or the payload is corrupted. ' +
504
+ 'Nothing was written. Re-run the import with the passphrase used for the export.');
505
+ }
506
+ let payload;
507
+ try {
508
+ payload = parsePayload(opened);
509
+ }
510
+ finally {
511
+ opened.fill(0);
512
+ }
513
+ // Phase 1 — encrypt everything under the local keys (no writes yet).
514
+ const writes = [];
515
+ const reentry = [...payload.reentry];
516
+ for (const entry of payload.entries) {
517
+ const descriptor = {
518
+ store: entry.store,
519
+ id: entry.id,
520
+ label: entry.label,
521
+ };
522
+ switch (entry.store) {
523
+ case 'project-agent': {
524
+ const { projectDbPath, projectId } = targets;
525
+ if (projectDbPath === undefined || projectId === undefined || projectId === '') {
526
+ reentry.push(reentryFor(descriptor, 'no project store to restore into'));
527
+ break;
528
+ }
529
+ const ciphertext = await encryptProjectSecret(entry.secret, projectId, {
530
+ cleoHome: targets.cleoHome,
531
+ });
532
+ writes.push({
533
+ entry,
534
+ apply: async () => updateCell(projectDbPath, 'tasks_agent_credentials', ['agent_id', 'api_key_encrypted'], 'UPDATE tasks_agent_credentials SET api_key_encrypted = ? WHERE agent_id = ?', [ciphertext, entry.id]),
535
+ });
536
+ break;
537
+ }
538
+ case 'service-connection': {
539
+ const { globalDbPath } = targets;
540
+ if (globalDbPath === undefined) {
541
+ reentry.push(reentryFor(descriptor, 'no global store to restore into'));
542
+ break;
543
+ }
544
+ const ciphertext = await encryptGlobal(entry.secret, `service:${entry.id}`, {
545
+ cleoHome: targets.cleoHome,
546
+ });
547
+ const { provider, label } = splitProviderLabel(entry.id);
548
+ writes.push({
549
+ entry,
550
+ apply: async () => updateCell(globalDbPath, 'service_connections', ['provider', 'label', 'credentials_enc'], 'UPDATE service_connections SET credentials_enc = ? WHERE provider = ? AND label = ?', [ciphertext, provider, label]),
551
+ });
552
+ break;
553
+ }
554
+ case 'agent-registry': {
555
+ const { globalDbPath } = targets;
556
+ if (globalDbPath === undefined) {
557
+ reentry.push(reentryFor(descriptor, 'no global store to restore into'));
558
+ break;
559
+ }
560
+ const stored = await sealAgentApiKey(entry.secret, entry.id, {
561
+ cleoHome: targets.cleoHome,
562
+ });
563
+ writes.push({
564
+ entry,
565
+ apply: async () => updateCell(globalDbPath, 'agent_registry_agents', ['agent_id', 'api_key_encrypted', 'requires_reauth'], 'UPDATE agent_registry_agents SET api_key_encrypted = ?, requires_reauth = 0 WHERE agent_id = ?', [stored ?? '', entry.id]),
566
+ });
567
+ break;
568
+ }
569
+ case 'llm-pool': {
570
+ if (targets.restoreLlmPool === false) {
571
+ reentry.push(reentryFor(descriptor, 'LLM pool restore was not requested'));
572
+ break;
573
+ }
574
+ if (targets.cleoHome !== undefined &&
575
+ canonicalPath(targets.cleoHome) !== canonicalPath(getCleoHome())) {
576
+ reentry.push(reentryFor(descriptor, 'the LLM pool of a non-active CLEO home cannot be written'));
577
+ break;
578
+ }
579
+ let credential;
580
+ try {
581
+ credential = JSON.parse(entry.secret);
582
+ }
583
+ catch {
584
+ credential = undefined;
585
+ }
586
+ if (!isStoredCredential(credential)) {
587
+ reentry.push(reentryFor(descriptor, 'sealed LLM entry is malformed'));
588
+ break;
589
+ }
590
+ const stored = credential;
591
+ writes.push({
592
+ entry,
593
+ apply: async () => {
594
+ await addCredential(stored);
595
+ return true;
596
+ },
597
+ });
598
+ break;
599
+ }
600
+ }
601
+ }
602
+ // Phase 2 — write.
603
+ const restored = [];
604
+ for (const { entry, apply } of writes) {
605
+ const descriptor = {
606
+ store: entry.store,
607
+ id: entry.id,
608
+ label: entry.label,
609
+ };
610
+ if (await apply())
611
+ restored.push(descriptor);
612
+ else
613
+ reentry.push(reentryFor(descriptor, 'the restored store has no row for it'));
614
+ }
615
+ return { restored, reentry };
616
+ }
617
+ // ---------------------------------------------------------------------------
618
+ // Project KDF migration (path-bound → project identity)
619
+ // ---------------------------------------------------------------------------
620
+ /**
621
+ * Re-encrypt project credentials from the legacy path-bound KDF to the
622
+ * project-identity KDF, in place.
623
+ *
624
+ * Each ciphertext is opened with {@link decryptProjectSecret}; a legacy one is
625
+ * rewritten with the re-wrapped ciphertext it returns. Current ciphertexts are
626
+ * left alone. A credential no candidate key opens is left UNTOUCHED (never
627
+ * deleted) and reported with its re-entry command — typically because it was
628
+ * encrypted at a path not in `legacyProjectPaths`, or on another device.
629
+ * Idempotent: a second run reports everything as `current`. With `dryRun`
630
+ * nothing is written and `migrated` lists what WOULD be migrated.
631
+ *
632
+ * @param sources - `projectDbPath`, `projectId`, and `legacyProjectPaths`.
633
+ * @param options - `dryRun` to report without writing.
634
+ * @returns Migrated, already-current, and unrecoverable credentials.
635
+ * @throws {CredentialTransferError} `E_CREDENTIAL_PROJECT_ID` when rows exist
636
+ * and `projectId` is missing.
637
+ * @task T12326
638
+ */
639
+ export async function migrateProjectCredentials(sources, options = {}) {
640
+ const rows = readProjectAgentRows(sources.projectDbPath);
641
+ const migrated = [];
642
+ const current = [];
643
+ const reentry = [];
644
+ if (rows.length === 0 || sources.projectDbPath === undefined) {
645
+ return { migrated, current, reentry };
646
+ }
647
+ const projectId = requireProjectId(sources.projectId, 'migrate');
648
+ const dbPath = sources.projectDbPath;
649
+ for (const row of rows) {
650
+ let result;
651
+ try {
652
+ result = await decryptProjectSecret(row.material, {
653
+ projectId,
654
+ legacyProjectPaths: sources.legacyProjectPaths,
655
+ cleoHome: sources.cleoHome,
656
+ });
657
+ }
658
+ catch (err) {
659
+ reentry.push(reentryFor(row.descriptor, err instanceof Error ? err.message : String(err)));
660
+ continue;
661
+ }
662
+ if (result.rewrapped === null) {
663
+ current.push(row.descriptor);
664
+ continue;
665
+ }
666
+ if (options.dryRun === true) {
667
+ migrated.push(row.descriptor);
668
+ continue;
669
+ }
670
+ // Compare-and-swap on the old ciphertext: a concurrent writer wins.
671
+ const swapped = updateCell(dbPath, 'tasks_agent_credentials', ['agent_id', 'api_key_encrypted'], 'UPDATE tasks_agent_credentials SET api_key_encrypted = ? WHERE agent_id = ? AND api_key_encrypted = ?', [result.rewrapped, row.descriptor.id, row.material]);
672
+ (swapped ? migrated : current).push(row.descriptor);
673
+ }
674
+ return { migrated, current, reentry };
675
+ }
676
+ /**
677
+ * Migrate a project's stored credentials off the path-bound KDF, resolving
678
+ * everything from the project root: the store is `<root>/.cleo/cleo.db`, the
679
+ * identity is `projectId` from `.cleo/project-info.json`, and the legacy
680
+ * candidates are the root as given and its real path.
681
+ *
682
+ * This is the trigger used by `cleo upgrade` and `cleo doctor credentials`.
683
+ * It is idempotent and never deletes; unrecoverable rows come back in
684
+ * `reentry` with the command that re-enters each one. A project with no
685
+ * credential rows returns empty lists without opening anything for write.
686
+ *
687
+ * @param projectRoot - Absolute project root.
688
+ * @param options - `dryRun` to report without writing; `cleoHome` to key with another home.
689
+ * @returns What was (or would be) migrated, what is current, and what must be re-entered.
690
+ * @throws {CredentialTransferError} `E_CREDENTIAL_PROJECT_ID` when credential
691
+ * rows exist but project-info.json has no `projectId`.
692
+ * @task T12326
693
+ */
694
+ export async function migrateProjectCredentialsAtRoot(projectRoot, options = {}) {
695
+ const root = path.resolve(projectRoot);
696
+ const cleoDir = path.join(root, '.cleo');
697
+ const projectDbPath = path.join(cleoDir, 'cleo.db');
698
+ let projectId = null;
699
+ try {
700
+ const parsed = JSON.parse(fs.readFileSync(path.join(cleoDir, 'project-info.json'), 'utf-8'));
701
+ if (typeof parsed === 'object' &&
702
+ parsed !== null &&
703
+ 'projectId' in parsed &&
704
+ typeof parsed.projectId === 'string' &&
705
+ parsed.projectId.length > 0) {
706
+ projectId = parsed.projectId;
707
+ }
708
+ }
709
+ catch {
710
+ projectId = null;
711
+ }
712
+ const result = await migrateProjectCredentials({
713
+ projectDbPath,
714
+ ...(projectId !== null ? { projectId } : {}),
715
+ legacyProjectPaths: [root, canonicalPath(root)],
716
+ ...(options.cleoHome !== undefined ? { cleoHome: options.cleoHome } : {}),
717
+ }, { dryRun: options.dryRun === true });
718
+ return { ...result, projectDbPath, projectId, dryRun: options.dryRun === true };
719
+ }
720
+ /**
721
+ * Find agents whose stored API key cannot be recovered and, unless `dryRun`,
722
+ * flag them `requires_reauth = 1` (T12352).
723
+ *
724
+ * Before T12352 `api_key_encrypted` held a derived HMAC instead of the key.
725
+ * The real key was discarded, so those rows cannot be migrated, only flagged
726
+ * and listed with the command that re-registers the key. Never modifies the
727
+ * stored value; idempotent.
728
+ *
729
+ * @param globalDbPath - Global `cleo.db`.
730
+ * @param options - `dryRun` to report without flagging; `cleoHome` for the keys.
731
+ * @returns Recoverable agents, agents to re-register, and how many were flagged.
732
+ * @task T12352
733
+ */
734
+ export async function auditAgentRegistryKeys(globalDbPath, options = {}) {
735
+ const current = [];
736
+ const reentry = [];
737
+ const unrecoverable = [];
738
+ for (const row of readAgentRegistryRows(globalDbPath)) {
739
+ const key = await openAgentApiKey(row.material, row.descriptor.id, {
740
+ ...(options.cleoHome !== undefined ? { cleoHome: options.cleoHome } : {}),
741
+ });
742
+ if (key.requiresReauth) {
743
+ reentry.push(reentryFor(row.descriptor, key.reason ?? 'the stored key is not recoverable'));
744
+ unrecoverable.push(row.descriptor.id);
745
+ }
746
+ else {
747
+ current.push(row.descriptor);
748
+ }
749
+ }
750
+ let flagged = 0;
751
+ if (options.dryRun !== true && unrecoverable.length > 0) {
752
+ flagged = withDb(globalDbPath, 0, (db) => {
753
+ if (!hasTable(db, 'agent_registry_agents', ['agent_id', 'requires_reauth']))
754
+ return 0;
755
+ const stmt = db.prepare('UPDATE agent_registry_agents SET requires_reauth = 1 WHERE agent_id = ? AND requires_reauth = 0');
756
+ let n = 0;
757
+ for (const id of unrecoverable)
758
+ n += Number(stmt.run(id).changes);
759
+ return n;
760
+ });
761
+ }
762
+ return { globalDbPath, current, reentry, flagged, dryRun: options.dryRun === true };
763
+ }
764
+ /**
765
+ * The live LLM pool path, re-exported so a bundle writer need not import the
766
+ * LLM module to fill {@link CredentialSources.llmStorePath}.
767
+ *
768
+ * @returns `<cleoHome>/llm-credentials.json`.
769
+ * @task T12326
770
+ */
771
+ export function liveLlmCredentialStorePath() {
772
+ return credentialsStorePath();
773
+ }
774
+ //# sourceMappingURL=credential-transfer.js.map