@phnx-labs/agents-cli 1.20.33 → 1.20.35

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 (159) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +32 -2
  3. package/dist/commands/computer-actions.d.ts +15 -0
  4. package/dist/commands/computer-actions.js +120 -22
  5. package/dist/commands/computer.d.ts +23 -0
  6. package/dist/commands/computer.js +45 -3
  7. package/dist/commands/cost.js +17 -12
  8. package/dist/commands/doctor.d.ts +11 -0
  9. package/dist/commands/doctor.js +108 -14
  10. package/dist/commands/events.d.ts +16 -0
  11. package/dist/commands/events.js +173 -0
  12. package/dist/commands/exec.js +6 -1
  13. package/dist/commands/hosts.js +28 -24
  14. package/dist/commands/import.js +1 -1
  15. package/dist/commands/inspect.d.ts +2 -0
  16. package/dist/commands/inspect.js +54 -7
  17. package/dist/commands/logs.d.ts +17 -0
  18. package/dist/commands/logs.js +139 -0
  19. package/dist/commands/models.d.ts +3 -0
  20. package/dist/commands/models.js +27 -8
  21. package/dist/commands/permissions.js +9 -2
  22. package/dist/commands/repo.d.ts +34 -0
  23. package/dist/commands/repo.js +243 -65
  24. package/dist/commands/resource-view.d.ts +20 -0
  25. package/dist/commands/resource-view.js +90 -28
  26. package/dist/commands/rules.js +1 -1
  27. package/dist/commands/secrets-migrate.js +23 -11
  28. package/dist/commands/secrets.d.ts +20 -0
  29. package/dist/commands/secrets.js +93 -17
  30. package/dist/commands/sessions-resume.d.ts +2 -0
  31. package/dist/commands/sessions-resume.js +209 -0
  32. package/dist/commands/sessions-tail.d.ts +10 -0
  33. package/dist/commands/sessions-tail.js +11 -0
  34. package/dist/commands/sessions.d.ts +73 -1
  35. package/dist/commands/sessions.js +273 -53
  36. package/dist/commands/status.d.ts +12 -0
  37. package/dist/commands/status.js +81 -0
  38. package/dist/commands/teams.js +70 -6
  39. package/dist/commands/versions.js +2 -1
  40. package/dist/commands/view.d.ts +42 -0
  41. package/dist/commands/view.js +213 -83
  42. package/dist/commands/wallet.d.ts +6 -0
  43. package/dist/commands/wallet.js +22 -5
  44. package/dist/index.js +61 -32
  45. package/dist/lib/acp/harnesses.d.ts +1 -1
  46. package/dist/lib/acp/harnesses.js +2 -2
  47. package/dist/lib/agents.d.ts +12 -0
  48. package/dist/lib/agents.js +145 -41
  49. package/dist/lib/browser/cdp.js +5 -1
  50. package/dist/lib/browser/chrome.js +20 -0
  51. package/dist/lib/browser/drivers/ssh.d.ts +27 -0
  52. package/dist/lib/browser/drivers/ssh.js +94 -19
  53. package/dist/lib/browser/ipc.d.ts +3 -0
  54. package/dist/lib/browser/ipc.js +13 -9
  55. package/dist/lib/browser/service.js +71 -15
  56. package/dist/lib/daemon.d.ts +35 -0
  57. package/dist/lib/daemon.js +33 -5
  58. package/dist/lib/devices/connect.d.ts +3 -2
  59. package/dist/lib/devices/connect.js +5 -3
  60. package/dist/lib/doctor-diff.js +29 -2
  61. package/dist/lib/drift-sync.d.ts +43 -0
  62. package/dist/lib/drift-sync.js +179 -0
  63. package/dist/lib/events.d.ts +9 -2
  64. package/dist/lib/events.js +70 -11
  65. package/dist/lib/exec.d.ts +15 -0
  66. package/dist/lib/exec.js +43 -11
  67. package/dist/lib/hooks.js +8 -2
  68. package/dist/lib/hosts/dispatch.js +6 -7
  69. package/dist/lib/hosts/logs.d.ts +16 -0
  70. package/dist/lib/hosts/logs.js +45 -0
  71. package/dist/lib/hosts/progress.d.ts +66 -0
  72. package/dist/lib/hosts/progress.js +125 -17
  73. package/dist/lib/hosts/ready.d.ts +23 -2
  74. package/dist/lib/hosts/ready.js +35 -13
  75. package/dist/lib/hosts/reconcile.d.ts +53 -0
  76. package/dist/lib/hosts/reconcile.js +81 -0
  77. package/dist/lib/hosts/tasks.d.ts +8 -0
  78. package/dist/lib/hosts/tasks.js +14 -0
  79. package/dist/lib/permissions.d.ts +4 -0
  80. package/dist/lib/permissions.js +35 -0
  81. package/dist/lib/picker.d.ts +24 -0
  82. package/dist/lib/picker.js +124 -0
  83. package/dist/lib/platform/winpath.d.ts +31 -2
  84. package/dist/lib/platform/winpath.js +133 -24
  85. package/dist/lib/plugin-marketplace.d.ts +30 -0
  86. package/dist/lib/plugin-marketplace.js +215 -2
  87. package/dist/lib/plugins.d.ts +5 -0
  88. package/dist/lib/plugins.js +45 -4
  89. package/dist/lib/pwsh.d.ts +11 -0
  90. package/dist/lib/pwsh.js +13 -0
  91. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  92. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  93. package/dist/lib/secrets/agent.d.ts +42 -1
  94. package/dist/lib/secrets/agent.js +89 -11
  95. package/dist/lib/secrets/bundles.js +40 -9
  96. package/dist/lib/secrets/filestore.js +31 -1
  97. package/dist/lib/secrets/index.d.ts +33 -1
  98. package/dist/lib/secrets/index.js +90 -9
  99. package/dist/lib/secrets/windows.d.ts +74 -0
  100. package/dist/lib/secrets/windows.js +440 -0
  101. package/dist/lib/session/active.d.ts +7 -0
  102. package/dist/lib/session/active.js +11 -3
  103. package/dist/lib/session/discover.js +94 -0
  104. package/dist/lib/session/parse.d.ts +6 -0
  105. package/dist/lib/session/parse.js +283 -2
  106. package/dist/lib/session/pid-registry.d.ts +21 -0
  107. package/dist/lib/session/pid-registry.js +80 -0
  108. package/dist/lib/session/remote-active.d.ts +26 -0
  109. package/dist/lib/session/remote-active.js +141 -0
  110. package/dist/lib/session/remote.js +2 -6
  111. package/dist/lib/session/render.js +1 -1
  112. package/dist/lib/session/sync/config.d.ts +8 -0
  113. package/dist/lib/session/sync/config.js +11 -2
  114. package/dist/lib/session/types.d.ts +1 -1
  115. package/dist/lib/session/types.js +1 -1
  116. package/dist/lib/shims.d.ts +30 -2
  117. package/dist/lib/shims.js +107 -50
  118. package/dist/lib/ssh-exec.d.ts +26 -3
  119. package/dist/lib/ssh-exec.js +45 -3
  120. package/dist/lib/ssh-tunnel.d.ts +24 -5
  121. package/dist/lib/ssh-tunnel.js +60 -62
  122. package/dist/lib/staleness/writers/hooks.js +1 -1
  123. package/dist/lib/startup/command-registry.d.ts +3 -0
  124. package/dist/lib/startup/command-registry.js +6 -0
  125. package/dist/lib/sync-status.d.ts +102 -0
  126. package/dist/lib/sync-status.js +135 -0
  127. package/dist/lib/teams/agents.d.ts +24 -0
  128. package/dist/lib/teams/agents.js +30 -1
  129. package/dist/lib/teams/registry.js +25 -9
  130. package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
  131. package/dist/lib/terminal/backends/ghostty.js +69 -0
  132. package/dist/lib/terminal/backends/index.d.ts +18 -0
  133. package/dist/lib/terminal/backends/index.js +29 -0
  134. package/dist/lib/terminal/backends/iterm.d.ts +6 -0
  135. package/dist/lib/terminal/backends/iterm.js +62 -0
  136. package/dist/lib/terminal/backends/tmux.d.ts +14 -0
  137. package/dist/lib/terminal/backends/tmux.js +23 -0
  138. package/dist/lib/terminal/engine.d.ts +39 -0
  139. package/dist/lib/terminal/engine.js +54 -0
  140. package/dist/lib/terminal/index.d.ts +14 -0
  141. package/dist/lib/terminal/index.js +5 -0
  142. package/dist/lib/terminal/policy.d.ts +11 -0
  143. package/dist/lib/terminal/policy.js +11 -0
  144. package/dist/lib/terminal/quote.d.ts +11 -0
  145. package/dist/lib/terminal/quote.js +13 -0
  146. package/dist/lib/terminal/shell.d.ts +6 -0
  147. package/dist/lib/terminal/shell.js +23 -0
  148. package/dist/lib/terminal/transport.d.ts +15 -0
  149. package/dist/lib/terminal/transport.js +40 -0
  150. package/dist/lib/terminal/types.d.ts +59 -0
  151. package/dist/lib/terminal/types.js +13 -0
  152. package/dist/lib/types.d.ts +27 -1
  153. package/dist/lib/usage.d.ts +30 -0
  154. package/dist/lib/usage.js +159 -2
  155. package/dist/lib/versions.d.ts +7 -3
  156. package/dist/lib/versions.js +10 -5
  157. package/dist/lib/whats-new.d.ts +9 -0
  158. package/dist/lib/whats-new.js +35 -0
  159. package/package.json +1 -1
@@ -0,0 +1,440 @@
1
+ /**
2
+ * Windows secret storage via Windows Credential Manager (wincred).
3
+ *
4
+ * Primary backend: the Credential Manager `advapi32` API (CredReadW /
5
+ * CredWriteW / CredDeleteW / CredEnumerateW), reached through a static
6
+ * PowerShell script that P/Invokes the C# shim below. PowerShell (Windows
7
+ * PowerShell 5.1) ships with every supported Windows, so there is no separate
8
+ * install step. Items are stored as CRED_TYPE_GENERIC with
9
+ * CRED_PERSIST_LOCAL_MACHINE — device-local, matching the biometry-bound model
10
+ * on macOS (src/lib/secrets/index.ts).
11
+ *
12
+ * Zero injection surface: the PS script is a single STATIC constant. All
13
+ * dynamic data rides in the child ENV (target name, list prefix) or STDIN (the
14
+ * secret value) — nothing is string-interpolated into the script. The child is
15
+ * spawned with a spawnSync ARGV ARRAY (`-EncodedCommand <base64>`), never a
16
+ * shell string.
17
+ *
18
+ * Headless fallback: when Credential Manager is unreachable (no logon session —
19
+ * ERROR_NO_SUCH_LOGON_SESSION 1312 — or powershell.exe missing from PATH), we
20
+ * transparently switch to the AES-256-GCM encrypted-file store in
21
+ * ./filestore.ts, exactly like the Linux locked-collection fallback. The
22
+ * decision is cached per process; one stderr line is emitted the first time.
23
+ *
24
+ * Item names are stored VERBATIM as the credential TargetName
25
+ * (`agents-cli.bundles.<name>` / `agents-cli.secrets.<bundle>.<key>` — the
26
+ * scheme shared with the file store, see ./filestore.ts) so `list` returns item
27
+ * names directly.
28
+ */
29
+ import { spawnSync } from 'child_process';
30
+ import { encodePwshBase64 } from '../pwsh.js';
31
+ import { fileStore, fileDir, fileStoreHasItems, machinePassphraseExists, _resetFileStoreForTest, } from './filestore.js';
32
+ // Re-exported so importers (and tests) can keep reaching these via './windows.js'.
33
+ export { encryptForFallback, decryptForFallback, fileBackend, } from './filestore.js';
34
+ const POWERSHELL = 'powershell.exe';
35
+ /**
36
+ * CRED_MAX_CREDENTIAL_BLOB_SIZE — Credential Manager rejects a generic
37
+ * credential blob larger than 2560 bytes with an opaque CredWrite failure. We
38
+ * guard against it in `set` with a clear message. Only pathologically large
39
+ * bundle metadata could hit this; such an item should live in a file-backed
40
+ * bundle (AGENTS_SECRETS_PASSPHRASE) instead.
41
+ */
42
+ export const CRED_MAX_CREDENTIAL_BLOB_SIZE = 2560;
43
+ /**
44
+ * The static PowerShell driver. Dispatches on $env:AGENTS_CRED_OP; reads the
45
+ * target name from $env:AGENTS_CRED_TARGET, the list prefix from
46
+ * $env:AGENTS_CRED_PREFIX, and (for `set`) the raw secret value from stdin.
47
+ * Nothing dynamic is interpolated into this string.
48
+ *
49
+ * Exit codes: 0 = success, 3 = clean "not found", 1 = error (message on stderr,
50
+ * carrying the Win32 code so the Node side can detect an unavailable store).
51
+ * `get` emits the blob as base64 (dodges PowerShell CRLF/encoding corruption);
52
+ * `list` prints one target name per line.
53
+ */
54
+ const PS_SCRIPT = `
55
+ $ErrorActionPreference = 'Stop'
56
+ Add-Type -TypeDefinition @'
57
+ using System;
58
+ using System.Runtime.InteropServices;
59
+ using System.Collections.Generic;
60
+
61
+ public static class AgentsCred {
62
+ const uint CRED_TYPE_GENERIC = 1;
63
+ const uint CRED_PERSIST_LOCAL_MACHINE = 2;
64
+ const int ERROR_NOT_FOUND = 1168;
65
+
66
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
67
+ struct CREDENTIAL {
68
+ public uint Flags;
69
+ public uint Type;
70
+ public IntPtr TargetName;
71
+ public IntPtr Comment;
72
+ public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
73
+ public uint CredentialBlobSize;
74
+ public IntPtr CredentialBlob;
75
+ public uint Persist;
76
+ public uint AttributeCount;
77
+ public IntPtr Attributes;
78
+ public IntPtr TargetAlias;
79
+ public IntPtr UserName;
80
+ }
81
+
82
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
83
+ static extern bool CredReadW(string target, uint type, uint flags, out IntPtr cred);
84
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
85
+ static extern bool CredWriteW(ref CREDENTIAL cred, uint flags);
86
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
87
+ static extern bool CredDeleteW(string target, uint type, uint flags);
88
+ [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
89
+ static extern bool CredEnumerateW(string filter, uint flags, out uint count, out IntPtr creds);
90
+ [DllImport("advapi32.dll", SetLastError = false)]
91
+ static extern void CredFree(IntPtr buffer);
92
+
93
+ public static bool Has(string target) {
94
+ IntPtr p;
95
+ if (CredReadW(target, CRED_TYPE_GENERIC, 0, out p)) { CredFree(p); return true; }
96
+ int err = Marshal.GetLastWin32Error();
97
+ if (err == ERROR_NOT_FOUND) return false;
98
+ throw new Exception("CredMan error " + err);
99
+ }
100
+
101
+ public static byte[] Get(string target) {
102
+ IntPtr p;
103
+ if (!CredReadW(target, CRED_TYPE_GENERIC, 0, out p)) {
104
+ int err = Marshal.GetLastWin32Error();
105
+ if (err == ERROR_NOT_FOUND) throw new Exception("NOTFOUND");
106
+ throw new Exception("CredMan error " + err);
107
+ }
108
+ try {
109
+ CREDENTIAL cred = (CREDENTIAL)Marshal.PtrToStructure(p, typeof(CREDENTIAL));
110
+ byte[] blob = new byte[cred.CredentialBlobSize];
111
+ if (cred.CredentialBlobSize > 0)
112
+ Marshal.Copy(cred.CredentialBlob, blob, 0, (int)cred.CredentialBlobSize);
113
+ return blob;
114
+ } finally { CredFree(p); }
115
+ }
116
+
117
+ public static void Set(string target, byte[] blob) {
118
+ CREDENTIAL cred = new CREDENTIAL();
119
+ cred.Type = CRED_TYPE_GENERIC;
120
+ cred.TargetName = Marshal.StringToCoTaskMemUni(target);
121
+ cred.CredentialBlobSize = (uint)blob.Length;
122
+ cred.CredentialBlob = (blob.Length > 0) ? Marshal.AllocCoTaskMem(blob.Length) : IntPtr.Zero;
123
+ if (blob.Length > 0) Marshal.Copy(blob, 0, cred.CredentialBlob, blob.Length);
124
+ cred.Persist = CRED_PERSIST_LOCAL_MACHINE;
125
+ cred.UserName = Marshal.StringToCoTaskMemUni(Environment.UserName);
126
+ try {
127
+ if (!CredWriteW(ref cred, 0)) {
128
+ int err = Marshal.GetLastWin32Error();
129
+ throw new Exception("CredMan error " + err);
130
+ }
131
+ } finally {
132
+ Marshal.FreeCoTaskMem(cred.TargetName);
133
+ if (cred.CredentialBlob != IntPtr.Zero) Marshal.FreeCoTaskMem(cred.CredentialBlob);
134
+ Marshal.FreeCoTaskMem(cred.UserName);
135
+ }
136
+ }
137
+
138
+ public static bool Delete(string target) {
139
+ if (CredDeleteW(target, CRED_TYPE_GENERIC, 0)) return true;
140
+ int err = Marshal.GetLastWin32Error();
141
+ if (err == ERROR_NOT_FOUND) return false;
142
+ throw new Exception("CredMan error " + err);
143
+ }
144
+
145
+ public static List<string> List(string filter) {
146
+ uint count;
147
+ IntPtr credsPtr;
148
+ List<string> results = new List<string>();
149
+ string f = string.IsNullOrEmpty(filter) ? null : filter;
150
+ if (!CredEnumerateW(f, 0, out count, out credsPtr)) {
151
+ int err = Marshal.GetLastWin32Error();
152
+ if (err == ERROR_NOT_FOUND) return results;
153
+ throw new Exception("CredMan error " + err);
154
+ }
155
+ try {
156
+ for (int i = 0; i < count; i++) {
157
+ IntPtr credPtr = Marshal.ReadIntPtr(credsPtr, i * IntPtr.Size);
158
+ CREDENTIAL cred = (CREDENTIAL)Marshal.PtrToStructure(credPtr, typeof(CREDENTIAL));
159
+ if (cred.TargetName != IntPtr.Zero)
160
+ results.Add(Marshal.PtrToStringUni(cred.TargetName));
161
+ }
162
+ } finally { CredFree(credsPtr); }
163
+ return results;
164
+ }
165
+ }
166
+ '@
167
+
168
+ try {
169
+ $op = $env:AGENTS_CRED_OP
170
+ $target = $env:AGENTS_CRED_TARGET
171
+ switch ($op) {
172
+ 'has' {
173
+ if ([AgentsCred]::Has($target)) { exit 0 } else { exit 3 }
174
+ }
175
+ 'get' {
176
+ $blob = [AgentsCred]::Get($target)
177
+ [Console]::Out.Write([Convert]::ToBase64String($blob))
178
+ exit 0
179
+ }
180
+ 'set' {
181
+ $value = [Console]::In.ReadToEnd()
182
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($value)
183
+ [AgentsCred]::Set($target, $bytes)
184
+ exit 0
185
+ }
186
+ 'delete' {
187
+ if ([AgentsCred]::Delete($target)) { exit 0 } else { exit 3 }
188
+ }
189
+ 'list' {
190
+ $prefix = $env:AGENTS_CRED_PREFIX
191
+ $filter = ''
192
+ if (-not [string]::IsNullOrEmpty($prefix)) { $filter = $prefix + '*' }
193
+ foreach ($n in [AgentsCred]::List($filter)) { [Console]::Out.WriteLine($n) }
194
+ exit 0
195
+ }
196
+ default {
197
+ [Console]::Error.Write('unknown op: ' + $op)
198
+ exit 1
199
+ }
200
+ }
201
+ } catch {
202
+ $m = $_.Exception.Message
203
+ if ($m -match 'NOTFOUND') { exit 3 }
204
+ [Console]::Error.Write($m)
205
+ exit 1
206
+ }
207
+ `;
208
+ const ENCODED_SCRIPT = encodePwshBase64(PS_SCRIPT);
209
+ function runCred(op, opts) {
210
+ const env = { ...process.env, AGENTS_CRED_OP: op };
211
+ if (opts.target !== undefined)
212
+ env.AGENTS_CRED_TARGET = opts.target;
213
+ if (opts.prefix !== undefined)
214
+ env.AGENTS_CRED_PREFIX = opts.prefix;
215
+ // ARGV ARRAY — never a shell string. -EncodedCommand rides base64 of the
216
+ // UTF-16LE script with zero escaping hazards.
217
+ const result = spawnSync(POWERSHELL, ['-NoProfile', '-NonInteractive', '-EncodedCommand', ENCODED_SCRIPT], {
218
+ env,
219
+ input: opts.input,
220
+ stdio: ['pipe', 'pipe', 'pipe'],
221
+ maxBuffer: 16 * 1024 * 1024,
222
+ });
223
+ return {
224
+ status: result.status,
225
+ stdout: result.stdout?.toString() ?? '',
226
+ stderr: result.stderr?.toString() ?? '',
227
+ spawnError: !!result.error,
228
+ };
229
+ }
230
+ // ---------- powershell availability ----------
231
+ function powershellAvailable() {
232
+ const result = spawnSync(POWERSHELL, ['-NoProfile', '-NonInteractive', '-Command', 'exit 0'], {
233
+ stdio: ['ignore', 'ignore', 'ignore'],
234
+ });
235
+ return !result.error && result.status === 0;
236
+ }
237
+ let checkedAvailability = false;
238
+ let isAvailable = false;
239
+ // ---------- file fallback state ----------
240
+ let useFileFallback = false;
241
+ let warnedFallback = false;
242
+ function activateFileFallback() {
243
+ if (useFileFallback)
244
+ return;
245
+ useFileFallback = true;
246
+ if (!warnedFallback) {
247
+ warnedFallback = true;
248
+ process.stderr.write(`[agents] Windows Credential Manager unavailable, using file-based store at ${fileDir()}\n`);
249
+ }
250
+ }
251
+ /**
252
+ * Credential Manager is "unavailable" (as opposed to a plain not-found or a
253
+ * malformed item) when there's no logon session to hold credentials
254
+ * (ERROR_NO_SUCH_LOGON_SESSION 1312 — common under a service account / SSH
255
+ * session with no interactive logon) or powershell.exe can't be spawned at all.
256
+ * Those cases route to the encrypted-file fallback, exactly like the Linux
257
+ * locked-collection error.
258
+ */
259
+ function isCredManUnavailableError(r) {
260
+ if (r.spawnError)
261
+ return true; // powershell.exe not found
262
+ return /\b1312\b/.test(r.stderr) || /NO_SUCH_LOGON_SESSION/i.test(r.stderr);
263
+ }
264
+ /**
265
+ * Decide which backend a given op should use. Activates the file fallback if a
266
+ * previous run already committed to it (encrypted items on disk), or if
267
+ * powershell.exe is missing and a passphrase source exists (explicit
268
+ * AGENTS_SECRETS_PASSPHRASE, a provisioned machine-local key, or a headless
269
+ * context). The disk-items check makes the fallback persistent across the many
270
+ * short-lived `agents secrets ...` Node processes.
271
+ */
272
+ function preflight() {
273
+ if (useFileFallback)
274
+ return 'file';
275
+ if (fileStoreHasItems()) {
276
+ activateFileFallback();
277
+ return 'file';
278
+ }
279
+ if (!checkedAvailability) {
280
+ isAvailable = powershellAvailable();
281
+ checkedAvailability = true;
282
+ }
283
+ if (!isAvailable) {
284
+ if (process.env.AGENTS_SECRETS_PASSPHRASE || machinePassphraseExists() || !process.stdin.isTTY) {
285
+ activateFileFallback();
286
+ return 'file';
287
+ }
288
+ throw new Error('powershell.exe not found on PATH; cannot reach Windows Credential Manager.\n' +
289
+ 'Set AGENTS_SECRETS_PASSPHRASE to use the encrypted-file fallback.');
290
+ }
291
+ return 'credman';
292
+ }
293
+ /**
294
+ * True when secret operations currently route to the encrypted-file store
295
+ * instead of Windows Credential Manager. Mirrors linux.ts:usesFileFallback so
296
+ * `listBundles()` doesn't double-count file-backed bundles under the fallback.
297
+ */
298
+ export function usesFileFallback() {
299
+ try {
300
+ return preflight() === 'file';
301
+ }
302
+ catch {
303
+ return false;
304
+ }
305
+ }
306
+ // ---------- Credential Manager ops with fallback ----------
307
+ export function hasCredManToken(item) {
308
+ if (preflight() === 'file')
309
+ return fileStore.has(item);
310
+ const r = runCred('has', { target: item });
311
+ if (r.status === 0)
312
+ return true;
313
+ if (r.status === 3)
314
+ return false;
315
+ if (isCredManUnavailableError(r)) {
316
+ activateFileFallback();
317
+ return fileStore.has(item);
318
+ }
319
+ return false;
320
+ }
321
+ export function getCredManToken(item) {
322
+ if (preflight() === 'file')
323
+ return fileStore.get(item);
324
+ const r = runCred('get', { target: item });
325
+ if (r.status === 0) {
326
+ // stdout is base64 of the raw UTF-8 blob (dodges PowerShell encoding corruption).
327
+ return Buffer.from(r.stdout.trim(), 'base64').toString('utf8');
328
+ }
329
+ if (r.status === 3)
330
+ throw new Error(`Secret '${item}' not found in Credential Manager.`);
331
+ if (isCredManUnavailableError(r)) {
332
+ activateFileFallback();
333
+ return fileStore.get(item);
334
+ }
335
+ throw new Error(`Failed to read secret '${item}': ${r.stderr.trim() || 'unknown error'}`);
336
+ }
337
+ export function setCredManToken(item, value) {
338
+ if (!value || !value.trim())
339
+ throw new Error('Secret value is empty.');
340
+ if (preflight() === 'file') {
341
+ fileStore.set(item, value);
342
+ return;
343
+ }
344
+ const byteLen = Buffer.byteLength(value, 'utf8');
345
+ if (byteLen > CRED_MAX_CREDENTIAL_BLOB_SIZE) {
346
+ throw new Error(`Secret '${item}' is ${byteLen} bytes, exceeding the Windows Credential Manager limit of ` +
347
+ `${CRED_MAX_CREDENTIAL_BLOB_SIZE} bytes (CRED_MAX_CREDENTIAL_BLOB_SIZE). ` +
348
+ 'Use a file-backed bundle (set AGENTS_SECRETS_PASSPHRASE) for values this large.');
349
+ }
350
+ const r = runCred('set', { target: item, input: value });
351
+ if (r.status === 0)
352
+ return;
353
+ if (isCredManUnavailableError(r)) {
354
+ activateFileFallback();
355
+ fileStore.set(item, value);
356
+ return;
357
+ }
358
+ throw new Error(`Failed to store secret '${item}': ${r.stderr.trim() || 'unknown error'}`);
359
+ }
360
+ export function deleteCredManToken(item) {
361
+ if (preflight() === 'file')
362
+ return fileStore.delete(item);
363
+ const r = runCred('delete', { target: item });
364
+ if (r.status === 0)
365
+ return true;
366
+ if (r.status === 3)
367
+ return false;
368
+ if (isCredManUnavailableError(r)) {
369
+ activateFileFallback();
370
+ return fileStore.delete(item);
371
+ }
372
+ return false;
373
+ }
374
+ export function listCredManItems(prefix) {
375
+ if (preflight() === 'file')
376
+ return fileStore.list(prefix);
377
+ const r = runCred('list', { prefix });
378
+ if (r.status === 0)
379
+ return parseWindowsCredList(r.stdout, prefix);
380
+ if (isCredManUnavailableError(r)) {
381
+ activateFileFallback();
382
+ return fileStore.list(prefix);
383
+ }
384
+ return [];
385
+ }
386
+ /**
387
+ * Parse the target names printed by the `list` op (one per line), keeping only
388
+ * those starting with `prefix` and deduping. Same contract as
389
+ * parseSecretToolItems (linux.ts). Exported for tests.
390
+ */
391
+ export function parseWindowsCredList(output, prefix) {
392
+ const items = output
393
+ .split(/\r?\n/)
394
+ .map((s) => s.trim())
395
+ .filter((s) => s.length > 0)
396
+ .filter((s) => s.startsWith(prefix));
397
+ return [...new Set(items)]; // dedupe
398
+ }
399
+ /**
400
+ * KeychainBackend implementation for Windows. Routes through Windows Credential
401
+ * Manager (via PowerShell P/Invoke) with a transparent encrypted-file fallback
402
+ * when the credential store is unreachable.
403
+ */
404
+ export const windowsBackend = {
405
+ has(item) {
406
+ return hasCredManToken(item);
407
+ },
408
+ get(item) {
409
+ return getCredManToken(item);
410
+ },
411
+ set(item, value) {
412
+ setCredManToken(item, value);
413
+ },
414
+ delete(item) {
415
+ return deleteCredManToken(item);
416
+ },
417
+ list(prefix) {
418
+ return listCredManItems(prefix);
419
+ },
420
+ };
421
+ /**
422
+ * Test-only: reset module state so independent test cases don't bleed
423
+ * availability / fallback decisions across each other. Pass `forceAvailable` to
424
+ * pin the powershell-availability probe (skips the real spawn); pass `fileDir`
425
+ * to redirect the encrypted-file store to a temp dir. File-store state lives in
426
+ * ./filestore.ts and is reset there.
427
+ */
428
+ export function _resetForTest(opts = {}) {
429
+ _resetFileStoreForTest({ fileDir: opts.fileDir ?? null, passphrase: opts.passphrase ?? null });
430
+ useFileFallback = opts.forceFileFallback ?? false;
431
+ warnedFallback = false;
432
+ if (opts.forceAvailable === undefined || opts.forceAvailable === null) {
433
+ checkedAvailability = false;
434
+ isAvailable = false;
435
+ }
436
+ else {
437
+ checkedAvailability = true;
438
+ isAvailable = opts.forceAvailable;
439
+ }
440
+ }
@@ -38,6 +38,13 @@ export interface ActiveSession {
38
38
  * Absent for cloud sessions (no local pid) and any pid whose env is unreadable.
39
39
  */
40
40
  provenance?: SessionProvenance;
41
+ /**
42
+ * The machine this session runs on, as a normalized device id (machineId()
43
+ * form). Set when merging cross-machine results so the grouped `--active`
44
+ * view can bucket by computer. Absent for a purely local query (the renderer
45
+ * falls back to provenance.host, then the local machine).
46
+ */
47
+ machine?: string;
41
48
  teamName?: string;
42
49
  agentId?: string;
43
50
  cloudProvider?: string;
@@ -24,6 +24,7 @@ import { promisify } from 'util';
24
24
  import { listActiveTasks } from '../cloud/store.js';
25
25
  import { AgentManager } from '../teams/agents.js';
26
26
  import { getTerminalsDir } from '../state.js';
27
+ import { readPidSessionEntry, prunePidSessionRegistry } from './pid-registry.js';
27
28
  import { buildClaudeLabelMap } from './discover.js';
28
29
  import { latestSessionFileForCwd } from './db.js';
29
30
  import { extractSessionTopic } from './prompt.js';
@@ -499,8 +500,13 @@ export async function listUnattributedActive(attributed) {
499
500
  const out = [];
500
501
  for (let i = 0; i < candidates.length; i++) {
501
502
  const { pid, kind } = candidates[i];
502
- const cwd = cwds[i];
503
- const sessionFile = findSessionFileForKind(kind, cwd);
503
+ // The per-pid registry (written by `ag run`) gives the EXACT session id this
504
+ // pid was launched with — so N agents in one cwd resolve to N distinct
505
+ // sessions instead of all collapsing onto the newest .jsonl. Absent (agent
506
+ // not launched via `ag run`, or one that takes no session id) → heuristic.
507
+ const entry = readPidSessionEntry(pid);
508
+ const cwd = cwds[i] ?? entry?.cwd ?? undefined;
509
+ const sessionFile = findSessionFileForKind(kind, cwd, entry?.sessionId);
504
510
  const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
505
511
  const host = detectHost(pid, procByPid);
506
512
  const context = host && UI_HOSTS.has(host) ? 'terminal' : 'headless';
@@ -511,11 +517,13 @@ export async function listUnattributedActive(attributed) {
511
517
  host,
512
518
  pid,
513
519
  cwd,
514
- sessionId: sessionIdFromFile(sessionFile),
520
+ sessionId: entry?.sessionId ?? sessionIdFromFile(sessionFile),
515
521
  topic,
516
522
  sessionFile,
517
523
  }, state, sessionFile));
518
524
  }
525
+ // Housekeeping: drop registry files for pids that have since died.
526
+ prunePidSessionRegistry(isPidAlive);
519
527
  return out;
520
528
  }
521
529
  /**
@@ -20,6 +20,7 @@ import { walkForFiles } from '../fs-walk.js';
20
20
  import { getConfigSymlinkVersion } from '../shims.js';
21
21
  import { SESSION_AGENTS } from './types.js';
22
22
  import { extractSessionTopic } from './prompt.js';
23
+ import { parseAntigravity } from './parse.js';
23
24
  import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand } from './state.js';
24
25
  import { costOfUsage } from '../pricing/index.js';
25
26
  import { getDB, getScanStampByPath, getScanStampsForPaths, recordScans, syncLabels, syncTopics, upsertSessionsBatch, querySessions, countSessions, ftsSearch, tryClaimScan, releaseScan, } from './db.js';
@@ -60,6 +61,7 @@ export async function discoverSessions(options) {
60
61
  case 'claude': return scanClaudeIncremental(onProgress);
61
62
  case 'codex': return scanCodexIncremental(onProgress);
62
63
  case 'gemini': return scanGeminiIncremental(onProgress);
64
+ case 'antigravity': return scanAntigravityIncremental(onProgress);
63
65
  case 'opencode': return scanOpenCodeIncremental();
64
66
  case 'openclaw': return scanOpenClawIncremental();
65
67
  case 'rush': return scanRushIncremental(onProgress);
@@ -864,6 +866,98 @@ function buildGeminiProjectMap() {
864
866
  return map;
865
867
  }
866
868
  // ---------------------------------------------------------------------------
869
+ // Antigravity
870
+ //
871
+ // Antigravity stores one SQLite DB per conversation at
872
+ // ~/.gemini/antigravity-cli/conversations/<trajectory-uuid>.db. The filename
873
+ // (minus .db) is the canonical session id. Each DB is stat'd against the ledger;
874
+ // only changed DBs are re-parsed (via parseAntigravity, which shells out to
875
+ // sqlite3). Tool count doubles as the message count; the toolSummary of the
876
+ // first tool call becomes the topic, and any run_command's Cwd fills in cwd.
877
+ // ---------------------------------------------------------------------------
878
+ /** Incrementally re-scan changed Antigravity conversation DBs and upsert into the DB. */
879
+ async function scanAntigravityIncremental(onProgress) {
880
+ const currentVersion = await getCurrentAgentVersion('antigravity');
881
+ const filePaths = [];
882
+ const seenPaths = new Set();
883
+ for (const conversationsDir of getAgentSessionDirs('antigravity', 'conversations')) {
884
+ let files;
885
+ try {
886
+ files = fs.readdirSync(conversationsDir).filter(f => f.endsWith('.db'));
887
+ }
888
+ catch {
889
+ continue;
890
+ }
891
+ for (const file of files) {
892
+ const fp = path.join(conversationsDir, file);
893
+ if (seenPaths.has(fp))
894
+ continue;
895
+ seenPaths.add(fp);
896
+ filePaths.push(fp);
897
+ }
898
+ }
899
+ const changed = filterChangedFiles(filePaths);
900
+ if (changed.length === 0)
901
+ return;
902
+ onProgress?.({ agent: 'antigravity', parsed: 0, total: changed.length });
903
+ const entries = [];
904
+ const touched = [];
905
+ const seen = new Set();
906
+ let parsed = 0;
907
+ for (const { filePath, scan } of changed) {
908
+ try {
909
+ const result = readAntigravityMeta(filePath, currentVersion);
910
+ if (result && !seen.has(result.meta.id)) {
911
+ seen.add(result.meta.id);
912
+ entries.push({ meta: result.meta, content: result.content, scan });
913
+ }
914
+ else {
915
+ touched.push({ filePath, scan });
916
+ }
917
+ }
918
+ catch {
919
+ touched.push({ filePath, scan });
920
+ }
921
+ parsed++;
922
+ onProgress?.({ agent: 'antigravity', parsed, total: changed.length });
923
+ }
924
+ upsertSessionsBatch(entries);
925
+ recordScans(touched);
926
+ }
927
+ /** Parse a single Antigravity conversation DB to extract session metadata. */
928
+ function readAntigravityMeta(filePath, currentVersion) {
929
+ const sessionId = path.basename(filePath).replace(/\.db$/, '');
930
+ if (!sessionId)
931
+ return null;
932
+ const events = parseAntigravity(filePath);
933
+ // cwd: first run_command carries the working directory in its Cwd arg.
934
+ let cwd;
935
+ const contentParts = [];
936
+ for (const e of events) {
937
+ if (!cwd && typeof e.args?.Cwd === 'string' && e.args.Cwd)
938
+ cwd = e.args.Cwd;
939
+ if (e.content)
940
+ contentParts.push(e.content);
941
+ }
942
+ const normalizedCwd = cwd ? normalizeCwd(cwd) : undefined;
943
+ // Topic: the first tool's human summary is a decent one-line label.
944
+ const topic = events.find(e => e.content)?.content;
945
+ const stat = safeStatSync(filePath);
946
+ const meta = {
947
+ id: sessionId,
948
+ shortId: sessionId.slice(0, 8),
949
+ agent: 'antigravity',
950
+ timestamp: stat ? stat.mtime.toISOString() : new Date().toISOString(),
951
+ project: normalizedCwd ? path.basename(normalizedCwd) : undefined,
952
+ cwd: normalizedCwd,
953
+ filePath,
954
+ version: resolveSessionVersion('antigravity', filePath, undefined, currentVersion),
955
+ topic: topic ? topic.slice(0, 120) : undefined,
956
+ messageCount: events.length,
957
+ };
958
+ return { meta, content: contentParts.join('\n') };
959
+ }
960
+ // ---------------------------------------------------------------------------
867
961
  // OpenCode
868
962
  // ---------------------------------------------------------------------------
869
963
  const OPENCODE_DB = path.join(HOME, '.local', 'share', 'opencode', 'opencode.db');
@@ -50,6 +50,12 @@ export declare function parseCodex(filePath: string): SessionEvent[];
50
50
  export declare function parseCodexContent(content: string): SessionEvent[];
51
51
  /** Parse a Gemini JSON session file into normalized events. */
52
52
  export declare function parseGemini(filePath: string): SessionEvent[];
53
+ /**
54
+ * Parse an Antigravity conversation SQLite DB into normalized tool_use events.
55
+ * Deduped by the tool-call id so each tool appears once (Antigravity writes a
56
+ * request step and a completion step that share the id).
57
+ */
58
+ export declare function parseAntigravity(dbPath: string): SessionEvent[];
53
59
  /**
54
60
  * Parse an OpenCode session from its SQLite database.
55
61
  * filePath format: "/path/to/opencode.db#session_id"