@phnx-labs/agents-cli 1.20.72 → 1.20.74

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 (234) hide show
  1. package/CHANGELOG.md +395 -0
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +12 -0
  5. package/dist/commands/activity.js +68 -0
  6. package/dist/commands/apply.d.ts +13 -0
  7. package/dist/commands/apply.js +28 -3
  8. package/dist/commands/browser.js +1 -1
  9. package/dist/commands/cloud.js +21 -11
  10. package/dist/commands/doctor.js +21 -5
  11. package/dist/commands/events.d.ts +11 -10
  12. package/dist/commands/events.js +30 -21
  13. package/dist/commands/exec.d.ts +34 -0
  14. package/dist/commands/exec.js +337 -39
  15. package/dist/commands/export.d.ts +11 -0
  16. package/dist/commands/export.js +215 -0
  17. package/dist/commands/feed.d.ts +13 -0
  18. package/dist/commands/feed.js +130 -29
  19. package/dist/commands/hq.d.ts +2 -0
  20. package/dist/commands/hq.js +58 -0
  21. package/dist/commands/import.js +51 -3
  22. package/dist/commands/lease.d.ts +19 -0
  23. package/dist/commands/lease.js +138 -11
  24. package/dist/commands/login.d.ts +2 -0
  25. package/dist/commands/login.js +123 -0
  26. package/dist/commands/logs.js +1 -1
  27. package/dist/commands/mine.d.ts +27 -0
  28. package/dist/commands/mine.js +207 -0
  29. package/dist/commands/monitors.js +79 -49
  30. package/dist/commands/plugins.js +1 -0
  31. package/dist/commands/profiles.js +158 -0
  32. package/dist/commands/repo.js +2 -60
  33. package/dist/commands/resources.d.ts +5 -0
  34. package/dist/commands/resources.js +95 -0
  35. package/dist/commands/routines.d.ts +2 -0
  36. package/dist/commands/routines.js +139 -37
  37. package/dist/commands/secrets.js +234 -47
  38. package/dist/commands/send.d.ts +15 -0
  39. package/dist/commands/send.js +62 -0
  40. package/dist/commands/sessions-tail.d.ts +1 -0
  41. package/dist/commands/sessions-tail.js +20 -9
  42. package/dist/commands/sessions.js +18 -3
  43. package/dist/commands/setup-mine.d.ts +18 -0
  44. package/dist/commands/setup-mine.js +106 -0
  45. package/dist/commands/setup-share.js +2 -2
  46. package/dist/commands/setup.js +9 -2
  47. package/dist/commands/share.d.ts +8 -1
  48. package/dist/commands/share.js +122 -37
  49. package/dist/commands/ssh.d.ts +9 -0
  50. package/dist/commands/ssh.js +196 -26
  51. package/dist/commands/sync.js +1 -1
  52. package/dist/commands/teams.js +11 -2
  53. package/dist/commands/versions.js +36 -5
  54. package/dist/commands/view.js +75 -27
  55. package/dist/index.js +80 -10
  56. package/dist/lib/activity.d.ts +124 -0
  57. package/dist/lib/activity.js +542 -0
  58. package/dist/lib/agents.d.ts +35 -0
  59. package/dist/lib/agents.js +79 -6
  60. package/dist/lib/artifact-actions.d.ts +1 -1
  61. package/dist/lib/artifact-actions.js +1 -1
  62. package/dist/lib/ask-classifier.d.ts +2 -1
  63. package/dist/lib/ask-classifier.js +3 -3
  64. package/dist/lib/auth-health.d.ts +32 -4
  65. package/dist/lib/auth-health.js +40 -1
  66. package/dist/lib/auto-pull.js +8 -1
  67. package/dist/lib/brand.d.ts +40 -0
  68. package/dist/lib/brand.js +122 -0
  69. package/dist/lib/browser/drivers/ssh.js +4 -7
  70. package/dist/lib/browser/service.js +6 -8
  71. package/dist/lib/channels/providers/index.d.ts +2 -0
  72. package/dist/lib/channels/providers/index.js +20 -0
  73. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  74. package/dist/lib/channels/providers/mailbox.js +26 -0
  75. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  76. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  77. package/dist/lib/channels/providers/rush.d.ts +6 -0
  78. package/dist/lib/channels/providers/rush.js +55 -0
  79. package/dist/lib/channels/registry.d.ts +42 -0
  80. package/dist/lib/channels/registry.js +20 -0
  81. package/dist/lib/channels/resolve.d.ts +11 -0
  82. package/dist/lib/channels/resolve.js +12 -0
  83. package/dist/lib/cli-resources.d.ts +18 -0
  84. package/dist/lib/cli-resources.js +53 -4
  85. package/dist/lib/cloud/codex.js +6 -3
  86. package/dist/lib/cloud/factory.d.ts +1 -0
  87. package/dist/lib/cloud/factory.js +10 -4
  88. package/dist/lib/cloud/rush.d.ts +3 -1
  89. package/dist/lib/cloud/rush.js +6 -1
  90. package/dist/lib/cloud/types.d.ts +2 -0
  91. package/dist/lib/codex-home.d.ts +35 -0
  92. package/dist/lib/codex-home.js +136 -0
  93. package/dist/lib/config-transfer.d.ts +16 -0
  94. package/dist/lib/config-transfer.js +56 -0
  95. package/dist/lib/crabbox/cli.d.ts +44 -0
  96. package/dist/lib/crabbox/cli.js +120 -20
  97. package/dist/lib/crabbox/lease.d.ts +52 -1
  98. package/dist/lib/crabbox/lease.js +117 -16
  99. package/dist/lib/crabbox/progress.d.ts +31 -0
  100. package/dist/lib/crabbox/progress.js +76 -0
  101. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  102. package/dist/lib/crabbox/runtimes.js +33 -8
  103. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  104. package/dist/lib/crabbox/setup-copy.js +127 -0
  105. package/dist/lib/daemon.d.ts +5 -5
  106. package/dist/lib/daemon.js +9 -9
  107. package/dist/lib/drive-sync.js +6 -3
  108. package/dist/lib/event-stream.d.ts +36 -0
  109. package/dist/lib/event-stream.js +68 -0
  110. package/dist/lib/events.d.ts +1 -1
  111. package/dist/lib/exec.d.ts +11 -0
  112. package/dist/lib/exec.js +23 -2
  113. package/dist/lib/export.d.ts +72 -0
  114. package/dist/lib/export.js +269 -0
  115. package/dist/lib/feed-policy.d.ts +1 -1
  116. package/dist/lib/feed-policy.js +14 -9
  117. package/dist/lib/feed-ranking.d.ts +32 -0
  118. package/dist/lib/feed-ranking.js +224 -0
  119. package/dist/lib/feed.d.ts +35 -2
  120. package/dist/lib/feed.js +66 -3
  121. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  122. package/dist/lib/fleet/auth-sync.js +92 -0
  123. package/dist/lib/fleet/remote-login.d.ts +187 -0
  124. package/dist/lib/fleet/remote-login.js +556 -0
  125. package/dist/lib/format.d.ts +30 -3
  126. package/dist/lib/format.js +34 -5
  127. package/dist/lib/fs-atomic.d.ts +7 -2
  128. package/dist/lib/fs-atomic.js +8 -12
  129. package/dist/lib/git.d.ts +16 -0
  130. package/dist/lib/git.js +79 -2
  131. package/dist/lib/heal.js +11 -3
  132. package/dist/lib/hosts/progress.d.ts +28 -10
  133. package/dist/lib/hosts/progress.js +79 -22
  134. package/dist/lib/hosts/remote-cmd.js +4 -0
  135. package/dist/lib/hq/floor.d.ts +87 -0
  136. package/dist/lib/hq/floor.js +226 -0
  137. package/dist/lib/import.d.ts +23 -0
  138. package/dist/lib/import.js +74 -2
  139. package/dist/lib/isolation-boundary-report.d.ts +10 -0
  140. package/dist/lib/isolation-boundary-report.js +35 -0
  141. package/dist/lib/menubar/install-menubar.js +14 -20
  142. package/dist/lib/notify.d.ts +1 -0
  143. package/dist/lib/notify.js +3 -3
  144. package/dist/lib/open-url.d.ts +2 -0
  145. package/dist/lib/open-url.js +19 -0
  146. package/dist/lib/openclaw-keychain.d.ts +56 -0
  147. package/dist/lib/openclaw-keychain.js +236 -0
  148. package/dist/lib/overdue.js +10 -0
  149. package/dist/lib/permissions.d.ts +44 -1
  150. package/dist/lib/permissions.js +284 -7
  151. package/dist/lib/project-launch.d.ts +6 -12
  152. package/dist/lib/project-launch.js +13 -228
  153. package/dist/lib/project-resources.d.ts +7 -0
  154. package/dist/lib/project-resources.js +306 -0
  155. package/dist/lib/pty-client.d.ts +27 -0
  156. package/dist/lib/pty-client.js +136 -10
  157. package/dist/lib/refresh.js +14 -10
  158. package/dist/lib/resource-profiles.d.ts +26 -0
  159. package/dist/lib/resource-profiles.js +157 -0
  160. package/dist/lib/resources/permissions.js +7 -1
  161. package/dist/lib/resources/types.d.ts +1 -1
  162. package/dist/lib/resources.d.ts +1 -1
  163. package/dist/lib/resources.js +32 -3
  164. package/dist/lib/routines.d.ts +11 -0
  165. package/dist/lib/routines.js +56 -15
  166. package/dist/lib/runner.d.ts +1 -0
  167. package/dist/lib/runner.js +66 -5
  168. package/dist/lib/secrets/bundles.d.ts +16 -3
  169. package/dist/lib/secrets/bundles.js +206 -37
  170. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  171. package/dist/lib/secrets/icloud-import.js +9 -6
  172. package/dist/lib/secrets/mcp.js +1 -1
  173. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  174. package/dist/lib/secrets/vault-age-helper.js +34 -0
  175. package/dist/lib/secrets/vault.d.ts +49 -0
  176. package/dist/lib/secrets/vault.js +397 -0
  177. package/dist/lib/self-heal/checks/path.js +3 -1
  178. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  179. package/dist/lib/self-heal/checks/shims.js +19 -11
  180. package/dist/lib/session/cloud.d.ts +2 -2
  181. package/dist/lib/session/cloud.js +2 -2
  182. package/dist/lib/session/db.d.ts +4 -0
  183. package/dist/lib/session/db.js +44 -7
  184. package/dist/lib/session/discover.d.ts +2 -0
  185. package/dist/lib/session/discover.js +114 -3
  186. package/dist/lib/session/stream-render.d.ts +3 -0
  187. package/dist/lib/session/stream-render.js +130 -0
  188. package/dist/lib/session/types.d.ts +6 -0
  189. package/dist/lib/share/analytics.d.ts +13 -0
  190. package/dist/lib/share/analytics.js +45 -0
  191. package/dist/lib/share/config.d.ts +19 -5
  192. package/dist/lib/share/config.js +44 -8
  193. package/dist/lib/share/provision.d.ts +58 -6
  194. package/dist/lib/share/provision.js +97 -22
  195. package/dist/lib/share/publish.d.ts +36 -13
  196. package/dist/lib/share/publish.js +55 -8
  197. package/dist/lib/share/worker-template.js +83 -17
  198. package/dist/lib/shims.d.ts +35 -0
  199. package/dist/lib/shims.js +159 -13
  200. package/dist/lib/ssh-exec.d.ts +14 -0
  201. package/dist/lib/ssh-exec.js +57 -0
  202. package/dist/lib/staleness/detectors/hooks.js +25 -1
  203. package/dist/lib/staleness/detectors/permissions.js +66 -0
  204. package/dist/lib/staleness/detectors/workflows.js +20 -0
  205. package/dist/lib/staleness/writers/hooks.js +58 -4
  206. package/dist/lib/staleness/writers/permissions.js +2 -2
  207. package/dist/lib/staleness/writers/skills.js +1 -1
  208. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  209. package/dist/lib/staleness/writers/sources.js +68 -1
  210. package/dist/lib/star-nudge.d.ts +45 -0
  211. package/dist/lib/star-nudge.js +91 -0
  212. package/dist/lib/startup/command-registry.d.ts +7 -1
  213. package/dist/lib/startup/command-registry.js +19 -3
  214. package/dist/lib/state.d.ts +2 -0
  215. package/dist/lib/state.js +17 -4
  216. package/dist/lib/subagents-registry.d.ts +2 -0
  217. package/dist/lib/subagents-registry.js +3 -0
  218. package/dist/lib/teams/parsers.js +214 -6
  219. package/dist/lib/teams/supervisor.d.ts +7 -0
  220. package/dist/lib/teams/supervisor.js +2 -1
  221. package/dist/lib/template.d.ts +1 -1
  222. package/dist/lib/template.js +1 -1
  223. package/dist/lib/triggers/webhook.js +36 -3
  224. package/dist/lib/types.d.ts +84 -0
  225. package/dist/lib/uninstall.js +1 -45
  226. package/dist/lib/version.d.ts +40 -0
  227. package/dist/lib/version.js +94 -16
  228. package/dist/lib/versions.d.ts +24 -0
  229. package/dist/lib/versions.js +269 -78
  230. package/dist/lib/workflows.d.ts +2 -0
  231. package/dist/lib/workflows.js +88 -0
  232. package/package.json +2 -1
  233. package/dist/commands/daemon.d.ts +0 -10
  234. package/dist/commands/daemon.js +0 -121
@@ -0,0 +1,397 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { spawnSync } from 'child_process';
4
+ import { armor } from 'age-encryption';
5
+ import { getUserAgentsDir } from '../state.js';
6
+ import { getCliLaunch } from '../cli-entry.js';
7
+ import { atomicWriteFileSync, withFileLock } from '../fs-atomic.js';
8
+ import { deleteKeychainToken, getKeychainToken, setKeychainToken, } from './index.js';
9
+ const VAULT_FILE_NAME = 'vault.age';
10
+ const VAULT_SESSION_ITEM = 'agents-cli.vault.session';
11
+ export const VAULT_SESSION_TTL_MS = 8 * 60 * 60 * 1000;
12
+ export const VAULT_SCRYPT_WORK_FACTOR = 18;
13
+ const VAULT_LOCK_STALE_MS = 120_000;
14
+ const VAULT_LOCK_ACQUIRE_TIMEOUT_MS = 180_000;
15
+ let overrideVaultPath = null;
16
+ let vaultDataCache = null;
17
+ let ageOperationCountForTest = 0;
18
+ let ageHelperLaunchForTest = null;
19
+ export function vaultPath() {
20
+ return overrideVaultPath ?? process.env.AGENTS_VAULT_PATH ?? path.join(getUserAgentsDir(), VAULT_FILE_NAME);
21
+ }
22
+ export function vaultExists() {
23
+ return fs.existsSync(vaultPath());
24
+ }
25
+ export function _setVaultPathForTest(filePath) {
26
+ overrideVaultPath = filePath;
27
+ clearVaultDataCache();
28
+ }
29
+ export function _clearVaultDataCacheForTest() {
30
+ clearVaultDataCache();
31
+ }
32
+ export function _resetVaultAgeOperationCountForTest() {
33
+ ageOperationCountForTest = 0;
34
+ }
35
+ export function _getVaultAgeOperationCountForTest() {
36
+ return ageOperationCountForTest;
37
+ }
38
+ export function _setVaultAgeHelperLaunchForTest(launch) {
39
+ ageHelperLaunchForTest = launch;
40
+ }
41
+ function emptyVault() {
42
+ return { v: 1, bundles: {} };
43
+ }
44
+ function parseVault(raw) {
45
+ let parsed;
46
+ try {
47
+ parsed = JSON.parse(raw);
48
+ }
49
+ catch {
50
+ throw new Error('Vault content is malformed.');
51
+ }
52
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
53
+ throw new Error('Vault content has an invalid shape.');
54
+ }
55
+ const obj = parsed;
56
+ if (obj.v !== 1 || !obj.bundles || typeof obj.bundles !== 'object' || Array.isArray(obj.bundles)) {
57
+ throw new Error('Vault content has an invalid shape.');
58
+ }
59
+ const bundles = {};
60
+ for (const [name, record] of Object.entries(obj.bundles)) {
61
+ if (!record || typeof record !== 'object' || Array.isArray(record)) {
62
+ throw new Error(`Vault bundle '${name}' has an invalid shape.`);
63
+ }
64
+ const rec = record;
65
+ if (rec.metadata !== undefined && typeof rec.metadata !== 'string') {
66
+ throw new Error(`Vault bundle '${name}' metadata is invalid.`);
67
+ }
68
+ if (!rec.keys || typeof rec.keys !== 'object' || Array.isArray(rec.keys)) {
69
+ throw new Error(`Vault bundle '${name}' keys are invalid.`);
70
+ }
71
+ const keys = {};
72
+ for (const [key, value] of Object.entries(rec.keys)) {
73
+ if (typeof value !== 'string')
74
+ throw new Error(`Vault key '${name}.${key}' is invalid.`);
75
+ keys[key] = value;
76
+ }
77
+ bundles[name] = { metadata: rec.metadata, keys };
78
+ }
79
+ return { v: 1, bundles };
80
+ }
81
+ function runAgeSync(input) {
82
+ ageOperationCountForTest++;
83
+ const launch = ageHelperLaunchForTest ?? getCliLaunch(['__vault-age-helper']);
84
+ const child = spawnSync(launch.command, launch.args, {
85
+ input: JSON.stringify(input),
86
+ encoding: 'utf-8',
87
+ stdio: ['pipe', 'pipe', 'pipe'],
88
+ });
89
+ if (child.status !== 0) {
90
+ const msg = child.stderr.trim();
91
+ throw new Error(msg || child.error?.message || 'age operation failed');
92
+ }
93
+ return child.stdout;
94
+ }
95
+ export function encrypt(key, json) {
96
+ return runAgeSync({
97
+ action: 'encrypt',
98
+ passphrase: key.passphrase,
99
+ plaintext: JSON.stringify(json),
100
+ scryptWorkFactor: VAULT_SCRYPT_WORK_FACTOR,
101
+ });
102
+ }
103
+ export function decrypt(key, blob) {
104
+ try {
105
+ // Validate armor in-process so malformed files fail before spawning.
106
+ armor.decode(blob);
107
+ const plaintext = runAgeSync({
108
+ action: 'decrypt',
109
+ passphrase: key.passphrase,
110
+ blob,
111
+ });
112
+ return parseVault(plaintext);
113
+ }
114
+ catch (err) {
115
+ throw new Error(`Failed to decrypt vault. Wrong password or tampered vault file. (${err.message})`);
116
+ }
117
+ }
118
+ function vaultFileStat(file) {
119
+ const stat = fs.statSync(file);
120
+ return { mtimeMs: stat.mtimeMs, size: stat.size };
121
+ }
122
+ function sameVaultFileStat(a, b) {
123
+ return a.mtimeMs === b.mtimeMs && a.size === b.size;
124
+ }
125
+ function clearVaultDataCache() {
126
+ vaultDataCache = null;
127
+ }
128
+ function cacheVaultData(file, key, data) {
129
+ vaultDataCache = {
130
+ file,
131
+ passphrase: key.passphrase,
132
+ stat: vaultFileStat(file),
133
+ data,
134
+ };
135
+ }
136
+ function vaultAlreadyExistsError(file) {
137
+ return new Error(`Vault file already exists: ${file}. Use --force to replace it.`);
138
+ }
139
+ export function createVault(password, opts = {}) {
140
+ if (!password)
141
+ throw new Error('Password is required.');
142
+ const file = vaultPath();
143
+ if (!opts.overwrite && fs.existsSync(file))
144
+ throw vaultAlreadyExistsError(file);
145
+ fs.mkdirSync(path.dirname(file), { recursive: true });
146
+ const key = { passphrase: password };
147
+ const data = emptyVault();
148
+ const blob = encrypt(key, data);
149
+ try {
150
+ fs.writeFileSync(file, blob, { mode: 0o600, flag: opts.overwrite ? 'w' : 'wx' });
151
+ }
152
+ catch (err) {
153
+ if (err.code === 'EEXIST')
154
+ throw vaultAlreadyExistsError(file);
155
+ throw err;
156
+ }
157
+ cacheVaultData(file, key, data);
158
+ cacheVaultKey(key);
159
+ return key;
160
+ }
161
+ export function joinVault(password, sourcePath, opts = {}) {
162
+ if (!password)
163
+ throw new Error('Password is required.');
164
+ const source = path.resolve(sourcePath);
165
+ const dest = vaultPath();
166
+ if (!opts.overwrite && fs.existsSync(dest))
167
+ throw vaultAlreadyExistsError(dest);
168
+ const blob = fs.readFileSync(source, 'utf-8');
169
+ const key = { passphrase: password };
170
+ const data = decrypt(key, blob);
171
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
172
+ try {
173
+ fs.copyFileSync(source, dest, opts.overwrite ? 0 : fs.constants.COPYFILE_EXCL);
174
+ }
175
+ catch (err) {
176
+ if (err.code === 'EEXIST')
177
+ throw vaultAlreadyExistsError(dest);
178
+ throw err;
179
+ }
180
+ fs.chmodSync(dest, 0o600);
181
+ cacheVaultData(dest, key, data);
182
+ cacheVaultKey(key);
183
+ return key;
184
+ }
185
+ export function unlock(password) {
186
+ if (!password)
187
+ throw new Error('Password is required.');
188
+ const file = vaultPath();
189
+ if (!fs.existsSync(file))
190
+ throw new Error(`Vault file not found: ${file}`);
191
+ const key = { passphrase: password };
192
+ const data = decrypt(key, fs.readFileSync(file, 'utf-8'));
193
+ cacheVaultData(file, key, data);
194
+ cacheVaultKey(key);
195
+ return key;
196
+ }
197
+ export function cacheVaultKey(key, ttlMs = VAULT_SESSION_TTL_MS) {
198
+ const session = {
199
+ v: 1,
200
+ passphrase: key.passphrase,
201
+ expiresAt: Date.now() + ttlMs,
202
+ };
203
+ setKeychainToken(VAULT_SESSION_ITEM, JSON.stringify(session), { noAcl: true });
204
+ }
205
+ export function clearVaultKey() {
206
+ clearVaultDataCache();
207
+ deleteKeychainToken(VAULT_SESSION_ITEM);
208
+ }
209
+ export function getVaultSession() {
210
+ let raw;
211
+ try {
212
+ raw = getKeychainToken(VAULT_SESSION_ITEM);
213
+ }
214
+ catch {
215
+ return { loggedIn: false };
216
+ }
217
+ let parsed;
218
+ try {
219
+ parsed = JSON.parse(raw);
220
+ }
221
+ catch {
222
+ clearVaultKey();
223
+ return { loggedIn: false };
224
+ }
225
+ if (parsed.v !== 1 || typeof parsed.passphrase !== 'string' || typeof parsed.expiresAt !== 'number') {
226
+ clearVaultKey();
227
+ return { loggedIn: false };
228
+ }
229
+ if (parsed.expiresAt <= Date.now()) {
230
+ clearVaultKey();
231
+ return { loggedIn: false, expiresAt: parsed.expiresAt };
232
+ }
233
+ return { loggedIn: true, key: { passphrase: parsed.passphrase }, expiresAt: parsed.expiresAt };
234
+ }
235
+ function requireVaultKey() {
236
+ const session = getVaultSession();
237
+ if (!session.loggedIn) {
238
+ throw new Error('Not logged in. Run: agents login');
239
+ }
240
+ return session.key;
241
+ }
242
+ function parseVaultItem(item) {
243
+ const metaPrefix = 'agents-cli.bundles.';
244
+ const keyPrefix = 'agents-cli.secrets.';
245
+ if (item.startsWith(metaPrefix)) {
246
+ return { kind: 'meta', bundle: item.slice(metaPrefix.length) };
247
+ }
248
+ if (item.startsWith(keyPrefix)) {
249
+ const rest = item.slice(keyPrefix.length);
250
+ const dot = rest.lastIndexOf('.');
251
+ if (dot <= 0 || dot >= rest.length - 1)
252
+ return null;
253
+ return { kind: 'key', bundle: rest.slice(0, dot), key: rest.slice(dot + 1) };
254
+ }
255
+ return null;
256
+ }
257
+ function vaultMissingError(file) {
258
+ return new Error(`Vault file is missing: ${file}. A vault was created but the file is gone` +
259
+ ' (a sync conflict, an undownloaded placeholder, or an accidental move/delete).' +
260
+ ' Restore it before writing, or run: agents login --create --force to start over.');
261
+ }
262
+ function readVaultData() {
263
+ const file = vaultPath();
264
+ if (!fs.existsSync(file)) {
265
+ // A live session means a vault was created/joined/unlocked, so the file
266
+ // must exist. If it is gone, refuse rather than silently starting from an
267
+ // empty vault (which a subsequent write would persist, destroying data).
268
+ if (getVaultSession().loggedIn)
269
+ throw vaultMissingError(file);
270
+ return emptyVault();
271
+ }
272
+ const key = requireVaultKey();
273
+ const stat = vaultFileStat(file);
274
+ if (vaultDataCache &&
275
+ vaultDataCache.file === file &&
276
+ vaultDataCache.passphrase === key.passphrase &&
277
+ sameVaultFileStat(vaultDataCache.stat, stat)) {
278
+ return vaultDataCache.data;
279
+ }
280
+ const data = decrypt(key, fs.readFileSync(file, 'utf-8'));
281
+ vaultDataCache = { file, passphrase: key.passphrase, stat, data };
282
+ return data;
283
+ }
284
+ function writeVaultData(data) {
285
+ const file = vaultPath();
286
+ const key = requireVaultKey();
287
+ fs.mkdirSync(path.dirname(file), { recursive: true });
288
+ atomicWriteFileSync(file, encrypt(key, data), { encoding: 'utf-8', mode: 0o600 });
289
+ cacheVaultData(file, key, data);
290
+ }
291
+ function withVaultMutation(mutate) {
292
+ const file = vaultPath();
293
+ requireVaultKey();
294
+ if (!fs.existsSync(file))
295
+ throw vaultMissingError(file);
296
+ return withFileLock(file, () => {
297
+ const data = readVaultData();
298
+ const { changed, result } = mutate(data);
299
+ if (changed)
300
+ writeVaultData(data);
301
+ return result;
302
+ }, { staleMs: VAULT_LOCK_STALE_MS, acquireTimeoutMs: VAULT_LOCK_ACQUIRE_TIMEOUT_MS });
303
+ }
304
+ export function vaultHasItem(item) {
305
+ const parsed = parseVaultItem(item);
306
+ if (!parsed)
307
+ return false;
308
+ const data = readVaultData();
309
+ const bundle = data.bundles[parsed.bundle];
310
+ if (!bundle)
311
+ return false;
312
+ return parsed.kind === 'meta' ? bundle.metadata !== undefined : parsed.key in bundle.keys;
313
+ }
314
+ export function vaultGetItem(item) {
315
+ const parsed = parseVaultItem(item);
316
+ if (!parsed)
317
+ throw new Error(`Vault item '${item}' is invalid.`);
318
+ const data = readVaultData();
319
+ const bundle = data.bundles[parsed.bundle];
320
+ const value = parsed.kind === 'meta' ? bundle?.metadata : bundle?.keys[parsed.key];
321
+ if (value === undefined)
322
+ throw new Error(`Vault item '${item}' not found.`);
323
+ return value;
324
+ }
325
+ export function vaultGetItems(items) {
326
+ const data = readVaultData();
327
+ const out = new Map();
328
+ for (const item of items) {
329
+ const parsed = parseVaultItem(item);
330
+ if (!parsed)
331
+ continue;
332
+ const bundle = data.bundles[parsed.bundle];
333
+ const value = parsed.kind === 'meta' ? bundle?.metadata : bundle?.keys[parsed.key];
334
+ if (value !== undefined)
335
+ out.set(item, value);
336
+ }
337
+ return out;
338
+ }
339
+ export function vaultSetItem(item, value) {
340
+ vaultSetItems(new Map([[item, value]]));
341
+ }
342
+ export function vaultSetItems(items) {
343
+ if (items.size === 0)
344
+ return;
345
+ withVaultMutation((data) => {
346
+ for (const [item, value] of items) {
347
+ const parsed = parseVaultItem(item);
348
+ if (!parsed)
349
+ throw new Error(`Vault item '${item}' is invalid.`);
350
+ const bundle = data.bundles[parsed.bundle] ?? { keys: {} };
351
+ if (parsed.kind === 'meta')
352
+ bundle.metadata = value;
353
+ else
354
+ bundle.keys[parsed.key] = value;
355
+ data.bundles[parsed.bundle] = bundle;
356
+ }
357
+ return { changed: true, result: undefined };
358
+ });
359
+ }
360
+ export function vaultDeleteItem(item) {
361
+ const parsed = parseVaultItem(item);
362
+ if (!parsed)
363
+ return false;
364
+ return withVaultMutation((data) => {
365
+ const bundle = data.bundles[parsed.bundle];
366
+ if (!bundle)
367
+ return { changed: false, result: false };
368
+ let deleted = false;
369
+ if (parsed.kind === 'meta') {
370
+ deleted = bundle.metadata !== undefined;
371
+ delete bundle.metadata;
372
+ }
373
+ else {
374
+ deleted = parsed.key in bundle.keys;
375
+ delete bundle.keys[parsed.key];
376
+ }
377
+ if (bundle.metadata === undefined && Object.keys(bundle.keys).length === 0) {
378
+ delete data.bundles[parsed.bundle];
379
+ }
380
+ return { changed: deleted, result: deleted };
381
+ });
382
+ }
383
+ export function vaultListItems(prefix) {
384
+ const data = readVaultData();
385
+ const out = [];
386
+ for (const [bundleName, bundle] of Object.entries(data.bundles)) {
387
+ const meta = `agents-cli.bundles.${bundleName}`;
388
+ if (bundle.metadata !== undefined && meta.startsWith(prefix))
389
+ out.push(meta);
390
+ for (const key of Object.keys(bundle.keys)) {
391
+ const item = `agents-cli.secrets.${bundleName}.${key}`;
392
+ if (item.startsWith(prefix))
393
+ out.push(item);
394
+ }
395
+ }
396
+ return out.sort();
397
+ }
@@ -6,12 +6,14 @@
6
6
  // Caveat: an already-open shell won't pick up the new rc/PATH entry until it reloads
7
7
  // — but new shells will, so the recurring prompt stops.
8
8
  import { resultOf } from '../types.js';
9
- import { isShimsInPath, addShimsToPath } from '../../shims.js';
9
+ import { isShimsInPath, addShimsToPath, listAgentsWithNonIsolatedInstalledVersions } from '../../shims.js';
10
10
  export const pathCheck = {
11
11
  id: 'path',
12
12
  title: 'Shims directory on PATH',
13
13
  cadence: 'startup',
14
14
  async run(ctx) {
15
+ if (listAgentsWithNonIsolatedInstalledVersions().length === 0)
16
+ return resultOf([], []);
15
17
  if (isShimsInPath())
16
18
  return resultOf([], []);
17
19
  if (ctx.dryRun)
@@ -5,7 +5,7 @@
5
5
  // and PATH-order problem are POSIX; Windows resolves via the registry PATH).
6
6
  import { resultOf } from '../types.js';
7
7
  import { AGENTS } from '../../agents.js';
8
- import { getPathShadowingExecutable, adoptShadowingLauncher, listAgentsWithInstalledVersions, } from '../../shims.js';
8
+ import { getPathShadowingExecutable, adoptShadowingLauncher, listAgentsWithNonIsolatedInstalledVersions, } from '../../shims.js';
9
9
  import { getGlobalDefault } from '../../versions.js';
10
10
  export const shadowingCheck = {
11
11
  id: 'shadowing',
@@ -15,7 +15,15 @@ export const shadowingCheck = {
15
15
  async run(ctx) {
16
16
  const fixed = [];
17
17
  const needsAttention = [];
18
- for (const agent of listAgentsWithInstalledVersions()) {
18
+ // Isolated-only agents are skipped outright. Adoption repoints the user's OWN
19
+ // launcher (e.g. the npm symlink ~/.npm-global/bin/codex) at our shim — the most
20
+ // invasive thing self-heal does, and the exact opposite of what `--isolated`
21
+ // promises. The `getGlobalDefault` guard below already blocked this in practice,
22
+ // since an isolated install never sets a default; but that made the boundary
23
+ // depend on an invariant enforced elsewhere. Any path that pins a default from
24
+ // an isolated copy would silently re-arm the adoption. Gate on the installs
25
+ // themselves so it cannot happen regardless of how a default got recorded.
26
+ for (const agent of listAgentsWithNonIsolatedInstalledVersions()) {
19
27
  if (!getGlobalDefault(agent))
20
28
  continue; // only default agents, like the interactive flow
21
29
  const cmd = AGENTS[agent].cliCommand;
@@ -4,7 +4,7 @@
4
4
  // background so the shim schema settles without user-facing churn.
5
5
  import { resultOf } from '../types.js';
6
6
  import { AGENTS } from '../../agents.js';
7
- import { createShim, ensureShimCurrent, ensureVersionedAliasCurrent, isShimCurrent, isVersionedAliasCurrent, shimPointsAtLiveInstall, removeLegacyUserShim, listAgentsWithInstalledVersions, listShimFileNames, pruneOrphanedCommandShim, } from '../../shims.js';
7
+ import { createShim, ensureShimCurrent, ensureVersionedAliasCurrent, isShimCurrent, isVersionedAliasCurrent, removeShim, shimExists, shimPointsAtLiveInstall, removeLegacyUserShim, listAgentsWithInstalledVersions, listAgentsWithNonIsolatedInstalledVersions, listShimFileNames, pruneOrphanedCommandShim, } from '../../shims.js';
8
8
  import { listInstalledVersions } from '../../versions.js';
9
9
  export const shimsCheck = {
10
10
  id: 'shims',
@@ -12,20 +12,28 @@ export const shimsCheck = {
12
12
  cadence: 'frequent',
13
13
  async run(ctx) {
14
14
  const fixed = [];
15
+ const agentsWithBareShims = new Set(listAgentsWithNonIsolatedInstalledVersions());
15
16
  for (const agent of listAgentsWithInstalledVersions()) {
16
17
  const cmd = AGENTS[agent].cliCommand;
17
- if (!isShimCurrent(agent)) {
18
- if (!ctx.dryRun)
19
- ensureShimCurrent(agent);
20
- fixed.push(`${cmd} shim`);
18
+ if (agentsWithBareShims.has(agent)) {
19
+ if (!isShimCurrent(agent)) {
20
+ if (!ctx.dryRun)
21
+ ensureShimCurrent(agent);
22
+ fixed.push(`${cmd} shim`);
23
+ }
24
+ else if (!shimPointsAtLiveInstall(agent)) {
25
+ // Schema is current but the baked AGENTS_BIN points at a different, removed
26
+ // install (dev build, old npm-global, rotated version dir). ensureShimCurrent
27
+ // would no-op on a schema-current shim, so force a rewrite to the current install.
28
+ if (!ctx.dryRun)
29
+ createShim(agent);
30
+ fixed.push(`${cmd} shim (repointed to current install)`);
31
+ }
21
32
  }
22
- else if (!shimPointsAtLiveInstall(agent)) {
23
- // Schema is current but the baked AGENTS_BIN points at a different, removed
24
- // install (dev build, old npm-global, rotated version dir). ensureShimCurrent
25
- // would no-op on a schema-current shim, so force a rewrite to the current install.
33
+ else if (shimExists(agent)) {
26
34
  if (!ctx.dryRun)
27
- createShim(agent);
28
- fixed.push(`${cmd} shim (repointed to current install)`);
35
+ removeShim(agent);
36
+ fixed.push(`removed ${cmd} shim (isolated-only)`);
29
37
  }
30
38
  for (const version of listInstalledVersions(agent)) {
31
39
  if (!isVersionedAliasCurrent(agent, version)) {
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Rush Cloud session source.
3
3
  *
4
- * Fetches cloud-captured sessions from halo/proxy (api.prix.dev) and caches
4
+ * Fetches cloud-captured sessions from the cloud proxy (api.prix.dev) and caches
5
5
  * them locally so the existing filesystem-based parse pipeline works unchanged.
6
6
  *
7
- * Endpoints consumed (implemented in halo/proxy/src/cloud-runs.ts):
7
+ * Endpoints consumed by the cloud proxy:
8
8
  * GET /api/v1/cloud-runs → list executions
9
9
  * GET /api/v1/cloud-runs/:id → get one (used for meta)
10
10
  * GET /api/v1/cloud-runs/:id/session.jsonl → raw captured jsonl
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Rush Cloud session source.
3
3
  *
4
- * Fetches cloud-captured sessions from halo/proxy (api.prix.dev) and caches
4
+ * Fetches cloud-captured sessions from the cloud proxy (api.prix.dev) and caches
5
5
  * them locally so the existing filesystem-based parse pipeline works unchanged.
6
6
  *
7
- * Endpoints consumed (implemented in halo/proxy/src/cloud-runs.ts):
7
+ * Endpoints consumed by the cloud proxy:
8
8
  * GET /api/v1/cloud-runs → list executions
9
9
  * GET /api/v1/cloud-runs/:id → get one (used for meta)
10
10
  * GET /api/v1/cloud-runs/:id/session.jsonl → raw captured jsonl
@@ -13,6 +13,9 @@ export interface SessionRow {
13
13
  id: string;
14
14
  short_id: string;
15
15
  agent: string;
16
+ origin: string | null;
17
+ routine_name: string | null;
18
+ routine_run_id: string | null;
16
19
  version: string | null;
17
20
  account: string | null;
18
21
  timestamp: string;
@@ -48,6 +51,7 @@ export interface ScanStamp {
48
51
  export interface QueryOptions {
49
52
  agent?: SessionAgentId;
50
53
  agents?: SessionAgentId[];
54
+ origin?: 'cli' | 'routine';
51
55
  version?: string;
52
56
  cwd?: string;
53
57
  /** Match any session whose cwd equals this or is a descendant of it. */
@@ -13,7 +13,7 @@ import { getSessionsDir, getSessionsDbPath } from '../state.js';
13
13
  const SESSIONS_DIR = getSessionsDir();
14
14
  const DB_PATH = getSessionsDbPath();
15
15
  /** Current schema version; bumped when migrations are added. */
16
- const SCHEMA_VERSION = 12;
16
+ const SCHEMA_VERSION = 13;
17
17
  /**
18
18
  * Canonicalize a file path for use as a scan_ledger key. The same physical
19
19
  * session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
@@ -43,6 +43,9 @@ CREATE TABLE IF NOT EXISTS sessions (
43
43
  id TEXT PRIMARY KEY,
44
44
  short_id TEXT NOT NULL,
45
45
  agent TEXT NOT NULL,
46
+ origin TEXT DEFAULT 'cli',
47
+ routine_name TEXT,
48
+ routine_run_id TEXT,
46
49
  version TEXT,
47
50
  account TEXT,
48
51
  timestamp TEXT NOT NULL,
@@ -235,6 +238,20 @@ function migrateSchema(db, fromVersion) {
235
238
  db.exec(`ALTER TABLE sessions ADD COLUMN output_tokens INTEGER`);
236
239
  db.exec(`DELETE FROM scan_ledger;`);
237
240
  }
241
+ if (fromVersion < 13) {
242
+ // v12 → v13: routine runs archive their sandboxed transcript into the run
243
+ // directory and get indexed as origin='routine', linked by routine_name and
244
+ // routine_run_id. Existing rows are normal CLI-origin sessions.
245
+ const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
246
+ if (!cols.some(c => c.name === 'origin'))
247
+ db.exec(`ALTER TABLE sessions ADD COLUMN origin TEXT DEFAULT 'cli'`);
248
+ if (!cols.some(c => c.name === 'routine_name'))
249
+ db.exec(`ALTER TABLE sessions ADD COLUMN routine_name TEXT`);
250
+ if (!cols.some(c => c.name === 'routine_run_id'))
251
+ db.exec(`ALTER TABLE sessions ADD COLUMN routine_run_id TEXT`);
252
+ db.exec(`UPDATE sessions SET origin = 'cli' WHERE origin IS NULL OR origin = ''`);
253
+ db.exec(`DELETE FROM scan_ledger;`);
254
+ }
238
255
  }
239
256
  /** Open (or return the cached) sessions database, applying migrations as needed. */
240
257
  export function getDB() {
@@ -266,6 +283,8 @@ export function getDB() {
266
283
  // run. It must NOT live in SCHEMA (executed before migration) or an existing
267
284
  // DB would fail the index build on a column it doesn't have yet.
268
285
  db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_last_activity ON sessions(last_activity DESC)`);
286
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_origin ON sessions(origin)`);
287
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_routine_run_id ON sessions(routine_run_id)`);
269
288
  // One-shot cleanup of the pre-SQLite JSONL indexes. Safe — nothing reads
270
289
  // them anymore. Guarded by a meta flag so we only try once.
271
290
  const cleaned = db.prepare(`SELECT value FROM meta WHERE key = 'legacy_indexes_removed'`).get();
@@ -447,13 +466,15 @@ export function recordScans(entries) {
447
466
  }
448
467
  const upsertSessionStmt = (db) => db.prepare(`
449
468
  INSERT INTO sessions (
450
- id, short_id, agent, version, account, timestamp, last_activity,
469
+ id, short_id, agent, origin, routine_name, routine_run_id,
470
+ version, account, timestamp, last_activity,
451
471
  project, cwd, git_branch, topic, label, message_count, token_count,
452
472
  output_tokens, cost_usd, duration_ms,
453
473
  file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
454
474
  pr_url, pr_number, worktree_slug, ticket_id, plan
455
475
  ) VALUES (
456
- @id, @short_id, @agent, @version, @account, @timestamp, @last_activity,
476
+ @id, @short_id, @agent, @origin, @routine_name, @routine_run_id,
477
+ @version, @account, @timestamp, @last_activity,
457
478
  @project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
458
479
  @output_tokens, @cost_usd, @duration_ms,
459
480
  @file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
@@ -462,6 +483,9 @@ const upsertSessionStmt = (db) => db.prepare(`
462
483
  ON CONFLICT(id) DO UPDATE SET
463
484
  short_id = excluded.short_id,
464
485
  agent = excluded.agent,
486
+ origin = excluded.origin,
487
+ routine_name = excluded.routine_name,
488
+ routine_run_id = excluded.routine_run_id,
465
489
  version = excluded.version,
466
490
  account = excluded.account,
467
491
  timestamp = excluded.timestamp,
@@ -511,6 +535,9 @@ export function upsertSession(meta, content, scan) {
511
535
  id: meta.id,
512
536
  short_id: meta.shortId,
513
537
  agent: meta.agent,
538
+ origin: meta.origin ?? 'cli',
539
+ routine_name: meta.routineName ?? null,
540
+ routine_run_id: meta.routineRunId ?? null,
514
541
  version: meta.version ?? null,
515
542
  account: meta.account ?? null,
516
543
  timestamp: meta.timestamp,
@@ -601,6 +628,9 @@ export function upsertSessionsBatch(entries) {
601
628
  id: meta.id,
602
629
  short_id: meta.shortId,
603
630
  agent: meta.agent,
631
+ origin: meta.origin ?? 'cli',
632
+ routine_name: meta.routineName ?? null,
633
+ routine_run_id: meta.routineRunId ?? null,
604
634
  version: meta.version ?? null,
605
635
  account: meta.account ?? null,
606
636
  timestamp: meta.timestamp,
@@ -778,6 +808,9 @@ function rowToMeta(row) {
778
808
  id: row.id,
779
809
  shortId: row.short_id,
780
810
  agent: row.agent,
811
+ origin: (row.origin === 'routine' ? 'routine' : 'cli'),
812
+ routineName: row.routine_name ?? undefined,
813
+ routineRunId: row.routine_run_id ?? undefined,
781
814
  timestamp: row.timestamp,
782
815
  lastActivity: row.last_activity ?? undefined,
783
816
  project: row.project ?? undefined,
@@ -867,6 +900,10 @@ function buildSessionWhere(options) {
867
900
  where.push('version = ?');
868
901
  params.push(options.version);
869
902
  }
903
+ if (options.origin) {
904
+ where.push("IFNULL(origin, 'cli') = ?");
905
+ params.push(options.origin);
906
+ }
870
907
  if (options.cwd) {
871
908
  where.push('cwd = ?');
872
909
  params.push(options.cwd);
@@ -887,12 +924,12 @@ function buildSessionWhere(options) {
887
924
  // for the same reason. short_id carries its own index (idx_sessions_short_id);
888
925
  // id is the PRIMARY KEY.
889
926
  if (options.idExact) {
890
- where.push('(id = ? COLLATE NOCASE OR short_id = ? COLLATE NOCASE)');
891
- params.push(options.idExact, options.idExact);
927
+ where.push('(id = ? COLLATE NOCASE OR short_id = ? COLLATE NOCASE OR routine_run_id = ? COLLATE NOCASE)');
928
+ params.push(options.idExact, options.idExact, options.idExact);
892
929
  }
893
930
  if (options.idPrefix) {
894
- where.push('(id LIKE ? OR short_id LIKE ?)');
895
- params.push(`${options.idPrefix}%`, `${options.idPrefix}%`);
931
+ where.push('(id LIKE ? OR short_id LIKE ? OR routine_run_id LIKE ?)');
932
+ params.push(`${options.idPrefix}%`, `${options.idPrefix}%`, `${options.idPrefix}%`);
896
933
  }
897
934
  if (typeof options.sinceMs === 'number') {
898
935
  // Compare as strings; ISO 8601 timestamps sort lexicographically.
@@ -26,6 +26,8 @@ export interface DiscoverOptions {
26
26
  excludeTeamOrigin?: boolean;
27
27
  /** Keep only team-spawned sessions (used for hidden-count queries). */
28
28
  onlyTeamOrigin?: boolean;
29
+ /** Keep only sessions from this source. */
30
+ origin?: 'cli' | 'routine';
29
31
  /** Column to order results by (all descending): 'timestamp' (default), 'cost', or 'duration'. */
30
32
  sortBy?: 'timestamp' | 'cost' | 'duration';
31
33
  /** Called as each agent makes parsing progress. Totals count only files that need re-parsing (cache misses). */