@phnx-labs/agents-cli 1.20.4 → 1.20.5
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 +19 -0
- package/README.md +48 -17
- package/dist/commands/cli.js +1 -1
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +2 -0
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/exec.js +52 -16
- package/dist/commands/hooks.js +6 -6
- package/dist/commands/inspect.d.ts +26 -0
- package/dist/commands/inspect.js +590 -0
- package/dist/commands/mcp.js +17 -16
- package/dist/commands/models.js +1 -1
- package/dist/commands/packages.js +6 -4
- package/dist/commands/permissions.js +13 -12
- package/dist/commands/plugins.d.ts +13 -0
- package/dist/commands/plugins.js +100 -11
- package/dist/commands/prune.js +3 -2
- package/dist/commands/pull.d.ts +12 -5
- package/dist/commands/pull.js +26 -422
- package/dist/commands/push.d.ts +14 -0
- package/dist/commands/push.js +30 -0
- package/dist/commands/repo.d.ts +1 -1
- package/dist/commands/repo.js +155 -112
- package/dist/commands/resource-view.d.ts +2 -0
- package/dist/commands/resource-view.js +12 -3
- package/dist/commands/routines.js +32 -7
- package/dist/commands/rules.js +1 -1
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/setup.d.ts +3 -3
- package/dist/commands/setup.js +15 -15
- package/dist/commands/skills.js +6 -5
- package/dist/commands/subagents.js +5 -4
- package/dist/commands/sync.d.ts +18 -5
- package/dist/commands/sync.js +251 -65
- package/dist/commands/teams.js +1 -0
- package/dist/commands/tmux.d.ts +25 -0
- package/dist/commands/tmux.js +415 -0
- package/dist/commands/trash.d.ts +2 -2
- package/dist/commands/trash.js +1 -1
- package/dist/commands/versions.js +2 -2
- package/dist/commands/view.js +9 -4
- package/dist/commands/workflows.js +4 -3
- package/dist/commands/worktree.d.ts +4 -5
- package/dist/commands/worktree.js +4 -4
- package/dist/index.js +68 -20
- package/dist/lib/agents.d.ts +19 -10
- package/dist/lib/agents.js +79 -25
- package/dist/lib/auto-pull-worker.d.ts +1 -1
- package/dist/lib/auto-pull-worker.js +2 -2
- package/dist/lib/auto-pull.d.ts +1 -1
- package/dist/lib/auto-pull.js +1 -1
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/capabilities.js +2 -0
- package/dist/lib/commands.d.ts +28 -1
- package/dist/lib/commands.js +125 -20
- package/dist/lib/doctor-diff.js +2 -2
- package/dist/lib/exec.d.ts +14 -0
- package/dist/lib/exec.js +39 -5
- package/dist/lib/fuzzy.d.ts +12 -2
- package/dist/lib/fuzzy.js +29 -4
- package/dist/lib/git.js +8 -1
- package/dist/lib/hooks.d.ts +2 -2
- package/dist/lib/hooks.js +97 -10
- package/dist/lib/mcp.js +32 -2
- package/dist/lib/migrate.d.ts +51 -0
- package/dist/lib/migrate.js +227 -1
- package/dist/lib/models.js +62 -15
- package/dist/lib/permissions.d.ts +36 -2
- package/dist/lib/permissions.js +217 -7
- package/dist/lib/plugin-marketplace.d.ts +98 -40
- package/dist/lib/plugin-marketplace.js +196 -93
- package/dist/lib/plugins.d.ts +21 -4
- package/dist/lib/plugins.js +130 -49
- package/dist/lib/profiles-presets.js +12 -12
- package/dist/lib/project-launch.d.ts +65 -0
- package/dist/lib/project-launch.js +367 -0
- package/dist/lib/pty-client.js +1 -1
- package/dist/lib/pty-server.d.ts +1 -1
- package/dist/lib/pty-server.js +1 -1
- package/dist/lib/refresh.d.ts +26 -0
- package/dist/lib/refresh.js +315 -0
- package/dist/lib/resource-patterns.d.ts +1 -1
- package/dist/lib/resource-patterns.js +1 -1
- package/dist/lib/resources/commands.js +2 -2
- package/dist/lib/resources/hooks.d.ts +1 -1
- package/dist/lib/resources/hooks.js +1 -1
- package/dist/lib/resources/mcp.d.ts +1 -1
- package/dist/lib/resources/mcp.js +5 -6
- package/dist/lib/resources/permissions.js +5 -2
- package/dist/lib/resources/rules.js +3 -2
- package/dist/lib/resources/skills.js +3 -2
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.js +2 -2
- package/dist/lib/rotate.d.ts +1 -1
- package/dist/lib/rotate.js +1 -1
- package/dist/lib/routines.d.ts +16 -4
- package/dist/lib/routines.js +67 -17
- package/dist/lib/rules/compile.js +22 -10
- package/dist/lib/rules/rules.js +3 -3
- package/dist/lib/runner.js +16 -3
- package/dist/lib/scheduler.js +15 -1
- 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/Agents CLI.app/Contents/_CodeSignature/CodeResources +9 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
- package/dist/lib/secrets/linux.d.ts +44 -9
- package/dist/lib/secrets/linux.js +302 -48
- package/dist/lib/session/db.js +15 -2
- package/dist/lib/session/discover.js +118 -3
- package/dist/lib/session/parse.js +3 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +10 -9
- package/dist/lib/shims.js +101 -50
- package/dist/lib/skills.d.ts +1 -1
- package/dist/lib/skills.js +10 -9
- package/dist/lib/staleness/detectors/commands.d.ts +3 -0
- package/dist/lib/staleness/detectors/commands.js +46 -0
- package/dist/lib/staleness/detectors/hooks.d.ts +3 -0
- package/dist/lib/staleness/detectors/hooks.js +44 -0
- package/dist/lib/staleness/detectors/mcp.d.ts +3 -0
- package/dist/lib/staleness/detectors/mcp.js +31 -0
- package/dist/lib/staleness/detectors/permissions.d.ts +3 -0
- package/dist/lib/staleness/detectors/permissions.js +201 -0
- package/dist/lib/staleness/detectors/plugins.d.ts +8 -0
- package/dist/lib/staleness/detectors/plugins.js +23 -0
- package/dist/lib/staleness/detectors/rules.d.ts +3 -0
- package/dist/lib/staleness/detectors/rules.js +34 -0
- package/dist/lib/staleness/detectors/skills.d.ts +3 -0
- package/dist/lib/staleness/detectors/skills.js +71 -0
- package/dist/lib/staleness/detectors/subagents.d.ts +3 -0
- package/dist/lib/staleness/detectors/subagents.js +50 -0
- package/dist/lib/staleness/detectors/types.d.ts +22 -0
- package/dist/lib/staleness/detectors/types.js +1 -0
- package/dist/lib/staleness/detectors/workflows.d.ts +3 -0
- package/dist/lib/staleness/detectors/workflows.js +28 -0
- package/dist/lib/staleness/registry.d.ts +26 -0
- package/dist/lib/staleness/registry.js +123 -0
- package/dist/lib/staleness/writers/commands.d.ts +3 -0
- package/dist/lib/staleness/writers/commands.js +111 -0
- package/dist/lib/staleness/writers/hooks.d.ts +3 -0
- package/dist/lib/staleness/writers/hooks.js +47 -0
- package/dist/lib/staleness/writers/kinds.d.ts +10 -0
- package/dist/lib/staleness/writers/kinds.js +15 -0
- package/dist/lib/staleness/writers/lazy-map.d.ts +13 -0
- package/dist/lib/staleness/writers/lazy-map.js +19 -0
- package/dist/lib/staleness/writers/mcp.d.ts +10 -0
- package/dist/lib/staleness/writers/mcp.js +19 -0
- package/dist/lib/staleness/writers/permissions.d.ts +13 -0
- package/dist/lib/staleness/writers/permissions.js +26 -0
- package/dist/lib/staleness/writers/plugins.d.ts +7 -0
- package/dist/lib/staleness/writers/plugins.js +31 -0
- package/dist/lib/staleness/writers/rules.d.ts +7 -0
- package/dist/lib/staleness/writers/rules.js +55 -0
- package/dist/lib/staleness/writers/skills.d.ts +3 -0
- package/dist/lib/staleness/writers/skills.js +81 -0
- package/dist/lib/staleness/writers/sources.d.ts +16 -0
- package/dist/lib/staleness/writers/sources.js +72 -0
- package/dist/lib/staleness/writers/subagents.d.ts +3 -0
- package/dist/lib/staleness/writers/subagents.js +53 -0
- package/dist/lib/staleness/writers/types.d.ts +36 -0
- package/dist/lib/staleness/writers/types.js +1 -0
- package/dist/lib/staleness/writers/workflows.d.ts +7 -0
- package/dist/lib/staleness/writers/workflows.js +31 -0
- package/dist/lib/state.d.ts +34 -11
- package/dist/lib/state.js +58 -13
- package/dist/lib/subagents.d.ts +0 -2
- package/dist/lib/subagents.js +6 -6
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/tmux/binary.d.ts +67 -0
- package/dist/lib/tmux/binary.js +141 -0
- package/dist/lib/tmux/index.d.ts +8 -0
- package/dist/lib/tmux/index.js +8 -0
- package/dist/lib/tmux/paths.d.ts +17 -0
- package/dist/lib/tmux/paths.js +30 -0
- package/dist/lib/tmux/session.d.ts +122 -0
- package/dist/lib/tmux/session.js +305 -0
- package/dist/lib/types.d.ts +58 -7
- package/dist/lib/types.js +1 -1
- package/dist/lib/usage.js +1 -1
- package/dist/lib/versions.d.ts +4 -4
- package/dist/lib/versions.js +135 -493
- package/dist/lib/workflows.d.ts +2 -4
- package/dist/lib/workflows.js +3 -4
- package/package.json +2 -2
- package/scripts/postinstall.js +16 -63
- package/dist/commands/status.d.ts +0 -9
- package/dist/commands/status.js +0 -25
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Linux secret storage via libsecret (GNOME Keyring / Secret Service API).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* On Ubuntu: apt install libsecret-tools
|
|
4
|
+
* Primary backend: `secret-tool` CLI (libsecret-tools package).
|
|
6
5
|
*
|
|
7
|
-
*
|
|
6
|
+
* Headless fallback: when the default Secret Service collection is locked
|
|
7
|
+
* (common on server-class Linux — no graphical login means the keyring
|
|
8
|
+
* passphrase never enters the daemon, so `secret-tool store` fails with
|
|
9
|
+
* "Cannot create an item in a locked collection"), we transparently switch
|
|
10
|
+
* to a file-based AES-256-GCM encrypted store under
|
|
11
|
+
* `~/.agents/.cache/secrets/`. The encryption key is scrypt-derived from a
|
|
12
|
+
* passphrase read from `AGENTS_SECRETS_PASSPHRASE` (preferred) or a TTY
|
|
13
|
+
* prompt. The decision is cached per process; one stderr line is emitted
|
|
14
|
+
* the first time the fallback activates.
|
|
15
|
+
*
|
|
16
|
+
* Secrets stored via secret-tool use:
|
|
8
17
|
* service = "agents-cli"
|
|
9
18
|
* account = username
|
|
10
|
-
* item
|
|
19
|
+
* item = the secret identifier
|
|
20
|
+
*
|
|
21
|
+
* File-fallback layout: one `<item>.enc` JSON file per item, mode 0600.
|
|
11
22
|
*/
|
|
12
|
-
import { spawnSync } from 'child_process';
|
|
23
|
+
import { spawnSync, execSync } from 'child_process';
|
|
24
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
|
|
25
|
+
import * as fs from 'fs';
|
|
13
26
|
import * as os from 'os';
|
|
27
|
+
import * as path from 'path';
|
|
14
28
|
const SERVICE = 'agents-cli';
|
|
29
|
+
// ---------- secret-tool availability ----------
|
|
15
30
|
function secretToolAvailable() {
|
|
16
31
|
const result = spawnSync('which', ['secret-tool'], {
|
|
17
32
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -20,108 +35,334 @@ function secretToolAvailable() {
|
|
|
20
35
|
}
|
|
21
36
|
let checkedAvailability = false;
|
|
22
37
|
let isAvailable = false;
|
|
23
|
-
|
|
38
|
+
// ---------- file fallback state ----------
|
|
39
|
+
let useFileFallback = false;
|
|
40
|
+
let warnedFallback = false;
|
|
41
|
+
let fileDirOverride = null;
|
|
42
|
+
let cachedPassphrase = null;
|
|
43
|
+
function fileDir() {
|
|
44
|
+
return fileDirOverride ?? path.join(os.homedir(), '.agents', '.cache', 'secrets');
|
|
45
|
+
}
|
|
46
|
+
function activateFileFallback() {
|
|
47
|
+
if (useFileFallback)
|
|
48
|
+
return;
|
|
49
|
+
useFileFallback = true;
|
|
50
|
+
if (!warnedFallback) {
|
|
51
|
+
warnedFallback = true;
|
|
52
|
+
process.stderr.write(`[agents] secret-service collection locked, using file-based store at ${fileDir()}\n`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function isLockedCollectionError(stderr) {
|
|
56
|
+
return /locked collection/i.test(stderr) ||
|
|
57
|
+
/Prompt was dismissed/i.test(stderr);
|
|
58
|
+
}
|
|
59
|
+
/** True if the fallback dir has any committed encrypted items. Means an
|
|
60
|
+
* earlier process (this one or another) already routed writes to the file
|
|
61
|
+
* store, so this process must keep reading/writing from the same store —
|
|
62
|
+
* otherwise `list` / `get` / `has` would silently miss them. */
|
|
63
|
+
function fileFallbackPreviouslyActivated() {
|
|
64
|
+
try {
|
|
65
|
+
return fs.readdirSync(fileDir()).some((e) => e.endsWith('.enc'));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Decide which backend a given op should use. Activates file fallback if
|
|
73
|
+
* `secret-tool` is missing and `AGENTS_SECRETS_PASSPHRASE` is set, OR if a
|
|
74
|
+
* previous run already committed to the file fallback (encrypted items on
|
|
75
|
+
* disk). The latter check is what makes the fallback persistent across the
|
|
76
|
+
* many short-lived `agents secrets ...` Node processes a user invokes.
|
|
77
|
+
*/
|
|
78
|
+
function preflight() {
|
|
79
|
+
if (useFileFallback)
|
|
80
|
+
return 'file';
|
|
81
|
+
if (fileFallbackPreviouslyActivated()) {
|
|
82
|
+
activateFileFallback();
|
|
83
|
+
return 'file';
|
|
84
|
+
}
|
|
24
85
|
if (!checkedAvailability) {
|
|
25
86
|
isAvailable = secretToolAvailable();
|
|
26
87
|
checkedAvailability = true;
|
|
27
88
|
}
|
|
28
89
|
if (!isAvailable) {
|
|
90
|
+
if (process.env.AGENTS_SECRETS_PASSPHRASE) {
|
|
91
|
+
activateFileFallback();
|
|
92
|
+
return 'file';
|
|
93
|
+
}
|
|
29
94
|
throw new Error('secret-tool not found. Install libsecret-tools:\n' +
|
|
30
95
|
' Ubuntu/Debian: sudo apt install libsecret-tools\n' +
|
|
31
96
|
' Fedora: sudo dnf install libsecret\n' +
|
|
32
|
-
' Arch: sudo pacman -S libsecret'
|
|
97
|
+
' Arch: sudo pacman -S libsecret\n' +
|
|
98
|
+
'\n' +
|
|
99
|
+
'Alternative: set AGENTS_SECRETS_PASSPHRASE to use the encrypted-file fallback.');
|
|
33
100
|
}
|
|
101
|
+
return 'secret-tool';
|
|
34
102
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
103
|
+
// ---------- passphrase ----------
|
|
104
|
+
function readPassphraseFromTty() {
|
|
105
|
+
const fd = fs.openSync('/dev/tty', 'r+');
|
|
106
|
+
let echoDisabled = false;
|
|
107
|
+
try {
|
|
108
|
+
fs.writeSync(fd, 'Enter AGENTS_SECRETS_PASSPHRASE: ');
|
|
109
|
+
try {
|
|
110
|
+
execSync('stty -echo < /dev/tty', { stdio: 'ignore' });
|
|
111
|
+
echoDisabled = true;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// stty not available — fall through; passphrase will echo. Better
|
|
115
|
+
// than refusing to function.
|
|
116
|
+
}
|
|
117
|
+
let pass = '';
|
|
118
|
+
const buf = Buffer.alloc(1);
|
|
119
|
+
while (true) {
|
|
120
|
+
const n = fs.readSync(fd, buf, 0, 1, null);
|
|
121
|
+
if (n === 0)
|
|
122
|
+
break;
|
|
123
|
+
const ch = buf.toString('utf8', 0, n);
|
|
124
|
+
if (ch === '\n' || ch === '\r')
|
|
125
|
+
break;
|
|
126
|
+
pass += ch;
|
|
127
|
+
}
|
|
128
|
+
return pass;
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
if (echoDisabled) {
|
|
132
|
+
try {
|
|
133
|
+
execSync('stty echo < /dev/tty', { stdio: 'ignore' });
|
|
134
|
+
}
|
|
135
|
+
catch { /* best effort */ }
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
fs.writeSync(fd, '\n');
|
|
139
|
+
}
|
|
140
|
+
catch { /* best effort */ }
|
|
141
|
+
fs.closeSync(fd);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function getPassphrase() {
|
|
145
|
+
if (cachedPassphrase !== null)
|
|
146
|
+
return cachedPassphrase;
|
|
147
|
+
const env = process.env.AGENTS_SECRETS_PASSPHRASE;
|
|
148
|
+
if (env && env.length > 0) {
|
|
149
|
+
cachedPassphrase = env;
|
|
150
|
+
return env;
|
|
151
|
+
}
|
|
152
|
+
if (!process.stdin.isTTY) {
|
|
153
|
+
throw new Error('Secret-service collection is locked and no AGENTS_SECRETS_PASSPHRASE is set.\n' +
|
|
154
|
+
'Set AGENTS_SECRETS_PASSPHRASE in your environment to use the encrypted-file fallback,\n' +
|
|
155
|
+
'or unlock the keyring (e.g. configure pam_gnome_keyring for SSH login).');
|
|
156
|
+
}
|
|
157
|
+
const p = readPassphraseFromTty();
|
|
158
|
+
if (!p)
|
|
159
|
+
throw new Error('No passphrase entered.');
|
|
160
|
+
cachedPassphrase = p;
|
|
161
|
+
return p;
|
|
162
|
+
}
|
|
163
|
+
function deriveKey(passphrase, salt) {
|
|
164
|
+
return scryptSync(passphrase, salt, 32);
|
|
165
|
+
}
|
|
166
|
+
/** Encrypt plaintext under a passphrase using AES-256-GCM with a random
|
|
167
|
+
* scrypt salt and a random 96-bit IV. Exported for tests. */
|
|
168
|
+
export function encryptForFallback(plaintext, passphrase) {
|
|
169
|
+
const salt = randomBytes(16);
|
|
170
|
+
const iv = randomBytes(12);
|
|
171
|
+
const key = deriveKey(passphrase, salt);
|
|
172
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
173
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
174
|
+
return {
|
|
175
|
+
salt: salt.toString('hex'),
|
|
176
|
+
iv: iv.toString('hex'),
|
|
177
|
+
authTag: cipher.getAuthTag().toString('hex'),
|
|
178
|
+
ciphertext: ciphertext.toString('hex'),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** Decrypt an EncFile under a passphrase. Throws on wrong key or tampered
|
|
182
|
+
* ciphertext (auth-tag mismatch). Exported for tests. */
|
|
183
|
+
export function decryptForFallback(enc, passphrase) {
|
|
184
|
+
const salt = Buffer.from(enc.salt, 'hex');
|
|
185
|
+
const iv = Buffer.from(enc.iv, 'hex');
|
|
186
|
+
const authTag = Buffer.from(enc.authTag, 'hex');
|
|
187
|
+
const ciphertext = Buffer.from(enc.ciphertext, 'hex');
|
|
188
|
+
const key = deriveKey(passphrase, salt);
|
|
189
|
+
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
|
190
|
+
decipher.setAuthTag(authTag);
|
|
191
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
192
|
+
return plaintext.toString('utf8');
|
|
193
|
+
}
|
|
194
|
+
// ---------- file backend ----------
|
|
195
|
+
function fileFor(item) {
|
|
196
|
+
return path.join(fileDir(), `${item}.enc`);
|
|
197
|
+
}
|
|
198
|
+
function ensureFileDir() {
|
|
199
|
+
fs.mkdirSync(fileDir(), { recursive: true, mode: 0o700 });
|
|
200
|
+
}
|
|
201
|
+
function fileHas(item) {
|
|
202
|
+
return fs.existsSync(fileFor(item));
|
|
203
|
+
}
|
|
204
|
+
function fileGet(item) {
|
|
205
|
+
const fp = fileFor(item);
|
|
206
|
+
if (!fs.existsSync(fp)) {
|
|
207
|
+
throw new Error(`Secret '${item}' not found in encrypted store.`);
|
|
208
|
+
}
|
|
209
|
+
const raw = fs.readFileSync(fp, 'utf8');
|
|
210
|
+
let parsed;
|
|
211
|
+
try {
|
|
212
|
+
parsed = JSON.parse(raw);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
throw new Error(`Encrypted secret file ${fp} is corrupt (not valid JSON).`);
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
return decryptForFallback(parsed, getPassphrase());
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
throw new Error(`Failed to decrypt '${item}'. Wrong AGENTS_SECRETS_PASSPHRASE or tampered file.`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function fileSet(item, value) {
|
|
225
|
+
ensureFileDir();
|
|
226
|
+
const enc = encryptForFallback(value, getPassphrase());
|
|
227
|
+
fs.writeFileSync(fileFor(item), JSON.stringify(enc), { mode: 0o600 });
|
|
228
|
+
}
|
|
229
|
+
function fileDelete(item) {
|
|
230
|
+
const fp = fileFor(item);
|
|
231
|
+
if (!fs.existsSync(fp))
|
|
232
|
+
return true; // idempotent, matches secret-tool clear
|
|
233
|
+
fs.unlinkSync(fp);
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
function fileList(prefix) {
|
|
237
|
+
const dir = fileDir();
|
|
238
|
+
if (!fs.existsSync(dir))
|
|
239
|
+
return [];
|
|
240
|
+
return fs.readdirSync(dir)
|
|
241
|
+
.filter((f) => f.endsWith('.enc'))
|
|
242
|
+
.map((f) => f.slice(0, -'.enc'.length))
|
|
243
|
+
.filter((name) => name.startsWith(prefix));
|
|
244
|
+
}
|
|
245
|
+
/** File-only KeychainBackend (exported for tests; the public surface uses
|
|
246
|
+
* the secret-tool-with-fallback `linuxBackend` below). */
|
|
247
|
+
export const fileBackend = {
|
|
248
|
+
has: fileHas,
|
|
249
|
+
get: fileGet,
|
|
250
|
+
set: fileSet,
|
|
251
|
+
delete: fileDelete,
|
|
252
|
+
list: fileList,
|
|
253
|
+
};
|
|
254
|
+
// ---------- secret-tool ops with fallback ----------
|
|
255
|
+
/** secret-tool lookup attributes:
|
|
256
|
+
* service=agents-cli account=<user> item=<itemName> */
|
|
39
257
|
export function hasSecretToolToken(item) {
|
|
40
|
-
|
|
258
|
+
if (preflight() === 'file')
|
|
259
|
+
return fileHas(item);
|
|
41
260
|
const user = os.userInfo().username;
|
|
42
261
|
const result = spawnSync('secret-tool', [
|
|
43
262
|
'lookup',
|
|
44
263
|
'service', SERVICE,
|
|
45
264
|
'account', user,
|
|
46
265
|
'item', item,
|
|
47
|
-
], {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
266
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
267
|
+
if (result.status === 0) {
|
|
268
|
+
return result.stdout?.toString().trim().length > 0;
|
|
269
|
+
}
|
|
270
|
+
const stderr = result.stderr?.toString() ?? '';
|
|
271
|
+
if (isLockedCollectionError(stderr)) {
|
|
272
|
+
activateFileFallback();
|
|
273
|
+
return fileHas(item);
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
51
276
|
}
|
|
52
277
|
export function getSecretToolToken(item) {
|
|
53
|
-
|
|
278
|
+
if (preflight() === 'file')
|
|
279
|
+
return fileGet(item);
|
|
54
280
|
const user = os.userInfo().username;
|
|
55
281
|
const result = spawnSync('secret-tool', [
|
|
56
282
|
'lookup',
|
|
57
283
|
'service', SERVICE,
|
|
58
284
|
'account', user,
|
|
59
285
|
'item', item,
|
|
60
|
-
], {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
286
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
287
|
+
if (result.status === 0) {
|
|
288
|
+
const token = result.stdout?.toString().trim();
|
|
289
|
+
if (!token)
|
|
290
|
+
throw new Error(`Secret '${item}' exists but is empty.`);
|
|
291
|
+
return token;
|
|
65
292
|
}
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
68
|
-
|
|
293
|
+
const stderr = result.stderr?.toString() ?? '';
|
|
294
|
+
if (isLockedCollectionError(stderr)) {
|
|
295
|
+
activateFileFallback();
|
|
296
|
+
return fileGet(item);
|
|
69
297
|
}
|
|
70
|
-
|
|
298
|
+
throw new Error(`Secret '${item}' not found in keyring.`);
|
|
71
299
|
}
|
|
72
300
|
export function setSecretToolToken(item, value) {
|
|
73
|
-
ensureSecretTool();
|
|
74
301
|
if (!value || !value.trim())
|
|
75
302
|
throw new Error('Secret value is empty.');
|
|
303
|
+
if (preflight() === 'file')
|
|
304
|
+
return fileSet(item, value);
|
|
76
305
|
const user = os.userInfo().username;
|
|
77
306
|
const label = `agents-cli: ${item}`;
|
|
78
|
-
// secret-tool store reads value from stdin
|
|
79
307
|
const result = spawnSync('secret-tool', [
|
|
80
308
|
'store',
|
|
81
309
|
'--label', label,
|
|
82
310
|
'service', SERVICE,
|
|
83
311
|
'account', user,
|
|
84
312
|
'item', item,
|
|
85
|
-
], {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
313
|
+
], { input: value, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
314
|
+
if (result.status === 0)
|
|
315
|
+
return;
|
|
316
|
+
const stderr = result.stderr?.toString().trim() ?? '';
|
|
317
|
+
if (isLockedCollectionError(stderr)) {
|
|
318
|
+
activateFileFallback();
|
|
319
|
+
fileSet(item, value);
|
|
320
|
+
return;
|
|
93
321
|
}
|
|
322
|
+
throw new Error(`Failed to store secret '${item}': ${stderr || 'unknown error'}\n` +
|
|
323
|
+
'Make sure GNOME Keyring or another Secret Service provider is running,\n' +
|
|
324
|
+
'or set AGENTS_SECRETS_PASSPHRASE to use the encrypted-file fallback.');
|
|
94
325
|
}
|
|
95
326
|
export function deleteSecretToolToken(item) {
|
|
96
|
-
|
|
327
|
+
if (preflight() === 'file')
|
|
328
|
+
return fileDelete(item);
|
|
97
329
|
const user = os.userInfo().username;
|
|
98
330
|
const result = spawnSync('secret-tool', [
|
|
99
331
|
'clear',
|
|
100
332
|
'service', SERVICE,
|
|
101
333
|
'account', user,
|
|
102
334
|
'item', item,
|
|
103
|
-
], {
|
|
104
|
-
|
|
105
|
-
|
|
335
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
336
|
+
if (result.status === 0)
|
|
337
|
+
return true;
|
|
338
|
+
const stderr = result.stderr?.toString() ?? '';
|
|
339
|
+
if (isLockedCollectionError(stderr)) {
|
|
340
|
+
activateFileFallback();
|
|
341
|
+
return fileDelete(item);
|
|
342
|
+
}
|
|
106
343
|
// secret-tool clear returns 0 whether the item existed or not.
|
|
107
|
-
//
|
|
108
|
-
|
|
344
|
+
// A non-zero exit that isn't a locked-collection error is a real failure;
|
|
345
|
+
// surface that rather than silently swallowing.
|
|
346
|
+
return false;
|
|
109
347
|
}
|
|
110
348
|
/**
|
|
111
349
|
* List secrets by prefix. secret-tool doesn't have a list command,
|
|
112
350
|
* so we use secret-tool search which outputs in a specific format.
|
|
113
351
|
*/
|
|
114
352
|
export function listSecretToolItems(prefix) {
|
|
115
|
-
|
|
116
|
-
|
|
353
|
+
if (preflight() === 'file')
|
|
354
|
+
return fileList(prefix);
|
|
117
355
|
const result = spawnSync('secret-tool', [
|
|
118
356
|
'search',
|
|
119
357
|
'--all',
|
|
120
358
|
'service', SERVICE,
|
|
121
|
-
], {
|
|
122
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
123
|
-
});
|
|
359
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
124
360
|
if (result.status !== 0) {
|
|
361
|
+
const stderr = result.stderr?.toString() ?? '';
|
|
362
|
+
if (isLockedCollectionError(stderr)) {
|
|
363
|
+
activateFileFallback();
|
|
364
|
+
return fileList(prefix);
|
|
365
|
+
}
|
|
125
366
|
return [];
|
|
126
367
|
}
|
|
127
368
|
const output = result.stdout?.toString() || '';
|
|
@@ -141,7 +382,10 @@ export function listSecretToolItems(prefix) {
|
|
|
141
382
|
}
|
|
142
383
|
return [...new Set(items)]; // dedupe
|
|
143
384
|
}
|
|
144
|
-
/** KeychainBackend implementation for Linux
|
|
385
|
+
/** KeychainBackend implementation for Linux. Routes through secret-tool
|
|
386
|
+
* with a transparent encrypted-file fallback when the default Secret
|
|
387
|
+
* Service collection is locked (or libsecret-tools is not installed but
|
|
388
|
+
* AGENTS_SECRETS_PASSPHRASE is set). */
|
|
145
389
|
export const linuxBackend = {
|
|
146
390
|
has(item) {
|
|
147
391
|
return hasSecretToolToken(item);
|
|
@@ -159,3 +403,13 @@ export const linuxBackend = {
|
|
|
159
403
|
return listSecretToolItems(prefix);
|
|
160
404
|
},
|
|
161
405
|
};
|
|
406
|
+
/** Test-only: reset module state so independent test cases don't bleed
|
|
407
|
+
* passphrase / fallback decisions across each other. */
|
|
408
|
+
export function _resetForTest(opts = {}) {
|
|
409
|
+
fileDirOverride = opts.fileDir ?? null;
|
|
410
|
+
useFileFallback = opts.forceFileFallback ?? false;
|
|
411
|
+
warnedFallback = false;
|
|
412
|
+
cachedPassphrase = opts.passphrase ?? null;
|
|
413
|
+
checkedAvailability = false;
|
|
414
|
+
isAvailable = false;
|
|
415
|
+
}
|
package/dist/lib/session/db.js
CHANGED
|
@@ -605,10 +605,23 @@ function buildSessionWhere(options) {
|
|
|
605
605
|
export function querySessions(options = {}) {
|
|
606
606
|
const db = getDB();
|
|
607
607
|
const { clause, params } = buildSessionWhere(options);
|
|
608
|
-
|
|
608
|
+
// When a LIMIT is in play, we still need to filter stale rows AFTER the query,
|
|
609
|
+
// so over-fetch a small buffer. Without this, a page of 50 rows where the first
|
|
610
|
+
// 5 are stale would return only 45 to the caller even when there are more.
|
|
611
|
+
const limitClause = options.limit
|
|
612
|
+
? `LIMIT ${Math.max(1, Math.floor(options.limit)) + 16}`
|
|
613
|
+
: '';
|
|
609
614
|
const sql = `SELECT * FROM sessions ${clause} ORDER BY timestamp DESC ${limitClause}`;
|
|
610
615
|
const rows = db.prepare(sql).all(...params);
|
|
611
|
-
|
|
616
|
+
// Belt-and-suspenders: drop rows whose JSONL no longer exists on disk. The
|
|
617
|
+
// authoritative fix is to keep file_path in sync (see updateSessionFilePaths
|
|
618
|
+
// callers), but skipping vanished rows here prevents phantom sessions from
|
|
619
|
+
// surfacing in the Factory UI if any code path forgets to rewrite (#136).
|
|
620
|
+
// Synthetic rows (OpenClaw channels/cron — see scanOpenClawIncremental) carry
|
|
621
|
+
// an empty file_path and are exempt; they're keyed by CLI output, not files.
|
|
622
|
+
const live = rows.filter(r => !r.file_path || fs.existsSync(r.file_path));
|
|
623
|
+
const trimmed = options.limit ? live.slice(0, options.limit) : live;
|
|
624
|
+
return trimmed.map(rowToMeta);
|
|
612
625
|
}
|
|
613
626
|
/** Count sessions matching the given filter options. */
|
|
614
627
|
export function countSessions(options = {}) {
|
|
@@ -15,7 +15,7 @@ import { execFile } from 'child_process';
|
|
|
15
15
|
import { promisify } from 'util';
|
|
16
16
|
import { getAgentsDir, getHistoryDir } from '../state.js';
|
|
17
17
|
const execFileAsync = promisify(execFile);
|
|
18
|
-
import { AGENTS, getCliVersion } from '../agents.js';
|
|
18
|
+
import { AGENTS, agentConfigDirName, getCliVersion } from '../agents.js';
|
|
19
19
|
import { walkForFiles } from '../fs-walk.js';
|
|
20
20
|
import { getConfigSymlinkVersion } from '../shims.js';
|
|
21
21
|
import { SESSION_AGENTS } from './types.js';
|
|
@@ -61,6 +61,7 @@ export async function discoverSessions(options) {
|
|
|
61
61
|
case 'openclaw': return scanOpenClawIncremental();
|
|
62
62
|
case 'rush': return scanRushIncremental(onProgress);
|
|
63
63
|
case 'hermes': return scanHermesIncremental(onProgress);
|
|
64
|
+
case 'kimi': return scanKimiIncremental(onProgress);
|
|
64
65
|
}
|
|
65
66
|
}));
|
|
66
67
|
}
|
|
@@ -201,14 +202,18 @@ export function getAgentSessionDirs(agent, subdir) {
|
|
|
201
202
|
resolved.add(key);
|
|
202
203
|
dirs.push(dir);
|
|
203
204
|
}
|
|
204
|
-
|
|
205
|
+
// Config-dir name relative to home — handles nested layouts (antigravity →
|
|
206
|
+
// .gemini/antigravity-cli) and ~/.config agents (amp, goose) as well as kimi
|
|
207
|
+
// (.kimi-code). Falls back to `.${agent}` for ids not in the registry.
|
|
208
|
+
const configDirName = agent in AGENTS ? agentConfigDirName(agent) : `.${agent}`;
|
|
209
|
+
addDir(path.join(HOME, configDirName, subdir));
|
|
205
210
|
for (const root of VERSIONS_ROOTS) {
|
|
206
211
|
const versionsBase = path.join(root, 'versions', agent);
|
|
207
212
|
if (!fs.existsSync(versionsBase))
|
|
208
213
|
continue;
|
|
209
214
|
try {
|
|
210
215
|
for (const version of fs.readdirSync(versionsBase)) {
|
|
211
|
-
addDir(path.join(versionsBase, version, 'home',
|
|
216
|
+
addDir(path.join(versionsBase, version, 'home', configDirName, subdir));
|
|
212
217
|
}
|
|
213
218
|
}
|
|
214
219
|
catch { /* dir unreadable */ }
|
|
@@ -1574,6 +1579,116 @@ function sumKnownNumbers(values) {
|
|
|
1574
1579
|
// ---------------------------------------------------------------------------
|
|
1575
1580
|
// Time range parsing
|
|
1576
1581
|
// ---------------------------------------------------------------------------
|
|
1582
|
+
// ---------------------------------------------------------------------------
|
|
1583
|
+
// Kimi
|
|
1584
|
+
// ---------------------------------------------------------------------------
|
|
1585
|
+
// Kimi stores sessions under ~/.kimi-code/sessions/<workdir_hash>/session_<uuid>/.
|
|
1586
|
+
// Each session has state.json (metadata) and agents/main/wire.jsonl (conversation).
|
|
1587
|
+
// A session_index.jsonl at ~/.kimi-code/ maps session IDs to directories.
|
|
1588
|
+
/** Incrementally re-scan changed Kimi session state.json files and upsert into the DB. */
|
|
1589
|
+
async function scanKimiIncremental(onProgress) {
|
|
1590
|
+
const filePaths = [];
|
|
1591
|
+
for (const sessionsDir of getAgentSessionDirs('kimi', 'sessions')) {
|
|
1592
|
+
if (!fs.existsSync(sessionsDir))
|
|
1593
|
+
continue;
|
|
1594
|
+
let workDirNames;
|
|
1595
|
+
try {
|
|
1596
|
+
workDirNames = fs.readdirSync(sessionsDir);
|
|
1597
|
+
}
|
|
1598
|
+
catch {
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
for (const workDirName of workDirNames) {
|
|
1602
|
+
const workDir = path.join(sessionsDir, workDirName);
|
|
1603
|
+
const stat = safeStatSync(workDir);
|
|
1604
|
+
if (!stat?.isDirectory())
|
|
1605
|
+
continue;
|
|
1606
|
+
let sessionNames;
|
|
1607
|
+
try {
|
|
1608
|
+
sessionNames = fs.readdirSync(workDir);
|
|
1609
|
+
}
|
|
1610
|
+
catch {
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
for (const sessionName of sessionNames) {
|
|
1614
|
+
if (!sessionName.startsWith('session_'))
|
|
1615
|
+
continue;
|
|
1616
|
+
const statePath = path.join(workDir, sessionName, 'state.json');
|
|
1617
|
+
if (!fs.existsSync(statePath))
|
|
1618
|
+
continue;
|
|
1619
|
+
filePaths.push(statePath);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
const changed = filterChangedFiles(filePaths);
|
|
1624
|
+
if (changed.length === 0)
|
|
1625
|
+
return;
|
|
1626
|
+
onProgress?.({ agent: 'kimi', parsed: 0, total: changed.length });
|
|
1627
|
+
const scanEntries = [];
|
|
1628
|
+
const touched = [];
|
|
1629
|
+
const seen = new Set();
|
|
1630
|
+
let parsed = 0;
|
|
1631
|
+
for (const { filePath, scan } of changed) {
|
|
1632
|
+
try {
|
|
1633
|
+
const result = readKimiMeta(filePath);
|
|
1634
|
+
if (result && !seen.has(result.meta.id)) {
|
|
1635
|
+
seen.add(result.meta.id);
|
|
1636
|
+
scanEntries.push({ meta: result.meta, content: result.content, scan });
|
|
1637
|
+
}
|
|
1638
|
+
else {
|
|
1639
|
+
touched.push({ filePath, scan });
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
catch {
|
|
1643
|
+
touched.push({ filePath, scan });
|
|
1644
|
+
}
|
|
1645
|
+
parsed++;
|
|
1646
|
+
onProgress?.({ agent: 'kimi', parsed, total: changed.length });
|
|
1647
|
+
}
|
|
1648
|
+
upsertSessionsBatch(scanEntries);
|
|
1649
|
+
recordScans(touched);
|
|
1650
|
+
}
|
|
1651
|
+
/** Parse a single Kimi session state.json file to extract session metadata. */
|
|
1652
|
+
function readKimiMeta(filePath) {
|
|
1653
|
+
let state;
|
|
1654
|
+
try {
|
|
1655
|
+
state = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
1656
|
+
}
|
|
1657
|
+
catch {
|
|
1658
|
+
return null;
|
|
1659
|
+
}
|
|
1660
|
+
const sessionDir = path.dirname(filePath);
|
|
1661
|
+
const sessionId = path.basename(sessionDir);
|
|
1662
|
+
if (!sessionId.startsWith('session_'))
|
|
1663
|
+
return null;
|
|
1664
|
+
const title = typeof state.title === 'string' ? state.title : undefined;
|
|
1665
|
+
const lastPrompt = typeof state.lastPrompt === 'string' ? state.lastPrompt : undefined;
|
|
1666
|
+
const topic = title || lastPrompt || undefined;
|
|
1667
|
+
const createdAt = typeof state.createdAt === 'string' ? state.createdAt : undefined;
|
|
1668
|
+
const updatedAt = typeof state.updatedAt === 'string' ? state.updatedAt : undefined;
|
|
1669
|
+
const timestamp = updatedAt || createdAt;
|
|
1670
|
+
const shortId = sessionId.replace(/^session_/, '').slice(0, 8);
|
|
1671
|
+
// Try to infer project from session directory path
|
|
1672
|
+
// ~/.kimi-code/sessions/<workdir_hash>/session_<uuid>/
|
|
1673
|
+
const workDirName = path.basename(path.dirname(sessionDir));
|
|
1674
|
+
let project;
|
|
1675
|
+
if (workDirName.startsWith('wd_')) {
|
|
1676
|
+
const parts = workDirName.slice(3).split('_');
|
|
1677
|
+
if (parts.length >= 2) {
|
|
1678
|
+
project = parts.slice(0, -1).join('/');
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
const meta = {
|
|
1682
|
+
id: sessionId,
|
|
1683
|
+
shortId,
|
|
1684
|
+
agent: 'kimi',
|
|
1685
|
+
timestamp,
|
|
1686
|
+
project,
|
|
1687
|
+
filePath,
|
|
1688
|
+
topic,
|
|
1689
|
+
};
|
|
1690
|
+
return { meta, content: lastPrompt || '' };
|
|
1691
|
+
}
|
|
1577
1692
|
/** Parse a time filter string (relative like '7d' or ISO timestamp) into epoch milliseconds. */
|
|
1578
1693
|
export function parseTimeFilter(input) {
|
|
1579
1694
|
const relativeMatch = input.match(/^(\d+)([mhdw])$/i);
|
|
@@ -105,6 +105,9 @@ export function parseSession(filePath, agent) {
|
|
|
105
105
|
case 'hermes':
|
|
106
106
|
events = parseHermes(filePath);
|
|
107
107
|
break;
|
|
108
|
+
case 'kimi':
|
|
109
|
+
events = [];
|
|
110
|
+
break; // Kimi event parsing not implemented yet — discover.ts builds metadata only
|
|
108
111
|
}
|
|
109
112
|
// Chokepoint: every string field that originated in an untrusted session
|
|
110
113
|
// file gets stripped of terminal escapes here, so renderers downstream can
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* speaks these types.
|
|
8
8
|
*/
|
|
9
9
|
/** Agents that store session data on disk and can be discovered by `agents sessions`. */
|
|
10
|
-
export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok';
|
|
10
|
+
export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok' | 'kimi';
|
|
11
11
|
/** All agents with session discovery support, in display order. */
|
|
12
12
|
export declare const SESSION_AGENTS: SessionAgentId[];
|
|
13
13
|
/** A single normalized event within a session (message, tool call, thinking, etc.). */
|
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
* speaks these types.
|
|
8
8
|
*/
|
|
9
9
|
/** All agents with session discovery support, in display order. */
|
|
10
|
-
export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes', 'grok'];
|
|
10
|
+
export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi'];
|