@phnx-labs/agents-cli 1.14.7 → 1.16.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/CHANGELOG.md +78 -39
- package/README.md +74 -7
- package/dist/commands/alias.js +2 -2
- package/dist/commands/beta.js +6 -1
- package/dist/commands/browser-picker.d.ts +21 -0
- package/dist/commands/browser-picker.js +114 -0
- package/dist/commands/browser.js +546 -75
- package/dist/commands/commands.js +72 -22
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/exec.js +9 -2
- package/dist/commands/fork.js +2 -2
- package/dist/commands/hooks.js +71 -26
- package/dist/commands/mcp.js +85 -43
- package/dist/commands/plugins.js +48 -15
- package/dist/commands/prune.d.ts +0 -20
- package/dist/commands/prune.js +291 -16
- package/dist/commands/pull.js +3 -3
- package/dist/commands/repo.js +1 -1
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.js +37 -1
- package/dist/commands/sessions.js +62 -19
- package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
- package/dist/commands/{init.js → setup.js} +32 -21
- package/dist/commands/skills.js +60 -19
- package/dist/commands/subagents.js +41 -13
- package/dist/commands/teams.js +2 -3
- package/dist/commands/usage.js +6 -0
- package/dist/commands/utils.d.ts +16 -0
- package/dist/commands/utils.js +32 -0
- package/dist/commands/versions.js +8 -6
- package/dist/commands/view.js +61 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -20
- package/dist/lib/agents.js +2 -2
- package/dist/lib/auto-pull-worker.js +2 -3
- package/dist/lib/auto-pull.js +2 -2
- package/dist/lib/browser/cdp.d.ts +7 -1
- package/dist/lib/browser/cdp.js +29 -1
- package/dist/lib/browser/chrome.js +6 -3
- package/dist/lib/browser/devices.d.ts +4 -0
- package/dist/lib/browser/devices.js +27 -0
- package/dist/lib/browser/drivers/local.js +9 -4
- package/dist/lib/browser/drivers/ssh.d.ts +1 -0
- package/dist/lib/browser/drivers/ssh.js +32 -4
- package/dist/lib/browser/ipc.js +145 -23
- package/dist/lib/browser/profiles.d.ts +5 -2
- package/dist/lib/browser/profiles.js +77 -37
- package/dist/lib/browser/service.d.ts +84 -13
- package/dist/lib/browser/service.js +806 -122
- package/dist/lib/browser/types.d.ts +81 -3
- package/dist/lib/browser/types.js +16 -0
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/commands.d.ts +1 -0
- package/dist/lib/commands.js +6 -2
- package/dist/lib/daemon.js +6 -7
- package/dist/lib/doctor-diff.js +4 -4
- package/dist/lib/events.d.ts +94 -1
- package/dist/lib/events.js +264 -6
- package/dist/lib/exec.js +16 -10
- package/dist/lib/hooks.d.ts +11 -7
- package/dist/lib/hooks.js +125 -49
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +1178 -21
- package/dist/lib/models.js +2 -2
- package/dist/lib/permissions.d.ts +14 -11
- package/dist/lib/permissions.js +46 -42
- package/dist/lib/plugins.d.ts +30 -1
- package/dist/lib/plugins.js +75 -3
- package/dist/lib/pty-server.js +9 -10
- package/dist/lib/resources/hooks.d.ts +5 -1
- package/dist/lib/resources/hooks.js +21 -4
- package/dist/lib/rotate.js +3 -4
- 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/active.d.ts +3 -0
- package/dist/lib/session/active.js +92 -6
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +34 -3
- package/dist/lib/session/discover.js +30 -15
- package/dist/lib/session/team-filter.js +2 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +6 -6
- package/dist/lib/skills.js +6 -2
- package/dist/lib/state.d.ts +86 -14
- package/dist/lib/state.js +150 -23
- package/dist/lib/subagents.d.ts +28 -0
- package/dist/lib/subagents.js +98 -1
- package/dist/lib/sync-manifest.d.ts +1 -1
- package/dist/lib/sync-manifest.js +3 -3
- package/dist/lib/teams/persistence.js +15 -5
- package/dist/lib/teams/registry.js +2 -2
- package/dist/lib/types.d.ts +32 -3
- package/dist/lib/types.js +3 -3
- package/dist/lib/usage.d.ts +1 -1
- package/dist/lib/usage.js +15 -48
- package/dist/lib/versions.js +31 -21
- package/package.json +1 -1
- package/scripts/postinstall.js +37 -9
|
@@ -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
|
+
};
|
|
@@ -8,7 +8,10 @@ export interface ActiveSession {
|
|
|
8
8
|
pid?: number;
|
|
9
9
|
sessionId?: string;
|
|
10
10
|
cwd?: string;
|
|
11
|
+
/** User-given name from /rename command. */
|
|
11
12
|
label?: string;
|
|
13
|
+
/** First meaningful line of the initial prompt (extracted topic). */
|
|
14
|
+
topic?: string;
|
|
12
15
|
sessionFile?: string;
|
|
13
16
|
startedAtMs?: number;
|
|
14
17
|
status: ActiveStatus;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Active-session detection across every context an agent can run in:
|
|
3
3
|
*
|
|
4
4
|
* - `terminal` — agents launched from VS Code / Cursor / Codium via the
|
|
5
|
-
* agents-cli extension. Published to `~/.agents/
|
|
5
|
+
* agents-cli extension. Published to `~/.agents/.cache/terminals/live-terminals.json`
|
|
6
6
|
* with PID + session UUID per entry.
|
|
7
7
|
* - `teams` — agents spawned by `agents teams add`, tracked in
|
|
8
8
|
* `~/.agents/teams/agents/<id>/meta.json` with a PID the manager polls.
|
|
@@ -23,10 +23,12 @@ import { execFile } from 'child_process';
|
|
|
23
23
|
import { promisify } from 'util';
|
|
24
24
|
import { listActiveTasks } from '../cloud/store.js';
|
|
25
25
|
import { AgentManager } from '../teams/agents.js';
|
|
26
|
-
import {
|
|
26
|
+
import { getTerminalsDir } from '../state.js';
|
|
27
|
+
import { buildClaudeLabelMap } from './discover.js';
|
|
28
|
+
import { extractSessionTopic } from './prompt.js';
|
|
27
29
|
const execFileAsync = promisify(execFile);
|
|
28
30
|
const HOME = os.homedir();
|
|
29
|
-
const LIVE_TERMINALS_FILE = path.join(
|
|
31
|
+
const LIVE_TERMINALS_FILE = path.join(getTerminalsDir(), 'live-terminals.json');
|
|
30
32
|
/**
|
|
31
33
|
* A process is classified `running` if its session file was touched in the
|
|
32
34
|
* last 2 minutes. Every Claude/Codex tool-call appends an event, so a
|
|
@@ -80,9 +82,9 @@ function readLiveTerminals() {
|
|
|
80
82
|
}
|
|
81
83
|
return Array.from(merged.values());
|
|
82
84
|
}
|
|
83
|
-
/** Convert an absolute cwd to the Claude-project folder name (slashes → dashes). */
|
|
85
|
+
/** Convert an absolute cwd to the Claude-project folder name (slashes and dots → dashes). */
|
|
84
86
|
function claudeProjectDirName(cwd) {
|
|
85
|
-
return cwd.replace(
|
|
87
|
+
return cwd.replace(/[/.]/g, '-');
|
|
86
88
|
}
|
|
87
89
|
/**
|
|
88
90
|
* Locate the active Claude session file for a process. If we know the session
|
|
@@ -126,6 +128,79 @@ function classifyActivity(sessionFile) {
|
|
|
126
128
|
return 'running';
|
|
127
129
|
}
|
|
128
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Extract the first user message's content from a Claude JSONL file.
|
|
133
|
+
* Reads only the first ~50 lines for speed, since the user message is
|
|
134
|
+
* typically near the top (after system/queue events).
|
|
135
|
+
*/
|
|
136
|
+
function extractClaudeUserText(parsed) {
|
|
137
|
+
const msg = parsed.message;
|
|
138
|
+
if (!msg?.content)
|
|
139
|
+
return undefined;
|
|
140
|
+
const content = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
141
|
+
const texts = [];
|
|
142
|
+
for (const block of content) {
|
|
143
|
+
if (typeof block === 'string')
|
|
144
|
+
texts.push(block);
|
|
145
|
+
else if (block?.type === 'text' && typeof block.text === 'string')
|
|
146
|
+
texts.push(block.text);
|
|
147
|
+
}
|
|
148
|
+
return texts.join('\n').trim() || undefined;
|
|
149
|
+
}
|
|
150
|
+
function quickExtractTopic(sessionFile) {
|
|
151
|
+
let fd;
|
|
152
|
+
try {
|
|
153
|
+
fd = fs.openSync(sessionFile, 'r');
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const chunkSize = 256 * 1024;
|
|
160
|
+
const maxBytes = 2 * 1024 * 1024;
|
|
161
|
+
let buffer = '';
|
|
162
|
+
let totalRead = 0;
|
|
163
|
+
let linesChecked = 0;
|
|
164
|
+
const maxLines = 30;
|
|
165
|
+
while (totalRead < maxBytes && linesChecked < maxLines) {
|
|
166
|
+
const chunk = Buffer.alloc(chunkSize);
|
|
167
|
+
const bytesRead = fs.readSync(fd, chunk, 0, chunkSize, totalRead);
|
|
168
|
+
if (bytesRead === 0)
|
|
169
|
+
break;
|
|
170
|
+
totalRead += bytesRead;
|
|
171
|
+
buffer += chunk.toString('utf8', 0, bytesRead);
|
|
172
|
+
let lineStart = 0;
|
|
173
|
+
let lineEnd;
|
|
174
|
+
while ((lineEnd = buffer.indexOf('\n', lineStart)) !== -1 && linesChecked < maxLines) {
|
|
175
|
+
const line = buffer.slice(lineStart, lineEnd);
|
|
176
|
+
lineStart = lineEnd + 1;
|
|
177
|
+
linesChecked++;
|
|
178
|
+
if (!line.trim())
|
|
179
|
+
continue;
|
|
180
|
+
let parsed;
|
|
181
|
+
try {
|
|
182
|
+
parsed = JSON.parse(line);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (parsed.type === 'user') {
|
|
188
|
+
const text = extractClaudeUserText(parsed);
|
|
189
|
+
if (text) {
|
|
190
|
+
const topic = extractSessionTopic(text);
|
|
191
|
+
if (topic)
|
|
192
|
+
return topic;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
buffer = buffer.slice(lineStart);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
fs.closeSync(fd);
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
129
204
|
/** Live teams teammates. Reuses AgentManager which already polls PIDs via `kill -0`. */
|
|
130
205
|
export async function listTeamsActive() {
|
|
131
206
|
const mgr = new AgentManager();
|
|
@@ -135,6 +210,7 @@ export async function listTeamsActive() {
|
|
|
135
210
|
const sessionFile = a.agentType === 'claude' && a.cwd
|
|
136
211
|
? findClaudeSessionFile(a.cwd, sessionId ?? undefined)
|
|
137
212
|
: undefined;
|
|
213
|
+
const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
|
|
138
214
|
return {
|
|
139
215
|
context: 'teams',
|
|
140
216
|
kind: a.agentType,
|
|
@@ -142,6 +218,7 @@ export async function listTeamsActive() {
|
|
|
142
218
|
sessionId,
|
|
143
219
|
cwd: a.cwd ?? undefined,
|
|
144
220
|
label: a.name ?? undefined,
|
|
221
|
+
topic,
|
|
145
222
|
sessionFile,
|
|
146
223
|
startedAtMs: a.startedAt.getTime(),
|
|
147
224
|
status: classifyActivity(sessionFile),
|
|
@@ -160,10 +237,16 @@ export async function listTerminalsActive() {
|
|
|
160
237
|
const procByPid = new Map();
|
|
161
238
|
for (const r of await readProcessTable())
|
|
162
239
|
procByPid.set(r.pid, r);
|
|
240
|
+
// Build label map from Claude's sessions/*.json for /rename support
|
|
241
|
+
const labelMap = buildClaudeLabelMap();
|
|
163
242
|
return entries.map((t) => {
|
|
164
243
|
const sessionFile = t.kind === 'claude' && t.cwd
|
|
165
244
|
? findClaudeSessionFile(t.cwd, t.sessionId)
|
|
166
245
|
: undefined;
|
|
246
|
+
// Prefer label from live terminal, fall back to Claude's session label
|
|
247
|
+
const label = t.label ?? (t.sessionId ? labelMap.get(t.sessionId) : undefined) ?? undefined;
|
|
248
|
+
// Extract topic from session file (first meaningful user message)
|
|
249
|
+
const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
|
|
167
250
|
return {
|
|
168
251
|
context: 'terminal',
|
|
169
252
|
kind: t.kind,
|
|
@@ -171,7 +254,8 @@ export async function listTerminalsActive() {
|
|
|
171
254
|
pid: t.pid,
|
|
172
255
|
sessionId: t.sessionId,
|
|
173
256
|
cwd: t.cwd ?? undefined,
|
|
174
|
-
label
|
|
257
|
+
label,
|
|
258
|
+
topic,
|
|
175
259
|
sessionFile,
|
|
176
260
|
startedAtMs: t.startedAtMs,
|
|
177
261
|
status: classifyActivity(sessionFile),
|
|
@@ -355,6 +439,7 @@ export async function listUnattributedActive(attributed) {
|
|
|
355
439
|
const { pid, kind } = candidates[i];
|
|
356
440
|
const cwd = cwds[i];
|
|
357
441
|
const sessionFile = kind === 'claude' && cwd ? findClaudeSessionFile(cwd) : undefined;
|
|
442
|
+
const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
|
|
358
443
|
const host = detectHost(pid, procByPid);
|
|
359
444
|
const context = host && UI_HOSTS.has(host) ? 'terminal' : 'headless';
|
|
360
445
|
out.push({
|
|
@@ -363,6 +448,7 @@ export async function listUnattributedActive(attributed) {
|
|
|
363
448
|
host,
|
|
364
449
|
pid,
|
|
365
450
|
cwd,
|
|
451
|
+
topic,
|
|
366
452
|
sessionFile,
|
|
367
453
|
status: classifyActivity(sessionFile),
|
|
368
454
|
});
|
|
@@ -13,10 +13,10 @@ import * as fs from 'fs';
|
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as os from 'os';
|
|
15
15
|
import * as yaml from 'yaml';
|
|
16
|
-
import {
|
|
16
|
+
import { getCacheDir } from '../state.js';
|
|
17
17
|
const PROXY_BASE = process.env.RUSH_PROXY_BASE ?? 'https://api.prix.dev';
|
|
18
18
|
const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
|
|
19
|
-
const CLOUD_CACHE_DIR = path.join(
|
|
19
|
+
const CLOUD_CACHE_DIR = path.join(getCacheDir(), 'cloud-runs');
|
|
20
20
|
function readToken() {
|
|
21
21
|
if (!fs.existsSync(USER_YAML)) {
|
|
22
22
|
throw new Error('Not logged in to Rush. Run `rush login` first.');
|
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
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import Database from '../sqlite.js';
|
|
12
|
-
import {
|
|
13
|
-
const SESSIONS_DIR =
|
|
14
|
-
const DB_PATH =
|
|
12
|
+
import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
13
|
+
const SESSIONS_DIR = getSessionsDir();
|
|
14
|
+
const DB_PATH = getSessionsDbPath();
|
|
15
15
|
/** Current schema version; bumped when migrations are added. */
|
|
16
16
|
const SCHEMA_VERSION = 5;
|
|
17
17
|
/**
|
|
@@ -151,6 +151,11 @@ export function getDB() {
|
|
|
151
151
|
db.pragma('journal_mode = WAL');
|
|
152
152
|
db.pragma('synchronous = NORMAL');
|
|
153
153
|
db.pragma('temp_store = MEMORY');
|
|
154
|
+
// Wait up to 10s instead of failing immediately on SQLITE_BUSY. Multiple
|
|
155
|
+
// agents (CLIs, indexers, hooks) all open this DB concurrently; without a
|
|
156
|
+
// busy timeout, parallel writers throw "database is locked" the moment one
|
|
157
|
+
// holds the write lock. 10s is well above any realistic transaction here.
|
|
158
|
+
db.pragma('busy_timeout = 10000');
|
|
154
159
|
db.exec(SCHEMA);
|
|
155
160
|
const current = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
156
161
|
const currentVersion = current ? parseInt(current.value, 10) : 0;
|
|
@@ -638,3 +643,29 @@ export function getRowCount() {
|
|
|
638
643
|
const textRows = db.prepare(`SELECT COUNT(*) AS c FROM session_text`).get().c;
|
|
639
644
|
return { sessions, textRows };
|
|
640
645
|
}
|
|
646
|
+
/** Count sessions older than the given timestamp (for dry-run previews). */
|
|
647
|
+
export function countSessionsOlderThan(cutoffMs) {
|
|
648
|
+
const db = getDB();
|
|
649
|
+
const cutoffIso = new Date(cutoffMs).toISOString();
|
|
650
|
+
const row = db.prepare(`SELECT COUNT(*) AS n FROM sessions WHERE timestamp < ?`).get(cutoffIso);
|
|
651
|
+
return row.n;
|
|
652
|
+
}
|
|
653
|
+
/** Delete sessions older than the given timestamp. Returns the number of rows deleted. */
|
|
654
|
+
export function deleteSessionsOlderThan(cutoffMs) {
|
|
655
|
+
const db = getDB();
|
|
656
|
+
const cutoffIso = new Date(cutoffMs).toISOString();
|
|
657
|
+
const rows = db.prepare(`SELECT id, file_path FROM sessions WHERE timestamp < ?`).all(cutoffIso);
|
|
658
|
+
if (rows.length === 0)
|
|
659
|
+
return 0;
|
|
660
|
+
const txn = db.transaction(() => {
|
|
661
|
+
for (const { id, file_path } of rows) {
|
|
662
|
+
db.prepare(`DELETE FROM session_text WHERE session_id = ?`).run(id);
|
|
663
|
+
db.prepare(`DELETE FROM sessions WHERE id = ?`).run(id);
|
|
664
|
+
if (file_path) {
|
|
665
|
+
db.prepare(`DELETE FROM scan_ledger WHERE file_path = ?`).run(canonicalLedgerKey(file_path));
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
txn();
|
|
670
|
+
return rows.length;
|
|
671
|
+
}
|