@phnx-labs/agents-cli 1.14.7 → 1.15.0
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/README.md +68 -1
- package/dist/commands/beta.js +6 -1
- package/dist/commands/exec.js +9 -2
- package/dist/commands/init.js +10 -0
- package/dist/commands/mcp.js +4 -4
- package/dist/commands/prune.d.ts +0 -20
- package/dist/commands/prune.js +268 -15
- package/dist/commands/teams.js +2 -3
- package/dist/commands/usage.js +6 -0
- package/dist/commands/versions.js +8 -6
- package/dist/lib/browser/chrome.js +1 -1
- package/dist/lib/browser/drivers/ssh.d.ts +1 -0
- package/dist/lib/browser/drivers/ssh.js +23 -2
- package/dist/lib/browser/ipc.js +1 -0
- package/dist/lib/browser/service.d.ts +3 -0
- package/dist/lib/browser/service.js +84 -7
- package/dist/lib/daemon.js +4 -4
- package/dist/lib/events.d.ts +94 -1
- package/dist/lib/events.js +262 -4
- package/dist/lib/exec.js +16 -10
- package/dist/lib/permissions.d.ts +6 -3
- package/dist/lib/permissions.js +38 -34
- package/dist/lib/routines.d.ts +15 -0
- package/dist/lib/routines.js +68 -0
- package/dist/lib/runner.js +9 -5
- package/dist/lib/secrets/index.d.ts +14 -11
- package/dist/lib/secrets/index.js +49 -21
- package/dist/lib/secrets/linux.d.ts +27 -0
- package/dist/lib/secrets/linux.js +161 -0
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +26 -0
- package/dist/lib/usage.d.ts +1 -1
- package/dist/lib/usage.js +13 -46
- package/dist/lib/versions.js +11 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +37 -9
package/dist/lib/runner.js
CHANGED
|
@@ -14,7 +14,7 @@ import { resolveJobPrompt, parseTimeout, writeRunMeta, getRunDir, } from './rout
|
|
|
14
14
|
import { getRunsDir } from './state.js';
|
|
15
15
|
import { prepareJobHome, buildSpawnEnv } from './sandbox.js';
|
|
16
16
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
17
|
-
import {
|
|
17
|
+
import { createTimer, maybeRotate, truncate } from './events.js';
|
|
18
18
|
/** CLI command templates per agent, with {prompt} as a placeholder. */
|
|
19
19
|
const AGENT_COMMANDS = {
|
|
20
20
|
claude: ['claude', '-p', '--verbose', '{prompt}', '--output-format', 'stream-json', '--permission-mode', 'plan'],
|
|
@@ -109,11 +109,13 @@ function generateRunId() {
|
|
|
109
109
|
/** Execute a job synchronously (waits for completion or timeout before resolving). */
|
|
110
110
|
export async function executeJob(config) {
|
|
111
111
|
maybeRotate();
|
|
112
|
-
const
|
|
112
|
+
const timer = createTimer('agent.run', {
|
|
113
113
|
agent: config.agent,
|
|
114
114
|
version: config.version,
|
|
115
115
|
jobName: config.name,
|
|
116
116
|
mode: config.mode,
|
|
117
|
+
prompt: truncate(config.prompt, 200),
|
|
118
|
+
schedule: config.schedule,
|
|
117
119
|
});
|
|
118
120
|
const resolvedPrompt = resolveJobPrompt(config);
|
|
119
121
|
const cmd = buildJobCommand(config, resolvedPrompt);
|
|
@@ -146,6 +148,8 @@ export async function executeJob(config) {
|
|
|
146
148
|
detached: true,
|
|
147
149
|
env: spawnEnv,
|
|
148
150
|
});
|
|
151
|
+
// Mark startup time (time from function call to process spawn)
|
|
152
|
+
timer.mark('startup');
|
|
149
153
|
meta.pid = child.pid || null;
|
|
150
154
|
writeRunMeta(meta);
|
|
151
155
|
let settled = false;
|
|
@@ -168,7 +172,7 @@ export async function executeJob(config) {
|
|
|
168
172
|
meta.status = 'timeout';
|
|
169
173
|
meta.completedAt = new Date().toISOString();
|
|
170
174
|
writeRunMeta(meta);
|
|
171
|
-
|
|
175
|
+
timer.end({ status: 'timeout', runId });
|
|
172
176
|
const reportPath = extractAndSaveReport(stdoutPath, config.agent, runDir);
|
|
173
177
|
resolve({ meta, reportPath });
|
|
174
178
|
}, timeoutMs);
|
|
@@ -185,7 +189,7 @@ export async function executeJob(config) {
|
|
|
185
189
|
meta.status = code === 0 ? 'completed' : 'failed';
|
|
186
190
|
meta.completedAt = new Date().toISOString();
|
|
187
191
|
writeRunMeta(meta);
|
|
188
|
-
|
|
192
|
+
timer.end({ status: meta.status, exitCode: code ?? undefined, runId });
|
|
189
193
|
const reportPath = extractAndSaveReport(stdoutPath, config.agent, runDir);
|
|
190
194
|
resolve({ meta, reportPath });
|
|
191
195
|
});
|
|
@@ -201,7 +205,7 @@ export async function executeJob(config) {
|
|
|
201
205
|
meta.status = 'failed';
|
|
202
206
|
meta.completedAt = new Date().toISOString();
|
|
203
207
|
writeRunMeta(meta);
|
|
204
|
-
|
|
208
|
+
timer.end({ status: 'failed', error: err.message, runId });
|
|
205
209
|
resolve({ meta, reportPath: null });
|
|
206
210
|
});
|
|
207
211
|
child.unref();
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Cross-platform secure credential storage.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* macOS: Uses Keychain via signed Swift helper (AgentsKeychain.app) or `security` CLI.
|
|
5
|
+
* Linux: Uses libsecret (GNOME Keyring) via `secret-tool` CLI.
|
|
6
|
+
* Windows: Not yet supported.
|
|
7
|
+
*
|
|
8
|
+
* The .app embeds a provisioning profile that grants the application-identifier
|
|
9
|
+
* + keychain-access-groups entitlement macOS requires for kSecAttrSynchronizable
|
|
10
|
+
* writes (iCloud Keychain). For device-local writes the helper is invoked with
|
|
11
|
+
* the `nosync` arg.
|
|
9
12
|
*/
|
|
10
13
|
/** Supported secret resolution backends. */
|
|
11
14
|
export type SecretProvider = 'keychain' | 'env' | 'file' | 'exec';
|
|
@@ -49,15 +52,15 @@ export interface KeychainBackend {
|
|
|
49
52
|
}
|
|
50
53
|
/** Install a custom keychain backend (test only). Returns the previous backend so callers can restore. */
|
|
51
54
|
export declare function setKeychainBackendForTest(b: KeychainBackend | null): KeychainBackend | null;
|
|
52
|
-
/** Check if a keychain item exists
|
|
55
|
+
/** Check if a keychain/keyring item exists. */
|
|
53
56
|
export declare function hasKeychainToken(item: string, sync?: boolean): boolean;
|
|
54
|
-
/** Retrieve a secret value from the
|
|
57
|
+
/** Retrieve a secret value from the keychain/keyring. Throws if not found. */
|
|
55
58
|
export declare function getKeychainToken(item: string, sync?: boolean): string;
|
|
56
|
-
/** Store or update a secret value in the
|
|
59
|
+
/** Store or update a secret value in the keychain/keyring. iCloud-synced when sync=true (macOS only). */
|
|
57
60
|
export declare function setKeychainToken(item: string, value: string, sync?: boolean): void;
|
|
58
|
-
/** Delete a keychain item. Returns true if it existed. */
|
|
61
|
+
/** Delete a keychain/keyring item. Returns true if it existed. */
|
|
59
62
|
export declare function deleteKeychainToken(item: string, sync?: boolean): boolean;
|
|
60
|
-
/** Enumerate keychain item
|
|
63
|
+
/** Enumerate keychain/keyring item names starting with the given prefix. */
|
|
61
64
|
export declare function listKeychainItems(prefix: string): string[];
|
|
62
65
|
/** Options controlling how secret refs are resolved. */
|
|
63
66
|
export interface ResolveOptions {
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Cross-platform secure credential storage.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* macOS: Uses Keychain via signed Swift helper (AgentsKeychain.app) or `security` CLI.
|
|
5
|
+
* Linux: Uses libsecret (GNOME Keyring) via `secret-tool` CLI.
|
|
6
|
+
* Windows: Not yet supported.
|
|
7
|
+
*
|
|
8
|
+
* The .app embeds a provisioning profile that grants the application-identifier
|
|
9
|
+
* + keychain-access-groups entitlement macOS requires for kSecAttrSynchronizable
|
|
10
|
+
* writes (iCloud Keychain). For device-local writes the helper is invoked with
|
|
11
|
+
* the `nosync` arg.
|
|
9
12
|
*/
|
|
10
13
|
import { fileURLToPath } from 'url';
|
|
11
14
|
import { execFileSync, spawnSync } from 'child_process';
|
|
12
15
|
import * as fs from 'fs';
|
|
13
16
|
import * as os from 'os';
|
|
14
17
|
import * as path from 'path';
|
|
18
|
+
import { linuxBackend } from './linux.js';
|
|
15
19
|
const SERVICE_PREFIX = 'agents-cli';
|
|
16
20
|
const REF_PATTERN = /^(keychain|env|file|exec):(.+)$/s;
|
|
17
21
|
/** Parse a bundle value into either a literal string or a typed secret ref. */
|
|
@@ -31,11 +35,18 @@ export function parseBundleValue(raw) {
|
|
|
31
35
|
export function serializeRef(ref) {
|
|
32
36
|
return `${ref.provider}:${ref.value}`;
|
|
33
37
|
}
|
|
34
|
-
function
|
|
35
|
-
if (process.platform !== 'darwin') {
|
|
36
|
-
throw new Error('
|
|
38
|
+
function assertSupportedPlatform() {
|
|
39
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
40
|
+
throw new Error('Secure credential storage requires macOS or Linux. ' +
|
|
41
|
+
'On Windows, use environment variables or .env files instead.');
|
|
37
42
|
}
|
|
38
43
|
}
|
|
44
|
+
function isLinux() {
|
|
45
|
+
return process.platform === 'linux';
|
|
46
|
+
}
|
|
47
|
+
function isMacOS() {
|
|
48
|
+
return process.platform === 'darwin';
|
|
49
|
+
}
|
|
39
50
|
/** Build the keychain item name for a profile provider token. */
|
|
40
51
|
export function profileKeychainItem(provider) {
|
|
41
52
|
return `${SERVICE_PREFIX}.${provider}.token`;
|
|
@@ -73,12 +84,14 @@ export function setKeychainBackendForTest(b) {
|
|
|
73
84
|
// holds the keychain-access-groups entitlement macOS requires for
|
|
74
85
|
// kSecAttrSynchronizable. Enumeration also goes through the .app because the
|
|
75
86
|
// security CLI doesn't expose listing by service prefix.
|
|
76
|
-
/** Check if a keychain item exists
|
|
87
|
+
/** Check if a keychain/keyring item exists. */
|
|
77
88
|
export function hasKeychainToken(item, sync = false) {
|
|
78
89
|
if (backend)
|
|
79
90
|
return backend.has(item, sync);
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
assertSupportedPlatform();
|
|
92
|
+
if (isLinux())
|
|
93
|
+
return linuxBackend.has(item, sync);
|
|
94
|
+
// macOS: Try security first (no prompts for local items), fall back to binary for synced items.
|
|
82
95
|
if (spawnSync('security', ['find-generic-password', '-a', os.userInfo().username, '-s', item, '-w'], {
|
|
83
96
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
84
97
|
}).status === 0)
|
|
@@ -89,12 +102,14 @@ export function hasKeychainToken(item, sync = false) {
|
|
|
89
102
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
90
103
|
}).status === 0;
|
|
91
104
|
}
|
|
92
|
-
/** Retrieve a secret value from the
|
|
105
|
+
/** Retrieve a secret value from the keychain/keyring. Throws if not found. */
|
|
93
106
|
export function getKeychainToken(item, sync = false) {
|
|
94
107
|
if (backend)
|
|
95
108
|
return backend.get(item, sync);
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
assertSupportedPlatform();
|
|
110
|
+
if (isLinux())
|
|
111
|
+
return linuxBackend.get(item, sync);
|
|
112
|
+
// macOS: Try security first (no prompts for local items)
|
|
98
113
|
const secResult = spawnSync('security', ['find-generic-password', '-a', os.userInfo().username, '-s', item, '-w'], {
|
|
99
114
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
100
115
|
});
|
|
@@ -119,17 +134,24 @@ export function getKeychainToken(item, sync = false) {
|
|
|
119
134
|
throw new Error(`Keychain item '${item}' exists but is empty.`);
|
|
120
135
|
return token;
|
|
121
136
|
}
|
|
122
|
-
/** Store or update a secret value in the
|
|
137
|
+
/** Store or update a secret value in the keychain/keyring. iCloud-synced when sync=true (macOS only). */
|
|
123
138
|
export function setKeychainToken(item, value, sync = false) {
|
|
124
139
|
if (backend) {
|
|
125
140
|
backend.set(item, value, sync);
|
|
126
141
|
return;
|
|
127
142
|
}
|
|
128
|
-
|
|
143
|
+
assertSupportedPlatform();
|
|
129
144
|
if (!value || !value.trim())
|
|
130
145
|
throw new Error('Secret value is empty.');
|
|
131
146
|
if (/[\r\n]/.test(value))
|
|
132
147
|
throw new Error('Secret value contains newlines, which are not supported.');
|
|
148
|
+
if (/[\x00=\r\n]/.test(item))
|
|
149
|
+
throw new Error('Secret item name contains invalid characters.');
|
|
150
|
+
if (isLinux()) {
|
|
151
|
+
linuxBackend.set(item, value, sync);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// macOS path
|
|
133
155
|
if (sync) {
|
|
134
156
|
const bin = ensureKeychainHelper();
|
|
135
157
|
const result = spawnSync(bin, ['set', item, os.userInfo().username], {
|
|
@@ -153,11 +175,14 @@ export function setKeychainToken(item, value, sync = false) {
|
|
|
153
175
|
throw new Error(`Failed to write keychain item '${item}' (exit ${result.status}).`);
|
|
154
176
|
}
|
|
155
177
|
}
|
|
156
|
-
/** Delete a keychain item. Returns true if it existed. */
|
|
178
|
+
/** Delete a keychain/keyring item. Returns true if it existed. */
|
|
157
179
|
export function deleteKeychainToken(item, sync = false) {
|
|
158
180
|
if (backend)
|
|
159
181
|
return backend.delete(item, sync);
|
|
160
|
-
|
|
182
|
+
assertSupportedPlatform();
|
|
183
|
+
if (isLinux())
|
|
184
|
+
return linuxBackend.delete(item, sync);
|
|
185
|
+
// macOS path
|
|
161
186
|
if (sync) {
|
|
162
187
|
const bin = ensureKeychainHelper();
|
|
163
188
|
return spawnSync(bin, ['delete', item, os.userInfo().username], {
|
|
@@ -171,11 +196,14 @@ export function deleteKeychainToken(item, sync = false) {
|
|
|
171
196
|
function quoteForSecurityCli(s) {
|
|
172
197
|
return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
173
198
|
}
|
|
174
|
-
/** Enumerate keychain item
|
|
199
|
+
/** Enumerate keychain/keyring item names starting with the given prefix. */
|
|
175
200
|
export function listKeychainItems(prefix) {
|
|
176
201
|
if (backend)
|
|
177
202
|
return backend.list(prefix);
|
|
178
|
-
|
|
203
|
+
assertSupportedPlatform();
|
|
204
|
+
if (isLinux())
|
|
205
|
+
return linuxBackend.list(prefix);
|
|
206
|
+
// macOS path
|
|
179
207
|
const bin = ensureKeychainHelper();
|
|
180
208
|
const result = spawnSync(bin, ['list', prefix], {
|
|
181
209
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linux secret storage via libsecret (GNOME Keyring / Secret Service API).
|
|
3
|
+
*
|
|
4
|
+
* Uses `secret-tool` CLI which is part of libsecret-tools package.
|
|
5
|
+
* On Ubuntu: apt install libsecret-tools
|
|
6
|
+
*
|
|
7
|
+
* Secrets are stored with:
|
|
8
|
+
* service = "agents-cli"
|
|
9
|
+
* account = username
|
|
10
|
+
* item = the secret identifier
|
|
11
|
+
*/
|
|
12
|
+
import type { KeychainBackend } from './index.js';
|
|
13
|
+
/**
|
|
14
|
+
* secret-tool lookup attributes:
|
|
15
|
+
* service=agents-cli account=<user> item=<itemName>
|
|
16
|
+
*/
|
|
17
|
+
export declare function hasSecretToolToken(item: string): boolean;
|
|
18
|
+
export declare function getSecretToolToken(item: string): string;
|
|
19
|
+
export declare function setSecretToolToken(item: string, value: string): void;
|
|
20
|
+
export declare function deleteSecretToolToken(item: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* List secrets by prefix. secret-tool doesn't have a list command,
|
|
23
|
+
* so we use secret-tool search which outputs in a specific format.
|
|
24
|
+
*/
|
|
25
|
+
export declare function listSecretToolItems(prefix: string): string[];
|
|
26
|
+
/** KeychainBackend implementation for Linux using secret-tool */
|
|
27
|
+
export declare const linuxBackend: KeychainBackend;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linux secret storage via libsecret (GNOME Keyring / Secret Service API).
|
|
3
|
+
*
|
|
4
|
+
* Uses `secret-tool` CLI which is part of libsecret-tools package.
|
|
5
|
+
* On Ubuntu: apt install libsecret-tools
|
|
6
|
+
*
|
|
7
|
+
* Secrets are stored with:
|
|
8
|
+
* service = "agents-cli"
|
|
9
|
+
* account = username
|
|
10
|
+
* item = the secret identifier
|
|
11
|
+
*/
|
|
12
|
+
import { spawnSync } from 'child_process';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
const SERVICE = 'agents-cli';
|
|
15
|
+
function secretToolAvailable() {
|
|
16
|
+
const result = spawnSync('which', ['secret-tool'], {
|
|
17
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
18
|
+
});
|
|
19
|
+
return result.status === 0;
|
|
20
|
+
}
|
|
21
|
+
let checkedAvailability = false;
|
|
22
|
+
let isAvailable = false;
|
|
23
|
+
function ensureSecretTool() {
|
|
24
|
+
if (!checkedAvailability) {
|
|
25
|
+
isAvailable = secretToolAvailable();
|
|
26
|
+
checkedAvailability = true;
|
|
27
|
+
}
|
|
28
|
+
if (!isAvailable) {
|
|
29
|
+
throw new Error('secret-tool not found. Install libsecret-tools:\n' +
|
|
30
|
+
' Ubuntu/Debian: sudo apt install libsecret-tools\n' +
|
|
31
|
+
' Fedora: sudo dnf install libsecret\n' +
|
|
32
|
+
' Arch: sudo pacman -S libsecret');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* secret-tool lookup attributes:
|
|
37
|
+
* service=agents-cli account=<user> item=<itemName>
|
|
38
|
+
*/
|
|
39
|
+
export function hasSecretToolToken(item) {
|
|
40
|
+
ensureSecretTool();
|
|
41
|
+
const user = os.userInfo().username;
|
|
42
|
+
const result = spawnSync('secret-tool', [
|
|
43
|
+
'lookup',
|
|
44
|
+
'service', SERVICE,
|
|
45
|
+
'account', user,
|
|
46
|
+
'item', item,
|
|
47
|
+
], {
|
|
48
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
49
|
+
});
|
|
50
|
+
return result.status === 0 && result.stdout?.toString().trim().length > 0;
|
|
51
|
+
}
|
|
52
|
+
export function getSecretToolToken(item) {
|
|
53
|
+
ensureSecretTool();
|
|
54
|
+
const user = os.userInfo().username;
|
|
55
|
+
const result = spawnSync('secret-tool', [
|
|
56
|
+
'lookup',
|
|
57
|
+
'service', SERVICE,
|
|
58
|
+
'account', user,
|
|
59
|
+
'item', item,
|
|
60
|
+
], {
|
|
61
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
62
|
+
});
|
|
63
|
+
if (result.status !== 0) {
|
|
64
|
+
throw new Error(`Secret '${item}' not found in keyring.`);
|
|
65
|
+
}
|
|
66
|
+
const token = result.stdout?.toString().trim();
|
|
67
|
+
if (!token) {
|
|
68
|
+
throw new Error(`Secret '${item}' exists but is empty.`);
|
|
69
|
+
}
|
|
70
|
+
return token;
|
|
71
|
+
}
|
|
72
|
+
export function setSecretToolToken(item, value) {
|
|
73
|
+
ensureSecretTool();
|
|
74
|
+
if (!value || !value.trim())
|
|
75
|
+
throw new Error('Secret value is empty.');
|
|
76
|
+
const user = os.userInfo().username;
|
|
77
|
+
const label = `agents-cli: ${item}`;
|
|
78
|
+
// secret-tool store reads value from stdin
|
|
79
|
+
const result = spawnSync('secret-tool', [
|
|
80
|
+
'store',
|
|
81
|
+
'--label', label,
|
|
82
|
+
'service', SERVICE,
|
|
83
|
+
'account', user,
|
|
84
|
+
'item', item,
|
|
85
|
+
], {
|
|
86
|
+
input: value,
|
|
87
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
88
|
+
});
|
|
89
|
+
if (result.status !== 0) {
|
|
90
|
+
const stderr = result.stderr?.toString().trim();
|
|
91
|
+
throw new Error(`Failed to store secret '${item}': ${stderr || 'unknown error'}\n` +
|
|
92
|
+
'Make sure GNOME Keyring or another Secret Service provider is running.');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function deleteSecretToolToken(item) {
|
|
96
|
+
ensureSecretTool();
|
|
97
|
+
const user = os.userInfo().username;
|
|
98
|
+
const result = spawnSync('secret-tool', [
|
|
99
|
+
'clear',
|
|
100
|
+
'service', SERVICE,
|
|
101
|
+
'account', user,
|
|
102
|
+
'item', item,
|
|
103
|
+
], {
|
|
104
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
105
|
+
});
|
|
106
|
+
// secret-tool clear returns 0 whether the item existed or not.
|
|
107
|
+
// This matches the macOS behavior where delete is idempotent.
|
|
108
|
+
return result.status === 0;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* List secrets by prefix. secret-tool doesn't have a list command,
|
|
112
|
+
* so we use secret-tool search which outputs in a specific format.
|
|
113
|
+
*/
|
|
114
|
+
export function listSecretToolItems(prefix) {
|
|
115
|
+
ensureSecretTool();
|
|
116
|
+
// secret-tool search outputs attributes, one item per block
|
|
117
|
+
const result = spawnSync('secret-tool', [
|
|
118
|
+
'search',
|
|
119
|
+
'--all',
|
|
120
|
+
'service', SERVICE,
|
|
121
|
+
], {
|
|
122
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
123
|
+
});
|
|
124
|
+
if (result.status !== 0) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const output = result.stdout?.toString() || '';
|
|
128
|
+
const items = [];
|
|
129
|
+
// Parse output format:
|
|
130
|
+
// [/org/freedesktop/secrets/collection/login/1]
|
|
131
|
+
// label = agents-cli: myitem
|
|
132
|
+
// ...
|
|
133
|
+
// attribute.item = myitem
|
|
134
|
+
const itemRegex = /attribute\.item\s*=\s*(.+)/g;
|
|
135
|
+
let match;
|
|
136
|
+
while ((match = itemRegex.exec(output)) !== null) {
|
|
137
|
+
const itemName = match[1].trim();
|
|
138
|
+
if (itemName.startsWith(prefix)) {
|
|
139
|
+
items.push(itemName);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return [...new Set(items)]; // dedupe
|
|
143
|
+
}
|
|
144
|
+
/** KeychainBackend implementation for Linux using secret-tool */
|
|
145
|
+
export const linuxBackend = {
|
|
146
|
+
has(item, _sync) {
|
|
147
|
+
return hasSecretToolToken(item);
|
|
148
|
+
},
|
|
149
|
+
get(item, _sync) {
|
|
150
|
+
return getSecretToolToken(item);
|
|
151
|
+
},
|
|
152
|
+
set(item, value, _sync) {
|
|
153
|
+
setSecretToolToken(item, value);
|
|
154
|
+
},
|
|
155
|
+
delete(item, _sync) {
|
|
156
|
+
return deleteSecretToolToken(item);
|
|
157
|
+
},
|
|
158
|
+
list(prefix) {
|
|
159
|
+
return listSecretToolItems(prefix);
|
|
160
|
+
},
|
|
161
|
+
};
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -137,3 +137,7 @@ export declare function getRowCount(): {
|
|
|
137
137
|
sessions: number;
|
|
138
138
|
textRows: number;
|
|
139
139
|
};
|
|
140
|
+
/** Count sessions older than the given timestamp (for dry-run previews). */
|
|
141
|
+
export declare function countSessionsOlderThan(cutoffMs: number): number;
|
|
142
|
+
/** Delete sessions older than the given timestamp. Returns the number of rows deleted. */
|
|
143
|
+
export declare function deleteSessionsOlderThan(cutoffMs: number): number;
|
package/dist/lib/session/db.js
CHANGED
|
@@ -638,3 +638,29 @@ export function getRowCount() {
|
|
|
638
638
|
const textRows = db.prepare(`SELECT COUNT(*) AS c FROM session_text`).get().c;
|
|
639
639
|
return { sessions, textRows };
|
|
640
640
|
}
|
|
641
|
+
/** Count sessions older than the given timestamp (for dry-run previews). */
|
|
642
|
+
export function countSessionsOlderThan(cutoffMs) {
|
|
643
|
+
const db = getDB();
|
|
644
|
+
const cutoffIso = new Date(cutoffMs).toISOString();
|
|
645
|
+
const row = db.prepare(`SELECT COUNT(*) AS n FROM sessions WHERE timestamp < ?`).get(cutoffIso);
|
|
646
|
+
return row.n;
|
|
647
|
+
}
|
|
648
|
+
/** Delete sessions older than the given timestamp. Returns the number of rows deleted. */
|
|
649
|
+
export function deleteSessionsOlderThan(cutoffMs) {
|
|
650
|
+
const db = getDB();
|
|
651
|
+
const cutoffIso = new Date(cutoffMs).toISOString();
|
|
652
|
+
const rows = db.prepare(`SELECT id, file_path FROM sessions WHERE timestamp < ?`).all(cutoffIso);
|
|
653
|
+
if (rows.length === 0)
|
|
654
|
+
return 0;
|
|
655
|
+
const txn = db.transaction(() => {
|
|
656
|
+
for (const { id, file_path } of rows) {
|
|
657
|
+
db.prepare(`DELETE FROM session_text WHERE session_id = ?`).run(id);
|
|
658
|
+
db.prepare(`DELETE FROM sessions WHERE id = ?`).run(id);
|
|
659
|
+
if (file_path) {
|
|
660
|
+
db.prepare(`DELETE FROM scan_ledger WHERE file_path = ?`).run(canonicalLedgerKey(file_path));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
txn();
|
|
665
|
+
return rows.length;
|
|
666
|
+
}
|
package/dist/lib/usage.d.ts
CHANGED
|
@@ -79,7 +79,7 @@ export declare function getUsageInfoForIdentity(input: UsageIdentityInput): Prom
|
|
|
79
79
|
export declare function formatUsageSummary(plan: string | null, snapshot: UsageSnapshot | null, planWidth?: number): string;
|
|
80
80
|
/** Format a multi-line usage section for detailed agent views. */
|
|
81
81
|
export declare function formatUsageSection(usage: UsageInfo): string[];
|
|
82
|
-
/** Load Claude OAuth credentials from the
|
|
82
|
+
/** Load Claude OAuth credentials from the system keychain/keyring. */
|
|
83
83
|
export declare function loadClaudeOauth(home?: string): Promise<ClaudeOauthCredentials | null>;
|
|
84
84
|
/**
|
|
85
85
|
* Derive the Keychain service name for a Claude home directory.
|
package/dist/lib/usage.js
CHANGED
|
@@ -15,6 +15,7 @@ import * as readline from 'readline';
|
|
|
15
15
|
import { promisify } from 'util';
|
|
16
16
|
import chalk from 'chalk';
|
|
17
17
|
import { walkForFiles } from './fs-walk.js';
|
|
18
|
+
import { getKeychainToken, setKeychainToken, deleteKeychainToken, } from './secrets/index.js';
|
|
18
19
|
import { getAgentsDir } from './state.js';
|
|
19
20
|
const execFileAsync = promisify(execFile);
|
|
20
21
|
const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
@@ -343,27 +344,15 @@ function normalizeClaudeWindow(window, key, label, shortLabel) {
|
|
|
343
344
|
windowMinutes: inferWindowMinutes(key),
|
|
344
345
|
};
|
|
345
346
|
}
|
|
346
|
-
|
|
347
|
-
/** Load Claude OAuth credentials from the macOS Keychain. */
|
|
347
|
+
/** Load Claude OAuth credentials from the system keychain/keyring. */
|
|
348
348
|
export async function loadClaudeOauth(home) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
process.stderr.write('[agents] Usage tracking requires macOS Keychain. Skipped on this platform.\n');
|
|
352
|
-
warnedNonDarwin = true;
|
|
353
|
-
}
|
|
349
|
+
// Windows not yet supported
|
|
350
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
354
351
|
return null;
|
|
355
352
|
}
|
|
356
353
|
try {
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
'find-generic-password',
|
|
360
|
-
'-a',
|
|
361
|
-
account,
|
|
362
|
-
'-s',
|
|
363
|
-
// Managed Claude homes must stay pinned to their own service name.
|
|
364
|
-
getClaudeKeychainService(home),
|
|
365
|
-
'-w',
|
|
366
|
-
]);
|
|
354
|
+
const service = getClaudeKeychainService(home);
|
|
355
|
+
const stdout = getKeychainToken(service);
|
|
367
356
|
const payload = JSON.parse(stdout.trim());
|
|
368
357
|
if (typeof payload?.claudeAiOauth?.accessToken !== 'string')
|
|
369
358
|
return null;
|
|
@@ -380,27 +369,20 @@ export async function loadClaudeOauth(home) {
|
|
|
380
369
|
}
|
|
381
370
|
}
|
|
382
371
|
/**
|
|
383
|
-
* Save Claude OAuth credentials to the
|
|
372
|
+
* Save Claude OAuth credentials to the system keychain/keyring.
|
|
384
373
|
* Reads the existing payload, merges the new OAuth fields, and writes back.
|
|
385
374
|
*/
|
|
386
375
|
async function saveClaudeOauth(home, credentials) {
|
|
387
|
-
|
|
376
|
+
// Windows not yet supported
|
|
377
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
388
378
|
return false;
|
|
389
379
|
}
|
|
390
380
|
try {
|
|
391
|
-
const account = os.userInfo().username;
|
|
392
381
|
const service = getClaudeKeychainService(home);
|
|
393
382
|
// Read existing payload to preserve other fields
|
|
394
383
|
let existingPayload = {};
|
|
395
384
|
try {
|
|
396
|
-
const
|
|
397
|
-
'find-generic-password',
|
|
398
|
-
'-a',
|
|
399
|
-
account,
|
|
400
|
-
'-s',
|
|
401
|
-
service,
|
|
402
|
-
'-w',
|
|
403
|
-
]);
|
|
385
|
+
const stdout = getKeychainToken(service);
|
|
404
386
|
existingPayload = JSON.parse(stdout.trim());
|
|
405
387
|
}
|
|
406
388
|
catch {
|
|
@@ -418,29 +400,14 @@ async function saveClaudeOauth(home, credentials) {
|
|
|
418
400
|
},
|
|
419
401
|
};
|
|
420
402
|
const payloadJson = JSON.stringify(newPayload);
|
|
421
|
-
// Delete existing entry first
|
|
403
|
+
// Delete existing entry first, then add updated entry
|
|
422
404
|
try {
|
|
423
|
-
|
|
424
|
-
'delete-generic-password',
|
|
425
|
-
'-a',
|
|
426
|
-
account,
|
|
427
|
-
'-s',
|
|
428
|
-
service,
|
|
429
|
-
]);
|
|
405
|
+
deleteKeychainToken(service);
|
|
430
406
|
}
|
|
431
407
|
catch {
|
|
432
408
|
// Entry might not exist, ignore
|
|
433
409
|
}
|
|
434
|
-
|
|
435
|
-
await execFileAsync('security', [
|
|
436
|
-
'add-generic-password',
|
|
437
|
-
'-a',
|
|
438
|
-
account,
|
|
439
|
-
'-s',
|
|
440
|
-
service,
|
|
441
|
-
'-w',
|
|
442
|
-
payloadJson,
|
|
443
|
-
]);
|
|
410
|
+
setKeychainToken(service, payloadJson);
|
|
444
411
|
return true;
|
|
445
412
|
}
|
|
446
413
|
catch {
|
package/dist/lib/versions.js
CHANGED
|
@@ -945,6 +945,17 @@ export async function installVersion(agent, version, onProgress) {
|
|
|
945
945
|
throw new Error(`Invalid version: ${JSON.stringify(version)}`);
|
|
946
946
|
}
|
|
947
947
|
try {
|
|
948
|
+
// Check npm is available
|
|
949
|
+
try {
|
|
950
|
+
await execFileAsync('which', ['npm']);
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
return {
|
|
954
|
+
success: false,
|
|
955
|
+
installedVersion: version,
|
|
956
|
+
error: 'npm is not installed. Install Node.js and npm first: https://nodejs.org/',
|
|
957
|
+
};
|
|
958
|
+
}
|
|
948
959
|
onProgress?.(`Installing ${packageSpec}...`);
|
|
949
960
|
const { stdout } = await execFileAsync('npm', ['install', packageSpec], { cwd: versionDir });
|
|
950
961
|
// Determine the actual installed version
|
package/package.json
CHANGED