@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.
- package/CHANGELOG.md +29 -0
- package/README.md +32 -2
- package/dist/commands/computer-actions.d.ts +15 -0
- package/dist/commands/computer-actions.js +120 -22
- package/dist/commands/computer.d.ts +23 -0
- package/dist/commands/computer.js +45 -3
- package/dist/commands/cost.js +17 -12
- package/dist/commands/doctor.d.ts +11 -0
- package/dist/commands/doctor.js +108 -14
- package/dist/commands/events.d.ts +16 -0
- package/dist/commands/events.js +173 -0
- package/dist/commands/exec.js +6 -1
- package/dist/commands/hosts.js +28 -24
- package/dist/commands/import.js +1 -1
- package/dist/commands/inspect.d.ts +2 -0
- package/dist/commands/inspect.js +54 -7
- package/dist/commands/logs.d.ts +17 -0
- package/dist/commands/logs.js +139 -0
- package/dist/commands/models.d.ts +3 -0
- package/dist/commands/models.js +27 -8
- package/dist/commands/permissions.js +9 -2
- package/dist/commands/repo.d.ts +34 -0
- package/dist/commands/repo.js +243 -65
- package/dist/commands/resource-view.d.ts +20 -0
- package/dist/commands/resource-view.js +90 -28
- package/dist/commands/rules.js +1 -1
- package/dist/commands/secrets-migrate.js +23 -11
- package/dist/commands/secrets.d.ts +20 -0
- package/dist/commands/secrets.js +93 -17
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +209 -0
- package/dist/commands/sessions-tail.d.ts +10 -0
- package/dist/commands/sessions-tail.js +11 -0
- package/dist/commands/sessions.d.ts +73 -1
- package/dist/commands/sessions.js +273 -53
- package/dist/commands/status.d.ts +12 -0
- package/dist/commands/status.js +81 -0
- package/dist/commands/teams.js +70 -6
- package/dist/commands/versions.js +2 -1
- package/dist/commands/view.d.ts +42 -0
- package/dist/commands/view.js +213 -83
- package/dist/commands/wallet.d.ts +6 -0
- package/dist/commands/wallet.js +22 -5
- package/dist/index.js +61 -32
- package/dist/lib/acp/harnesses.d.ts +1 -1
- package/dist/lib/acp/harnesses.js +2 -2
- package/dist/lib/agents.d.ts +12 -0
- package/dist/lib/agents.js +145 -41
- package/dist/lib/browser/cdp.js +5 -1
- package/dist/lib/browser/chrome.js +20 -0
- package/dist/lib/browser/drivers/ssh.d.ts +27 -0
- package/dist/lib/browser/drivers/ssh.js +94 -19
- package/dist/lib/browser/ipc.d.ts +3 -0
- package/dist/lib/browser/ipc.js +13 -9
- package/dist/lib/browser/service.js +71 -15
- package/dist/lib/daemon.d.ts +35 -0
- package/dist/lib/daemon.js +33 -5
- package/dist/lib/devices/connect.d.ts +3 -2
- package/dist/lib/devices/connect.js +5 -3
- package/dist/lib/doctor-diff.js +29 -2
- package/dist/lib/drift-sync.d.ts +43 -0
- package/dist/lib/drift-sync.js +179 -0
- package/dist/lib/events.d.ts +9 -2
- package/dist/lib/events.js +70 -11
- package/dist/lib/exec.d.ts +15 -0
- package/dist/lib/exec.js +43 -11
- package/dist/lib/hooks.js +8 -2
- package/dist/lib/hosts/dispatch.js +6 -7
- package/dist/lib/hosts/logs.d.ts +16 -0
- package/dist/lib/hosts/logs.js +45 -0
- package/dist/lib/hosts/progress.d.ts +66 -0
- package/dist/lib/hosts/progress.js +125 -17
- package/dist/lib/hosts/ready.d.ts +23 -2
- package/dist/lib/hosts/ready.js +35 -13
- package/dist/lib/hosts/reconcile.d.ts +53 -0
- package/dist/lib/hosts/reconcile.js +81 -0
- package/dist/lib/hosts/tasks.d.ts +8 -0
- package/dist/lib/hosts/tasks.js +14 -0
- package/dist/lib/permissions.d.ts +4 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/picker.d.ts +24 -0
- package/dist/lib/picker.js +124 -0
- package/dist/lib/platform/winpath.d.ts +31 -2
- package/dist/lib/platform/winpath.js +133 -24
- package/dist/lib/plugin-marketplace.d.ts +30 -0
- package/dist/lib/plugin-marketplace.js +215 -2
- package/dist/lib/plugins.d.ts +5 -0
- package/dist/lib/plugins.js +45 -4
- package/dist/lib/pwsh.d.ts +11 -0
- package/dist/lib/pwsh.js +13 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +42 -1
- package/dist/lib/secrets/agent.js +89 -11
- package/dist/lib/secrets/bundles.js +40 -9
- package/dist/lib/secrets/filestore.js +31 -1
- package/dist/lib/secrets/index.d.ts +33 -1
- package/dist/lib/secrets/index.js +90 -9
- package/dist/lib/secrets/windows.d.ts +74 -0
- package/dist/lib/secrets/windows.js +440 -0
- package/dist/lib/session/active.d.ts +7 -0
- package/dist/lib/session/active.js +11 -3
- package/dist/lib/session/discover.js +94 -0
- package/dist/lib/session/parse.d.ts +6 -0
- package/dist/lib/session/parse.js +283 -2
- package/dist/lib/session/pid-registry.d.ts +21 -0
- package/dist/lib/session/pid-registry.js +80 -0
- package/dist/lib/session/remote-active.d.ts +26 -0
- package/dist/lib/session/remote-active.js +141 -0
- package/dist/lib/session/remote.js +2 -6
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/sync/config.d.ts +8 -0
- package/dist/lib/session/sync/config.js +11 -2
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +30 -2
- package/dist/lib/shims.js +107 -50
- package/dist/lib/ssh-exec.d.ts +26 -3
- package/dist/lib/ssh-exec.js +45 -3
- package/dist/lib/ssh-tunnel.d.ts +24 -5
- package/dist/lib/ssh-tunnel.js +60 -62
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +3 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/sync-status.d.ts +102 -0
- package/dist/lib/sync-status.js +135 -0
- package/dist/lib/teams/agents.d.ts +24 -0
- package/dist/lib/teams/agents.js +30 -1
- package/dist/lib/teams/registry.js +25 -9
- package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
- package/dist/lib/terminal/backends/ghostty.js +69 -0
- package/dist/lib/terminal/backends/index.d.ts +18 -0
- package/dist/lib/terminal/backends/index.js +29 -0
- package/dist/lib/terminal/backends/iterm.d.ts +6 -0
- package/dist/lib/terminal/backends/iterm.js +62 -0
- package/dist/lib/terminal/backends/tmux.d.ts +14 -0
- package/dist/lib/terminal/backends/tmux.js +23 -0
- package/dist/lib/terminal/engine.d.ts +39 -0
- package/dist/lib/terminal/engine.js +54 -0
- package/dist/lib/terminal/index.d.ts +14 -0
- package/dist/lib/terminal/index.js +5 -0
- package/dist/lib/terminal/policy.d.ts +11 -0
- package/dist/lib/terminal/policy.js +11 -0
- package/dist/lib/terminal/quote.d.ts +11 -0
- package/dist/lib/terminal/quote.js +13 -0
- package/dist/lib/terminal/shell.d.ts +6 -0
- package/dist/lib/terminal/shell.js +23 -0
- package/dist/lib/terminal/transport.d.ts +15 -0
- package/dist/lib/terminal/transport.js +40 -0
- package/dist/lib/terminal/types.d.ts +59 -0
- package/dist/lib/terminal/types.js +13 -0
- package/dist/lib/types.d.ts +27 -1
- package/dist/lib/usage.d.ts +30 -0
- package/dist/lib/usage.js +159 -2
- package/dist/lib/versions.d.ts +7 -3
- package/dist/lib/versions.js +10 -5
- package/dist/lib/whats-new.d.ts +9 -0
- package/dist/lib/whats-new.js +35 -0
- 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
|
-
|
|
503
|
-
|
|
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"
|