@phnx-labs/agents-cli 1.19.2 → 1.20.3

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 (156) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +72 -12
  3. package/dist/browser.js +0 -0
  4. package/dist/commands/browser.js +88 -16
  5. package/dist/commands/cli.d.ts +14 -0
  6. package/dist/commands/cli.js +244 -0
  7. package/dist/commands/cloud.js +1 -1
  8. package/dist/commands/commands.js +27 -10
  9. package/dist/commands/computer.js +18 -1
  10. package/dist/commands/doctor.d.ts +1 -1
  11. package/dist/commands/doctor.js +2 -2
  12. package/dist/commands/exec.js +38 -18
  13. package/dist/commands/factory.d.ts +3 -14
  14. package/dist/commands/factory.js +3 -3
  15. package/dist/commands/feedback.d.ts +7 -0
  16. package/dist/commands/feedback.js +89 -0
  17. package/dist/commands/helper.d.ts +12 -0
  18. package/dist/commands/helper.js +87 -0
  19. package/dist/commands/hooks.js +89 -10
  20. package/dist/commands/mcp.js +166 -10
  21. package/dist/commands/packages.js +196 -27
  22. package/dist/commands/permissions.js +21 -6
  23. package/dist/commands/plugins.js +11 -4
  24. package/dist/commands/profiles.d.ts +8 -0
  25. package/dist/commands/profiles.js +118 -5
  26. package/dist/commands/prune.js +39 -160
  27. package/dist/commands/pull.js +58 -5
  28. package/dist/commands/routines.js +107 -14
  29. package/dist/commands/rules.js +8 -4
  30. package/dist/commands/secrets-migrate.d.ts +24 -0
  31. package/dist/commands/secrets-migrate.js +198 -0
  32. package/dist/commands/secrets-sync.d.ts +11 -0
  33. package/dist/commands/secrets-sync.js +155 -0
  34. package/dist/commands/secrets.js +79 -46
  35. package/dist/commands/sessions.d.ts +28 -0
  36. package/dist/commands/sessions.js +98 -33
  37. package/dist/commands/setup.d.ts +1 -0
  38. package/dist/commands/setup.js +37 -28
  39. package/dist/commands/skills.js +25 -8
  40. package/dist/commands/subagents.js +69 -49
  41. package/dist/commands/teams.js +61 -10
  42. package/dist/commands/utils.d.ts +33 -0
  43. package/dist/commands/utils.js +139 -0
  44. package/dist/commands/versions.d.ts +4 -3
  45. package/dist/commands/versions.js +134 -130
  46. package/dist/commands/view.d.ts +6 -0
  47. package/dist/commands/view.js +175 -19
  48. package/dist/commands/workflows.js +29 -6
  49. package/dist/computer.js +0 -0
  50. package/dist/index.js +38 -6
  51. package/dist/lib/acp/client.js +6 -1
  52. package/dist/lib/acp/harnesses.js +8 -0
  53. package/dist/lib/agents.d.ts +4 -0
  54. package/dist/lib/agents.js +125 -34
  55. package/dist/lib/auto-pull-worker.js +18 -1
  56. package/dist/lib/browser/cdp.d.ts +8 -1
  57. package/dist/lib/browser/cdp.js +40 -3
  58. package/dist/lib/browser/chrome.d.ts +13 -0
  59. package/dist/lib/browser/chrome.js +46 -3
  60. package/dist/lib/browser/domain-skills.d.ts +51 -0
  61. package/dist/lib/browser/domain-skills.js +157 -0
  62. package/dist/lib/browser/drivers/local.js +45 -4
  63. package/dist/lib/browser/drivers/ssh.js +2 -2
  64. package/dist/lib/browser/ipc.d.ts +8 -1
  65. package/dist/lib/browser/ipc.js +37 -28
  66. package/dist/lib/browser/profiles.d.ts +16 -3
  67. package/dist/lib/browser/profiles.js +44 -4
  68. package/dist/lib/browser/service.d.ts +3 -0
  69. package/dist/lib/browser/service.js +40 -5
  70. package/dist/lib/browser/types.d.ts +11 -4
  71. package/dist/lib/cli-resources.d.ts +137 -0
  72. package/dist/lib/cli-resources.js +477 -0
  73. package/dist/lib/cloud/factory.d.ts +1 -1
  74. package/dist/lib/cloud/factory.js +1 -1
  75. package/dist/lib/cloud/rush.js +5 -5
  76. package/dist/lib/command-skills.js +0 -2
  77. package/dist/lib/computer-rpc.d.ts +3 -0
  78. package/dist/lib/computer-rpc.js +53 -0
  79. package/dist/lib/daemon.js +20 -0
  80. package/dist/lib/events.d.ts +16 -2
  81. package/dist/lib/events.js +33 -2
  82. package/dist/lib/exec.d.ts +42 -13
  83. package/dist/lib/exec.js +127 -33
  84. package/dist/lib/help.js +11 -5
  85. package/dist/lib/hooks/cache.d.ts +38 -0
  86. package/dist/lib/hooks/cache.js +242 -0
  87. package/dist/lib/hooks/profile.d.ts +33 -0
  88. package/dist/lib/hooks/profile.js +129 -0
  89. package/dist/lib/hooks.d.ts +0 -10
  90. package/dist/lib/hooks.js +246 -11
  91. package/dist/lib/mcp.d.ts +15 -0
  92. package/dist/lib/mcp.js +46 -0
  93. package/dist/lib/migrate.js +1 -1
  94. package/dist/lib/overdue.d.ts +26 -0
  95. package/dist/lib/overdue.js +101 -0
  96. package/dist/lib/permissions.d.ts +13 -0
  97. package/dist/lib/permissions.js +55 -1
  98. package/dist/lib/plugin-marketplace.js +1 -1
  99. package/dist/lib/plugins.js +15 -1
  100. package/dist/lib/profiles-presets.d.ts +26 -0
  101. package/dist/lib/profiles-presets.js +216 -0
  102. package/dist/lib/profiles.d.ts +34 -0
  103. package/dist/lib/profiles.js +112 -1
  104. package/dist/lib/resources/mcp.js +37 -0
  105. package/dist/lib/resources.d.ts +1 -1
  106. package/dist/lib/rotate.js +10 -4
  107. package/dist/lib/routines-format.d.ts +47 -0
  108. package/dist/lib/routines-format.js +194 -0
  109. package/dist/lib/routines.d.ts +8 -2
  110. package/dist/lib/routines.js +34 -14
  111. package/dist/lib/runner.js +83 -15
  112. package/dist/lib/scheduler.js +8 -1
  113. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  114. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  115. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
  116. package/dist/lib/secrets/bundles.d.ts +34 -17
  117. package/dist/lib/secrets/bundles.js +210 -36
  118. package/dist/lib/secrets/index.d.ts +49 -30
  119. package/dist/lib/secrets/index.js +126 -115
  120. package/dist/lib/secrets/install-helper.d.ts +45 -0
  121. package/dist/lib/secrets/install-helper.js +165 -0
  122. package/dist/lib/secrets/linux.js +4 -4
  123. package/dist/lib/secrets/sync.d.ts +56 -0
  124. package/dist/lib/secrets/sync.js +180 -0
  125. package/dist/lib/session/active.d.ts +8 -0
  126. package/dist/lib/session/active.js +3 -2
  127. package/dist/lib/session/db.d.ts +0 -4
  128. package/dist/lib/session/db.js +0 -26
  129. package/dist/lib/session/parse.d.ts +1 -0
  130. package/dist/lib/session/parse.js +44 -0
  131. package/dist/lib/session/render.js +4 -4
  132. package/dist/lib/session/types.d.ts +2 -2
  133. package/dist/lib/session/types.js +1 -1
  134. package/dist/lib/shims.d.ts +5 -2
  135. package/dist/lib/shims.js +70 -38
  136. package/dist/lib/state.d.ts +14 -2
  137. package/dist/lib/state.js +51 -20
  138. package/dist/lib/teams/agents.d.ts +5 -4
  139. package/dist/lib/teams/agents.js +48 -22
  140. package/dist/lib/teams/api.d.ts +2 -1
  141. package/dist/lib/teams/api.js +4 -3
  142. package/dist/lib/teams/parsers.d.ts +1 -1
  143. package/dist/lib/teams/parsers.js +153 -3
  144. package/dist/lib/teams/summarizer.js +18 -2
  145. package/dist/lib/teams/worktree.js +14 -3
  146. package/dist/lib/types.d.ts +63 -4
  147. package/dist/lib/types.js +8 -3
  148. package/dist/lib/usage.d.ts +27 -2
  149. package/dist/lib/usage.js +100 -17
  150. package/dist/lib/versions.d.ts +45 -3
  151. package/dist/lib/versions.js +455 -60
  152. package/package.json +15 -14
  153. package/scripts/install-helper.js +97 -0
  154. package/scripts/postinstall.js +16 -0
  155. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  156. package/npm-shrinkwrap.json +0 -3162
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Remote sync client for secrets bundles.
3
+ *
4
+ * Replaces the previous "leave it to iCloud Keychain" model with explicit
5
+ * push/pull against api.prix.dev. Bundle contents (vars + secret values) are
6
+ * encrypted client-side with AES-256-GCM under a key derived from a
7
+ * user-supplied passphrase via PBKDF2-SHA256. Plaintext never leaves the
8
+ * machine — api.prix.dev only ever sees the ciphertext + KDF parameters.
9
+ */
10
+ import { type SecretsBundle } from './bundles.js';
11
+ export declare const MIN_PASSPHRASE_LEN = 12;
12
+ /** Envelope for an encrypted bundle. All byte fields are base64. */
13
+ export interface EncryptedEnvelope {
14
+ v: 1;
15
+ kdf: 'pbkdf2-sha256';
16
+ iter: number;
17
+ salt: string;
18
+ iv: string;
19
+ ct: string;
20
+ tag: string;
21
+ }
22
+ interface RemoteBundleSummary {
23
+ name: string;
24
+ updated_at: string;
25
+ }
26
+ /** Encrypt a JSON-serializable payload with a passphrase. */
27
+ export declare function encryptBlob(plaintext: string, passphrase: string): EncryptedEnvelope;
28
+ /** Decrypt an envelope. Throws on bad passphrase (auth tag mismatch). */
29
+ export declare function decryptBlob(envelope: EncryptedEnvelope, passphrase: string): string;
30
+ /** The plaintext we serialize before encrypting: bundle metadata + secret values. */
31
+ export interface BundleSnapshot {
32
+ bundle: SecretsBundle;
33
+ /** keychain shortId -> plaintext value. Only present for keychain: refs. */
34
+ secrets: Record<string, string>;
35
+ }
36
+ /** Options for pushBundle. */
37
+ export interface PushOptions {
38
+ passphrase: string;
39
+ }
40
+ /** Push a local bundle to api.prix.dev. Encrypts client-side; server only sees ciphertext. */
41
+ export declare function pushBundle(name: string, opts: PushOptions): Promise<{
42
+ updated_at: string;
43
+ }>;
44
+ /** Options for pullBundle. */
45
+ export interface PullOptions {
46
+ passphrase: string;
47
+ /** When true, overwrite an existing local bundle. */
48
+ force?: boolean;
49
+ }
50
+ /** Pull a bundle by name from api.prix.dev and materialize it locally. */
51
+ export declare function pullBundle(name: string, opts: PullOptions): Promise<SecretsBundle>;
52
+ /** Delete a bundle on the remote. */
53
+ export declare function deleteRemoteBundle(name: string): Promise<boolean>;
54
+ /** List bundles currently stored on api.prix.dev for this user. */
55
+ export declare function listRemoteBundles(): Promise<RemoteBundleSummary[]>;
56
+ export {};
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Remote sync client for secrets bundles.
3
+ *
4
+ * Replaces the previous "leave it to iCloud Keychain" model with explicit
5
+ * push/pull against api.prix.dev. Bundle contents (vars + secret values) are
6
+ * encrypted client-side with AES-256-GCM under a key derived from a
7
+ * user-supplied passphrase via PBKDF2-SHA256. Plaintext never leaves the
8
+ * machine — api.prix.dev only ever sees the ciphertext + KDF parameters.
9
+ */
10
+ import * as crypto from 'crypto';
11
+ import * as fs from 'fs';
12
+ import * as os from 'os';
13
+ import * as path from 'path';
14
+ import * as yaml from 'yaml';
15
+ import { getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from './index.js';
16
+ import { readBundle, writeBundle, keychainItemsForBundle, validateBundleName, } from './bundles.js';
17
+ const PROXY_BASE = 'https://api.prix.dev';
18
+ const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
19
+ const BUNDLE_ENDPOINT = '/api/v1/secrets/bundles';
20
+ // PBKDF2 cost. 600k SHA-256 iters matches OWASP 2023+ guidance and keeps a
21
+ // passphrase prompt under a second on the hardware the CLI targets.
22
+ const PBKDF2_ITER = 600_000;
23
+ export const MIN_PASSPHRASE_LEN = 12;
24
+ const KEY_LEN = 32;
25
+ const SALT_LEN = 16;
26
+ const IV_LEN = 12;
27
+ function readRushToken() {
28
+ if (!fs.existsSync(USER_YAML)) {
29
+ throw new Error('Not logged in to Rush. Run `rush login` first.');
30
+ }
31
+ const raw = fs.readFileSync(USER_YAML, 'utf-8');
32
+ const data = yaml.parse(raw);
33
+ const token = data?.session?.access_token;
34
+ if (!token) {
35
+ throw new Error('No session token in ~/.rush/user.yaml. Run `rush login` first.');
36
+ }
37
+ return token;
38
+ }
39
+ async function api(method, endpoint, body) {
40
+ const token = readRushToken();
41
+ const url = endpoint.startsWith('http') ? endpoint : `${PROXY_BASE}${endpoint}`;
42
+ return fetch(url, {
43
+ method,
44
+ headers: {
45
+ Authorization: `Bearer ${token}`,
46
+ 'Content-Type': 'application/json',
47
+ },
48
+ body: body === undefined ? undefined : JSON.stringify(body),
49
+ });
50
+ }
51
+ function deriveKey(passphrase, salt) {
52
+ return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITER, KEY_LEN, 'sha256');
53
+ }
54
+ /** Encrypt a JSON-serializable payload with a passphrase. */
55
+ export function encryptBlob(plaintext, passphrase) {
56
+ if (!passphrase || passphrase.length < MIN_PASSPHRASE_LEN) {
57
+ throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
58
+ }
59
+ const salt = crypto.randomBytes(SALT_LEN);
60
+ const iv = crypto.randomBytes(IV_LEN);
61
+ const key = deriveKey(passphrase, salt);
62
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
63
+ const ct = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
64
+ const tag = cipher.getAuthTag();
65
+ return {
66
+ v: 1,
67
+ kdf: 'pbkdf2-sha256',
68
+ iter: PBKDF2_ITER,
69
+ salt: salt.toString('base64'),
70
+ iv: iv.toString('base64'),
71
+ ct: ct.toString('base64'),
72
+ tag: tag.toString('base64'),
73
+ };
74
+ }
75
+ /** Decrypt an envelope. Throws on bad passphrase (auth tag mismatch). */
76
+ export function decryptBlob(envelope, passphrase) {
77
+ if (envelope.v !== 1 || envelope.kdf !== 'pbkdf2-sha256') {
78
+ throw new Error(`Unsupported envelope version (v${envelope.v}, kdf=${envelope.kdf}).`);
79
+ }
80
+ const salt = Buffer.from(envelope.salt, 'base64');
81
+ const iv = Buffer.from(envelope.iv, 'base64');
82
+ const ct = Buffer.from(envelope.ct, 'base64');
83
+ const tag = Buffer.from(envelope.tag, 'base64');
84
+ const key = deriveKey(passphrase, salt);
85
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
86
+ decipher.setAuthTag(tag);
87
+ try {
88
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf-8');
89
+ }
90
+ catch {
91
+ throw new Error('Decryption failed — wrong passphrase or corrupt blob.');
92
+ }
93
+ }
94
+ function snapshotBundle(name) {
95
+ const bundle = readBundle(name);
96
+ const secrets = {};
97
+ for (const { key, item } of keychainItemsForBundle(bundle)) {
98
+ if (!hasKeychainToken(item)) {
99
+ throw new Error(`Bundle '${name}' key '${key}': keychain item '${item}' missing — cannot push incomplete bundle.`);
100
+ }
101
+ const raw = bundle.vars[key];
102
+ if (typeof raw !== 'string' || !raw.startsWith('keychain:'))
103
+ continue;
104
+ const shortId = raw.slice('keychain:'.length);
105
+ secrets[shortId] = getKeychainToken(item);
106
+ }
107
+ return { bundle, secrets };
108
+ }
109
+ function restoreSnapshot(snap) {
110
+ const bundle = snap.bundle;
111
+ validateBundleName(bundle.name);
112
+ for (const [shortId, value] of Object.entries(snap.secrets)) {
113
+ const item = secretsKeychainItem(bundle.name, shortId);
114
+ setKeychainToken(item, value);
115
+ }
116
+ writeBundle(bundle);
117
+ }
118
+ /** Push a local bundle to api.prix.dev. Encrypts client-side; server only sees ciphertext. */
119
+ export async function pushBundle(name, opts) {
120
+ validateBundleName(name);
121
+ const snap = snapshotBundle(name);
122
+ const envelope = encryptBlob(JSON.stringify(snap), opts.passphrase);
123
+ const updated_at = new Date().toISOString();
124
+ const payload = { envelope, updated_at };
125
+ const res = await api('PUT', `${BUNDLE_ENDPOINT}/${encodeURIComponent(name)}`, payload);
126
+ if (!res.ok) {
127
+ const body = await res.text().catch(() => '');
128
+ throw new Error(`Push failed (${res.status} ${res.statusText}): ${body}`);
129
+ }
130
+ return { updated_at };
131
+ }
132
+ /** Pull a bundle by name from api.prix.dev and materialize it locally. */
133
+ export async function pullBundle(name, opts) {
134
+ validateBundleName(name);
135
+ const res = await api('GET', `${BUNDLE_ENDPOINT}/${encodeURIComponent(name)}`);
136
+ if (res.status === 404) {
137
+ throw new Error(`Remote bundle '${name}' not found on api.prix.dev.`);
138
+ }
139
+ if (!res.ok) {
140
+ const body = await res.text().catch(() => '');
141
+ throw new Error(`Pull failed (${res.status} ${res.statusText}): ${body}`);
142
+ }
143
+ const data = await res.json();
144
+ const plaintext = decryptBlob(data.envelope, opts.passphrase);
145
+ const snap = JSON.parse(plaintext);
146
+ if (!snap || !snap.bundle || snap.bundle.name !== name) {
147
+ throw new Error(`Decrypted payload for '${name}' is malformed (bundle name mismatch).`);
148
+ }
149
+ // existence check is the caller's responsibility; we trust opts.force.
150
+ if (!opts.force) {
151
+ const { bundleExists } = await import('./bundles.js');
152
+ if (bundleExists(name)) {
153
+ throw new Error(`Local bundle '${name}' already exists. Re-run with --force to overwrite.`);
154
+ }
155
+ }
156
+ restoreSnapshot(snap);
157
+ return snap.bundle;
158
+ }
159
+ /** Delete a bundle on the remote. */
160
+ export async function deleteRemoteBundle(name) {
161
+ validateBundleName(name);
162
+ const res = await api('DELETE', `${BUNDLE_ENDPOINT}/${encodeURIComponent(name)}`);
163
+ if (res.status === 404)
164
+ return false;
165
+ if (!res.ok) {
166
+ const body = await res.text().catch(() => '');
167
+ throw new Error(`Delete failed (${res.status} ${res.statusText}): ${body}`);
168
+ }
169
+ return true;
170
+ }
171
+ /** List bundles currently stored on api.prix.dev for this user. */
172
+ export async function listRemoteBundles() {
173
+ const res = await api('GET', BUNDLE_ENDPOINT);
174
+ if (!res.ok) {
175
+ const body = await res.text().catch(() => '');
176
+ throw new Error(`List failed (${res.status} ${res.statusText}): ${body}`);
177
+ }
178
+ const data = await res.json();
179
+ return data.bundles ?? [];
180
+ }
@@ -20,6 +20,14 @@ export interface ActiveSession {
20
20
  cloudProvider?: string;
21
21
  cloudTaskId?: string;
22
22
  cloudStatus?: string;
23
+ /**
24
+ * IDE window that owns this terminal. Source of truth is the per-window
25
+ * slice key in `live-terminals.json` (computeWindowId in the swarmify
26
+ * extension): `${vscode.env.sessionId}-${extension-host pid}`. Lets the
27
+ * renderer cluster terminals that belong to the same IDE window even when
28
+ * two windows have the same cwd open. Only populated for `terminal` context.
29
+ */
30
+ windowId?: string;
23
31
  }
24
32
  export interface ActiveQueryOptions {
25
33
  /** Skip the `ps` scan for ad-hoc headless agents. */
@@ -73,11 +73,11 @@ function readLiveTerminals() {
73
73
  if (!parsed || typeof parsed !== 'object')
74
74
  return [];
75
75
  const merged = new Map();
76
- for (const slice of Object.values(parsed)) {
76
+ for (const [windowId, slice] of Object.entries(parsed)) {
77
77
  for (const e of (slice?.entries ?? [])) {
78
78
  if (!e?.sessionId || !isPidAlive(e.pid))
79
79
  continue;
80
- merged.set(e.sessionId, e);
80
+ merged.set(e.sessionId, { ...e, windowId });
81
81
  }
82
82
  }
83
83
  return Array.from(merged.values());
@@ -259,6 +259,7 @@ export async function listTerminalsActive() {
259
259
  sessionFile,
260
260
  startedAtMs: t.startedAtMs,
261
261
  status: classifyActivity(sessionFile),
262
+ windowId: t.windowId,
262
263
  };
263
264
  });
264
265
  }
@@ -155,8 +155,6 @@ export declare function getRowCount(): {
155
155
  sessions: number;
156
156
  textRows: number;
157
157
  };
158
- /** Count sessions older than the given timestamp (for dry-run previews). */
159
- export declare function countSessionsOlderThan(cutoffMs: number): number;
160
158
  /**
161
159
  * Rewrite file_path for all sessions whose path starts with oldPrefix, replacing
162
160
  * it with newPrefix + the unchanged suffix. Also clears the matching scan_ledger
@@ -167,5 +165,3 @@ export declare function countSessionsOlderThan(cutoffMs: number): number;
167
165
  * Returns the number of session rows updated.
168
166
  */
169
167
  export declare function updateSessionFilePaths(oldPrefix: string, newPrefix: string): number;
170
- /** Delete sessions older than the given timestamp. Returns the number of rows deleted. */
171
- export declare function deleteSessionsOlderThan(cutoffMs: number): number;
@@ -742,13 +742,6 @@ export function getRowCount() {
742
742
  const textRows = db.prepare(`SELECT COUNT(*) AS c FROM session_text`).get().c;
743
743
  return { sessions, textRows };
744
744
  }
745
- /** Count sessions older than the given timestamp (for dry-run previews). */
746
- export function countSessionsOlderThan(cutoffMs) {
747
- const db = getDB();
748
- const cutoffIso = new Date(cutoffMs).toISOString();
749
- const row = db.prepare(`SELECT COUNT(*) AS n FROM sessions WHERE timestamp < ?`).get(cutoffIso);
750
- return row.n;
751
- }
752
745
  /**
753
746
  * Rewrite file_path for all sessions whose path starts with oldPrefix, replacing
754
747
  * it with newPrefix + the unchanged suffix. Also clears the matching scan_ledger
@@ -775,22 +768,3 @@ export function updateSessionFilePaths(oldPrefix, newPrefix) {
775
768
  txn();
776
769
  return rows.length;
777
770
  }
778
- /** Delete sessions older than the given timestamp. Returns the number of rows deleted. */
779
- export function deleteSessionsOlderThan(cutoffMs) {
780
- const db = getDB();
781
- const cutoffIso = new Date(cutoffMs).toISOString();
782
- const rows = db.prepare(`SELECT id, file_path FROM sessions WHERE timestamp < ?`).all(cutoffIso);
783
- if (rows.length === 0)
784
- return 0;
785
- const txn = db.transaction(() => {
786
- for (const { id, file_path } of rows) {
787
- db.prepare(`DELETE FROM session_text WHERE session_id = ?`).run(id);
788
- db.prepare(`DELETE FROM sessions WHERE id = ?`).run(id);
789
- if (file_path) {
790
- db.prepare(`DELETE FROM scan_ledger WHERE file_path = ?`).run(canonicalLedgerKey(file_path));
791
- }
792
- }
793
- });
794
- txn();
795
- return rows.length;
796
- }
@@ -43,6 +43,7 @@ export declare function parseGemini(filePath: string): SessionEvent[];
43
43
  * Messages have role (user/assistant) and metadata.
44
44
  * Parts contain the actual content: text, tool, reasoning, patch, step-start/finish.
45
45
  */
46
+ export declare function parseGrok(filePath: string): SessionEvent[];
46
47
  export declare function parseOpenCode(filePath: string): SessionEvent[];
47
48
  /** Parse a Rush JSONL session file into normalized events. */
48
49
  export declare function parseRush(filePath: string): SessionEvent[];
@@ -93,6 +93,9 @@ export function parseSession(filePath, agent) {
93
93
  case 'opencode':
94
94
  events = parseOpenCode(filePath);
95
95
  break;
96
+ case 'grok':
97
+ events = parseGrok(filePath);
98
+ break;
96
99
  case 'rush':
97
100
  events = parseRush(filePath);
98
101
  break;
@@ -118,6 +121,8 @@ export function detectAgent(filePath) {
118
121
  return 'codex';
119
122
  if (filePath.includes('/.gemini/') || filePath.includes('\\.gemini\\'))
120
123
  return 'gemini';
124
+ if (filePath.includes('/.grok/') || filePath.includes('\\.grok\\'))
125
+ return 'grok';
121
126
  if (filePath.includes('/.rush/') || filePath.includes('\\.rush\\'))
122
127
  return 'rush';
123
128
  if (filePath.includes('/.hermes/') || filePath.includes('\\.hermes\\'))
@@ -645,6 +650,45 @@ function extractGeminiContent(content) {
645
650
  * Messages have role (user/assistant) and metadata.
646
651
  * Parts contain the actual content: text, tool, reasoning, patch, step-start/finish.
647
652
  */
653
+ export function parseGrok(filePath) {
654
+ // Grok sessions are rich (summary.json + events.jsonl + chat_history.jsonl + updates.jsonl)
655
+ // This is a minimal stub for now so grok appears in `agents sessions`.
656
+ // Full parser (with subagents, tool calls, etc.) can be expanded later.
657
+ try {
658
+ const content = fs.readFileSync(filePath, 'utf-8');
659
+ // If it's a summary.json, create a basic event
660
+ if (filePath.endsWith('summary.json')) {
661
+ const summary = JSON.parse(content);
662
+ return [{
663
+ timestamp: summary.created_at || new Date().toISOString(),
664
+ type: 'session_start',
665
+ content: summary.session_summary || 'Grok session',
666
+ agent: 'grok',
667
+ metadata: { sessionId: summary.id, cwd: summary.cwd },
668
+ }];
669
+ }
670
+ // For JSONL files (events, chat_history, updates), return basic parsed lines
671
+ if (filePath.endsWith('.jsonl')) {
672
+ const lines = content.trim().split('\n').filter(Boolean);
673
+ return lines.slice(0, 50).map((line, i) => {
674
+ try {
675
+ const obj = JSON.parse(line);
676
+ return {
677
+ timestamp: obj.timestamp || obj.ts || new Date().toISOString(),
678
+ type: obj.type || obj.method || 'grok_event',
679
+ content: typeof obj.content === 'string' ? obj.content : JSON.stringify(obj).slice(0, 200),
680
+ agent: 'grok',
681
+ };
682
+ }
683
+ catch {
684
+ return { timestamp: new Date().toISOString(), type: 'raw', content: line.slice(0, 200), agent: 'grok' };
685
+ }
686
+ });
687
+ }
688
+ }
689
+ catch { }
690
+ return [];
691
+ }
648
692
  export function parseOpenCode(filePath) {
649
693
  const [dbPath, sessionId] = filePath.split('#');
650
694
  if (!dbPath || !sessionId)
@@ -806,15 +806,15 @@ export function renderConversationMarkdown(events, opts = {}) {
806
806
  for (const event of events) {
807
807
  if (event.type === 'message') {
808
808
  if (event.role === 'user') {
809
- parts.push(`## User\n\n${event.content ?? ''}`);
809
+ parts.push(`## User\n\n${sanitize(event.content ?? '')}`);
810
810
  }
811
811
  else if (event.role === 'assistant') {
812
- parts.push(`## Assistant\n\n${event.content ?? ''}`);
812
+ parts.push(`## Assistant\n\n${sanitize(event.content ?? '')}`);
813
813
  }
814
814
  }
815
815
  else if (event.type === 'thinking') {
816
816
  if (event.content)
817
- parts.push(`### Thinking\n\n${event.content}`);
817
+ parts.push(`### Thinking\n\n${sanitize(event.content)}`);
818
818
  }
819
819
  else if (event.type === 'tool_use') {
820
820
  const tool = event.tool || 'unknown';
@@ -837,7 +837,7 @@ export function renderConversationMarkdown(events, opts = {}) {
837
837
  }
838
838
  }
839
839
  else if (event.type === 'error') {
840
- parts.push(`### Error\n\n${event.content || event.tool || 'Unknown error'}`);
840
+ parts.push(`### Error\n\n${event.content ? sanitize(event.content) : (event.tool || 'Unknown error')}`);
841
841
  }
842
842
  }
843
843
  return parts.join('\n\n');
@@ -7,7 +7,7 @@
7
7
  * speaks these types.
8
8
  */
9
9
  /** Agents that store session data on disk and can be discovered by `agents sessions`. */
10
- export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'openclaw' | 'rush' | 'hermes';
10
+ export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok';
11
11
  /** All agents with session discovery support, in display order. */
12
12
  export declare const SESSION_AGENTS: SessionAgentId[];
13
13
  /** A single normalized event within a session (message, tool call, thinking, etc.). */
@@ -37,7 +37,7 @@ export interface SessionEvent {
37
37
  export interface TeamOrigin {
38
38
  /** Teammate name if set, otherwise first 8 chars of the agent UUID. */
39
39
  handle?: string;
40
- /** Agent mode: 'plan', 'edit', or 'full'. */
40
+ /** Agent mode: 'plan', 'edit', 'auto', or 'skip' ('full' accepted as legacy alias for 'skip'). */
41
41
  mode?: string;
42
42
  }
43
43
  /** Lightweight metadata for a discovered session, used in listings and pickers. */
@@ -7,4 +7,4 @@
7
7
  * speaks these types.
8
8
  */
9
9
  /** All agents with session discovery support, in display order. */
10
- export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes'];
10
+ export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes', 'grok'];
@@ -59,8 +59,11 @@ export interface ConflictInfo {
59
59
  * instead of hardcoding `.${agent}`. Backwards-compatible for every
60
60
  * existing agent (their configDir is `~/.{agent}`); enables nested
61
61
  * layouts like Antigravity's `~/.gemini/antigravity-cli/`.
62
+ * v15 — remove foreground resource sync / rules refresh from launch shims.
63
+ * Version homes are reconciled by agents-cli management commands; the
64
+ * shim hot path only resolves a version and execs the agent binary.
62
65
  */
63
- export declare const SHIM_SCHEMA_VERSION = 14;
66
+ export declare const SHIM_SCHEMA_VERSION = 15;
64
67
  /**
65
68
  * Generate the full bash shim script for the given agent. The returned string
66
69
  * is written to ~/.agents/shims/{cliCommand} and made executable.
@@ -213,7 +216,7 @@ export declare function getPathShadowingExecutable(agent: AgentId): string | nul
213
216
  * Delete the legacy ~/.agents/shims/<cli> file if it exists, returning whether
214
217
  * anything was removed. Pre-split installs put shims under ~/.agents/shims/;
215
218
  * the new layout uses ~/.agents-system/shims/. The leftover file causes the
216
- * repair-prompt loop reported in EXAMPLE-664 — `getPathShadowingExecutable` flags
219
+ * repair-prompt loop reported in PROJ-789 — `getPathShadowingExecutable` flags
217
220
  * it as a shadow but `addShimsToPath` only edits rc files, never the file
218
221
  * itself. Removing it ends the loop.
219
222
  */
package/dist/lib/shims.js CHANGED
@@ -183,8 +183,11 @@ async function promptConflictStrategy(conflictInfos) {
183
183
  * instead of hardcoding `.${agent}`. Backwards-compatible for every
184
184
  * existing agent (their configDir is `~/.{agent}`); enables nested
185
185
  * layouts like Antigravity's `~/.gemini/antigravity-cli/`.
186
+ * v15 — remove foreground resource sync / rules refresh from launch shims.
187
+ * Version homes are reconciled by agents-cli management commands; the
188
+ * shim hot path only resolves a version and execs the agent binary.
186
189
  */
187
- export const SHIM_SCHEMA_VERSION = 14;
190
+ export const SHIM_SCHEMA_VERSION = 15;
188
191
  /** Internal marker string used to embed the schema version in shim scripts. */
189
192
  const SHIM_VERSION_MARKER = 'agents-shim-version:';
190
193
  function shellQuote(value) {
@@ -226,19 +229,22 @@ fi
226
229
  # written by agents-cli actually take effect.
227
230
  export CODEX_HOME="$VERSION_DIR/home/${configDirName}"
228
231
  `
229
- : '';
230
- // Agents that don't natively resolve @-imports in their rules file need
231
- // agents-cli to recompile when the user edits a rule/preset file. The
232
- // check is fast (sha256 of ~8 small files) and skips the recompile when
233
- // sources haven't changed.
234
- const refreshRulesCall = !agentConfig.capabilities.rulesImports
235
- ? `
236
- # Recompile rules if any rule/preset source has changed since last sync.
237
- # Fast-path check (~10-20ms) when nothing changed; full recompile only on
238
- # actual diff. Non-blocking failure — if the refresh errors, we still launch.
239
- "$AGENTS_BIN" refresh-rules --agent "$AGENT" --agent-version "$VERSION" --quiet 2>/dev/null || true
232
+ : agent === 'copilot'
233
+ ? `
234
+ # GitHub Copilot CLI honors COPILOT_HOME to relocate its config and state
235
+ # (settings.json, mcp-config.json, session-state/, logs/, plugins/). Point
236
+ # it at the versioned home so MCP servers, custom agents, and session
237
+ # history are isolated per copilot version.
238
+ export COPILOT_HOME="$VERSION_DIR/home/${configDirName}"
239
+ `
240
+ : agent === 'grok'
241
+ ? `
242
+ # Grok Build uses GROK_HOME to isolate its entire configuration tree
243
+ # (skills, hooks, plugins, agents, memory, sessions, config.toml, MCP, etc.).
244
+ # This gives agents-cli full versioned isolation + resource sync for grok.
245
+ export GROK_HOME="$VERSION_DIR/home/.grok"
240
246
  `
241
- : '';
247
+ : '';
242
248
  const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
243
249
  return `#!/bin/bash
244
250
  # Auto-generated by agents-cli - do not edit
@@ -292,22 +298,6 @@ resolve_default_version() {
292
298
  fi
293
299
  }
294
300
 
295
- # Find project-scoped .agents directory (stop at agents.yaml or .git)
296
- find_project_agents_dir() {
297
- local dir="$PWD"
298
- while [ "$dir" != "/" ]; do
299
- if [ -d "$dir/.agents" ]; then
300
- echo "$dir/.agents"
301
- return 0
302
- fi
303
- if [ -f "$dir/agents.yaml" ] || [ -d "$dir/.git" ] || [ -f "$dir/.git" ]; then
304
- break
305
- fi
306
- dir=$(dirname "$dir")
307
- done
308
- return 1
309
- }
310
-
311
301
  # Find the latest installed version by numeric component comparison.
312
302
  # Handles both semver (2.1.138) and date-based (2026.5.7) version strings.
313
303
  find_latest_installed() {
@@ -376,7 +366,27 @@ if [[ ! "$VERSION" =~ ^(latest|[A-Za-z0-9._+-]{1,64})$ || "$VERSION" == *..* ]];
376
366
  fi
377
367
 
378
368
  VERSION_DIR="$AGENTS_USER_DIR/.history/versions/$AGENT/$VERSION"
379
- BINARY="$VERSION_DIR/node_modules/.bin/$CLI_COMMAND"
369
+
370
+ # Grok special case: binary lives in ~/.grok/downloads/, not node_modules.
371
+ # We still use the agents-cli version dir purely for GROK_HOME isolation.
372
+ if [ "$AGENT" = "grok" ]; then
373
+ # Try to find a matching binary for the pinned version in the global grok downloads dir.
374
+ GROK_DOWNLOADS="$HOME/.grok/downloads"
375
+ if [ -d "$GROK_DOWNLOADS" ]; then
376
+ # Prefer a binary whose filename contains the exact version
377
+ BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "$VERSION" | head -1)
378
+ if [ -z "$BINARY" ]; then
379
+ # Fallback to the "current" grok binary (symlink or latest)
380
+ BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
381
+ fi
382
+ fi
383
+ if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
384
+ # Last resort: whatever is on PATH (user may have installed grok globally)
385
+ BINARY=$(command -v grok 2>/dev/null || echo "")
386
+ fi
387
+ else
388
+ BINARY="$VERSION_DIR/node_modules/.bin/$CLI_COMMAND"
389
+ fi
380
390
 
381
391
  # Auto-install if not present
382
392
  if [ ! -x "$BINARY" ]; then
@@ -439,12 +449,7 @@ if [ ! -x "$BINARY" ]; then
439
449
  fi
440
450
  fi
441
451
 
442
- # Sync project-scoped resources into version home if a project .agents/ is present
443
- PROJECT_AGENTS_DIR=$(find_project_agents_dir)
444
- if [ -n "$PROJECT_AGENTS_DIR" ]; then
445
- "$AGENTS_BIN" sync --agent "$AGENT" --agent-version "$VERSION" --project-dir "$PROJECT_AGENTS_DIR" --quiet >/dev/null 2>&1
446
- fi
447
- ${refreshRulesCall}${managedEnv}
452
+ ${managedEnv}
448
453
 
449
454
  exec "$BINARY"${launchArgs} "$@"
450
455
  `;
@@ -528,7 +533,14 @@ export CLAUDE_CONFIG_DIR="$HOME/.agents/.history/versions/${agent}/${version}/ho
528
533
  # and rules written by agents-cli actually take effect.
529
534
  export CODEX_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
530
535
  `
531
- : '';
536
+ : agent === 'copilot'
537
+ ? `
538
+ # Copilot honors COPILOT_HOME to relocate ~/.copilot (settings, mcp-config.json,
539
+ # session-state, logs). Point direct aliases at the versioned home so per-
540
+ # version MCP and session state are isolated.
541
+ export COPILOT_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
542
+ `
543
+ : '';
532
544
  const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
533
545
  return `#!/bin/bash
534
546
  # Auto-generated by agents-cli - do not edit
@@ -719,6 +731,26 @@ export async function switchConfigSymlink(agent, version) {
719
731
  // Already pointing to correct target, no-op
720
732
  return { success: true };
721
733
  }
734
+ // openclaw mixes user data (openclaw.json, openclaw.db, per-agent
735
+ // workspaces under ~/.openclaw/{agentId}/, memory/) with the version
736
+ // home — silently swapping the symlink to a fresh version home strips
737
+ // every running agent's config + workspace + memory. Carry the user
738
+ // data forward into the new version home before flipping the symlink
739
+ // (keep-dest preserves anything the new version already shipped).
740
+ // Other agents (Claude, Codex, etc.) keep user data outside the
741
+ // version-home dir, so this is openclaw-only by design.
742
+ if (agent === 'openclaw') {
743
+ try {
744
+ if (fs.existsSync(resolvedCurrent) && fs.statSync(resolvedCurrent).isDirectory()) {
745
+ await copyDirContents(resolvedCurrent, versionConfigPath, 'keep-dest');
746
+ }
747
+ }
748
+ catch (migrationErr) {
749
+ console.error(`Warning: openclaw data migration from ${resolvedCurrent} -> ${versionConfigPath} ` +
750
+ `failed: ${migrationErr.message}. The previous version's data is intact ` +
751
+ `at the old path; you can copy it manually if needed.`);
752
+ }
753
+ }
722
754
  // Different target - update it
723
755
  fs.unlinkSync(configPath);
724
756
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
@@ -1182,7 +1214,7 @@ export function getPathShadowingExecutable(agent) {
1182
1214
  * Delete the legacy ~/.agents/shims/<cli> file if it exists, returning whether
1183
1215
  * anything was removed. Pre-split installs put shims under ~/.agents/shims/;
1184
1216
  * the new layout uses ~/.agents-system/shims/. The leftover file causes the
1185
- * repair-prompt loop reported in EXAMPLE-664 — `getPathShadowingExecutable` flags
1217
+ * repair-prompt loop reported in PROJ-789 — `getPathShadowingExecutable` flags
1186
1218
  * it as a shadow but `addShimsToPath` only edits rc files, never the file
1187
1219
  * itself. Removing it ends the loop.
1188
1220
  */