@phnx-labs/agents-cli 1.20.46 → 1.20.48
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 +15 -1
- package/README.md +135 -12
- package/dist/commands/secrets-import.d.ts +18 -0
- package/dist/commands/secrets-import.js +74 -0
- package/dist/commands/secrets.js +2 -0
- package/dist/lib/daemon.js +31 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +16 -0
- package/dist/lib/menubar/install-menubar.js +43 -1
- package/dist/lib/secrets/fallback.d.ts +48 -0
- package/dist/lib/secrets/fallback.js +48 -0
- package/dist/lib/secrets/index.d.ts +11 -0
- package/dist/lib/secrets/index.js +20 -2
- package/dist/lib/secrets/linux.d.ts +7 -0
- package/dist/lib/secrets/linux.js +113 -5
- package/dist/lib/secrets/windows.d.ts +7 -0
- package/dist/lib/secrets/windows.js +110 -5
- package/dist/lib/self-heal/checks/shims.js +19 -1
- package/dist/lib/session/discover.js +41 -0
- package/dist/lib/shims.d.ts +22 -0
- package/dist/lib/shims.js +79 -0
- package/dist/lib/versions.d.ts +16 -0
- package/dist/lib/versions.js +83 -12
- package/package.json +3 -3
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* through the explicit export/import flow in src/lib/secrets/sync.ts
|
|
23
23
|
* rather than the system's cloud-keychain path.
|
|
24
24
|
*/
|
|
25
|
+
import type { NativeImportReport } from './fallback.js';
|
|
26
|
+
export type { NativeImportReport, NativeImportResult, NativeImportStatus } from './fallback.js';
|
|
25
27
|
/** Supported secret resolution backends. */
|
|
26
28
|
export type SecretProvider = 'keychain' | 'env' | 'file' | 'exec';
|
|
27
29
|
/** A typed reference to a secret, consisting of a provider and a provider-specific value. */
|
|
@@ -181,6 +183,15 @@ export declare function parseOrphanMigrationOutput(stdout: string): OrphanMigrat
|
|
|
181
183
|
* aborted" from "nothing to do" (empty array).
|
|
182
184
|
*/
|
|
183
185
|
export declare function migrateOrphanedKeychainItems(prefix: string): OrphanMigrationResult[];
|
|
186
|
+
/**
|
|
187
|
+
* Import agents-cli secrets from the native store (GNOME Keyring / Windows
|
|
188
|
+
* Credential Manager) into the encrypted file store — the Linux/Windows
|
|
189
|
+
* analogue of the macOS orphan/legacy migration, exposed as
|
|
190
|
+
* `agents secrets import-keyring`. Requires the native store to be
|
|
191
|
+
* reachable/unlocked; `commit=false` is a dry-run. macOS returns an empty
|
|
192
|
+
* report (it has no file fallback and uses `migrate-acl` instead).
|
|
193
|
+
*/
|
|
194
|
+
export declare function importNativeItems(prefix: string, commit: boolean): NativeImportReport;
|
|
184
195
|
/** Options controlling how secret refs are resolved. */
|
|
185
196
|
export interface ResolveOptions {
|
|
186
197
|
/** Translate a short keychain ID to a fully namespaced item name. */
|
|
@@ -26,8 +26,8 @@ import { execFileSync, spawnSync } from 'child_process';
|
|
|
26
26
|
import * as fs from 'fs';
|
|
27
27
|
import * as os from 'os';
|
|
28
28
|
import * as path from 'path';
|
|
29
|
-
import { linuxBackend, usesFileFallback as linuxUsesFileFallback } from './linux.js';
|
|
30
|
-
import { windowsBackend, usesFileFallback as windowsUsesFileFallback } from './windows.js';
|
|
29
|
+
import { linuxBackend, usesFileFallback as linuxUsesFileFallback, importNativeSecretToolItems } from './linux.js';
|
|
30
|
+
import { windowsBackend, usesFileFallback as windowsUsesFileFallback, importNativeCredManItems } from './windows.js';
|
|
31
31
|
import { getKeychainHelperPath } from './install-helper.js';
|
|
32
32
|
const SERVICE_PREFIX = 'agents-cli';
|
|
33
33
|
const SECRETS_ITEM_PREFIX = `${SERVICE_PREFIX}.secrets.`;
|
|
@@ -518,6 +518,24 @@ export function migrateOrphanedKeychainItems(prefix) {
|
|
|
518
518
|
}
|
|
519
519
|
return parseOrphanMigrationOutput(result.stdout?.toString() || '');
|
|
520
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* Import agents-cli secrets from the native store (GNOME Keyring / Windows
|
|
523
|
+
* Credential Manager) into the encrypted file store — the Linux/Windows
|
|
524
|
+
* analogue of the macOS orphan/legacy migration, exposed as
|
|
525
|
+
* `agents secrets import-keyring`. Requires the native store to be
|
|
526
|
+
* reachable/unlocked; `commit=false` is a dry-run. macOS returns an empty
|
|
527
|
+
* report (it has no file fallback and uses `migrate-acl` instead).
|
|
528
|
+
*/
|
|
529
|
+
export function importNativeItems(prefix, commit) {
|
|
530
|
+
if (backend)
|
|
531
|
+
return { available: false, locked: false, results: [] };
|
|
532
|
+
assertSupportedPlatform();
|
|
533
|
+
if (isLinux())
|
|
534
|
+
return importNativeSecretToolItems(prefix, commit);
|
|
535
|
+
if (isWindows())
|
|
536
|
+
return importNativeCredManItems(prefix, commit);
|
|
537
|
+
return { available: false, locked: false, results: [] };
|
|
538
|
+
}
|
|
521
539
|
function expandHome(p) {
|
|
522
540
|
if (p.startsWith('~/') || p === '~') {
|
|
523
541
|
return path.join(os.homedir(), p.slice(1));
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* item = the secret identifier
|
|
18
18
|
*/
|
|
19
19
|
import type { KeychainBackend } from './index.js';
|
|
20
|
+
import { type NativeImportReport } from './fallback.js';
|
|
20
21
|
export { encryptForFallback, decryptForFallback, fileBackend, type EncFile, } from './filestore.js';
|
|
21
22
|
/**
|
|
22
23
|
* True when secret operations currently route to the encrypted-file store
|
|
@@ -55,6 +56,12 @@ export declare function parseSecretToolItems(output: string, prefix: string): st
|
|
|
55
56
|
* so we use secret-tool search which outputs in a specific format.
|
|
56
57
|
*/
|
|
57
58
|
export declare function listSecretToolItems(prefix: string): string[];
|
|
59
|
+
/**
|
|
60
|
+
* Copy agents-cli items from the keyring into the file store (the `import-keyring`
|
|
61
|
+
* backend for Linux). Requires an unlocked keyring; items already in the file
|
|
62
|
+
* store are left untouched. With `commit=false` it reports what it *would* do.
|
|
63
|
+
*/
|
|
64
|
+
export declare function importNativeSecretToolItems(prefix: string, commit: boolean): NativeImportReport;
|
|
58
65
|
/** KeychainBackend implementation for Linux. Routes through secret-tool
|
|
59
66
|
* with a transparent encrypted-file fallback when the default Secret
|
|
60
67
|
* Service collection is locked (or libsecret-tools is not installed but
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { spawnSync } from 'child_process';
|
|
20
20
|
import * as os from 'os';
|
|
21
21
|
import { fileStore, fileDir, fileStoreHasItems, machinePassphraseExists, _resetFileStoreForTest, } from './filestore.js';
|
|
22
|
+
import { noteNativeShadow, _resetFallbackNoticeForTest, } from './fallback.js';
|
|
22
23
|
// Re-exported so existing importers (and tests) can keep reaching these via
|
|
23
24
|
// './linux.js'. The implementations live in ./filestore.ts.
|
|
24
25
|
export { encryptForFallback, decryptForFallback, fileBackend, } from './filestore.js';
|
|
@@ -35,13 +36,16 @@ let isAvailable = false;
|
|
|
35
36
|
// ---------- file fallback state ----------
|
|
36
37
|
let useFileFallback = false;
|
|
37
38
|
let warnedFallback = false;
|
|
39
|
+
// Set once the keyring is observed locked/unreachable in this process, so the
|
|
40
|
+
// read-through in get/has stops re-probing it (and stops re-emitting notices).
|
|
41
|
+
let nativeUnreachable = false;
|
|
38
42
|
function activateFileFallback() {
|
|
39
43
|
if (useFileFallback)
|
|
40
44
|
return;
|
|
41
45
|
useFileFallback = true;
|
|
42
46
|
if (!warnedFallback) {
|
|
43
47
|
warnedFallback = true;
|
|
44
|
-
process.stderr.write(`[agents]
|
|
48
|
+
process.stderr.write(`[agents] using the encrypted file store at ${fileDir()}\n`);
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
function isLockedCollectionError(stderr) {
|
|
@@ -111,8 +115,21 @@ export function usesFileFallback() {
|
|
|
111
115
|
/** secret-tool lookup attributes:
|
|
112
116
|
* service=agents-cli account=<user> item=<itemName> */
|
|
113
117
|
export function hasSecretToolToken(item) {
|
|
114
|
-
if (preflight() === 'file')
|
|
115
|
-
|
|
118
|
+
if (preflight() === 'file') {
|
|
119
|
+
if (fileStore.has(item))
|
|
120
|
+
return true;
|
|
121
|
+
// The file store is primary under the fallback, but an item can still live
|
|
122
|
+
// only in an (unlocked) keyring that predates it — read through so it isn't
|
|
123
|
+
// silently shadowed.
|
|
124
|
+
const probe = readNativeItemRaw(item);
|
|
125
|
+
if (probe.value !== undefined) {
|
|
126
|
+
noteNativeShadow('shadowed', fileDir());
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
if (probe.locked)
|
|
130
|
+
noteNativeShadow('locked', fileDir());
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
116
133
|
const user = os.userInfo().username;
|
|
117
134
|
const result = spawnSync('secret-tool', [
|
|
118
135
|
'lookup',
|
|
@@ -125,14 +142,25 @@ export function hasSecretToolToken(item) {
|
|
|
125
142
|
}
|
|
126
143
|
const stderr = result.stderr?.toString() ?? '';
|
|
127
144
|
if (isLockedCollectionError(stderr)) {
|
|
145
|
+
nativeUnreachable = true;
|
|
128
146
|
activateFileFallback();
|
|
129
147
|
return fileStore.has(item);
|
|
130
148
|
}
|
|
131
149
|
return false;
|
|
132
150
|
}
|
|
133
151
|
export function getSecretToolToken(item) {
|
|
134
|
-
if (preflight() === 'file')
|
|
135
|
-
|
|
152
|
+
if (preflight() === 'file') {
|
|
153
|
+
if (fileStore.has(item))
|
|
154
|
+
return fileStore.get(item);
|
|
155
|
+
const probe = readNativeItemRaw(item);
|
|
156
|
+
if (probe.value !== undefined) {
|
|
157
|
+
noteNativeShadow('shadowed', fileDir());
|
|
158
|
+
return probe.value;
|
|
159
|
+
}
|
|
160
|
+
if (probe.locked)
|
|
161
|
+
noteNativeShadow('locked', fileDir());
|
|
162
|
+
throw new Error(`Secret '${item}' not found in the file store or keyring.`);
|
|
163
|
+
}
|
|
136
164
|
const user = os.userInfo().username;
|
|
137
165
|
const result = spawnSync('secret-tool', [
|
|
138
166
|
'lookup',
|
|
@@ -148,6 +176,7 @@ export function getSecretToolToken(item) {
|
|
|
148
176
|
}
|
|
149
177
|
const stderr = result.stderr?.toString() ?? '';
|
|
150
178
|
if (isLockedCollectionError(stderr)) {
|
|
179
|
+
nativeUnreachable = true;
|
|
151
180
|
activateFileFallback();
|
|
152
181
|
return fileStore.get(item);
|
|
153
182
|
}
|
|
@@ -171,6 +200,7 @@ export function setSecretToolToken(item, value) {
|
|
|
171
200
|
return;
|
|
172
201
|
const stderr = result.stderr?.toString().trim() ?? '';
|
|
173
202
|
if (isLockedCollectionError(stderr)) {
|
|
203
|
+
nativeUnreachable = true;
|
|
174
204
|
activateFileFallback();
|
|
175
205
|
fileStore.set(item, value);
|
|
176
206
|
return;
|
|
@@ -193,6 +223,7 @@ export function deleteSecretToolToken(item) {
|
|
|
193
223
|
return true;
|
|
194
224
|
const stderr = result.stderr?.toString() ?? '';
|
|
195
225
|
if (isLockedCollectionError(stderr)) {
|
|
226
|
+
nativeUnreachable = true;
|
|
196
227
|
activateFileFallback();
|
|
197
228
|
return fileStore.delete(item);
|
|
198
229
|
}
|
|
@@ -252,6 +283,81 @@ export function listSecretToolItems(prefix) {
|
|
|
252
283
|
const output = `${result.stdout?.toString() || ''}\n${result.stderr?.toString() || ''}`;
|
|
253
284
|
return parseSecretToolItems(output, prefix);
|
|
254
285
|
}
|
|
286
|
+
// ---------- native-direct helpers (bypass preflight routing) ----------
|
|
287
|
+
//
|
|
288
|
+
// These always talk to secret-tool regardless of whether the process has fallen
|
|
289
|
+
// back to the file store. They power (a) the read-through that keeps the file
|
|
290
|
+
// store from silently shadowing keyring items, and (b) `import-keyring`.
|
|
291
|
+
/**
|
|
292
|
+
* Read one item straight from the keyring. Returns `{value}` on a hit,
|
|
293
|
+
* `{locked:true}` when the collection is locked/unreachable, and `{}` on a plain
|
|
294
|
+
* miss. Never throws and never emits — the caller decides whether to notice.
|
|
295
|
+
*/
|
|
296
|
+
function readNativeItemRaw(item) {
|
|
297
|
+
if (nativeUnreachable)
|
|
298
|
+
return { locked: true };
|
|
299
|
+
if (!secretToolAvailable())
|
|
300
|
+
return {};
|
|
301
|
+
const user = os.userInfo().username;
|
|
302
|
+
const r = spawnSync('secret-tool', [
|
|
303
|
+
'lookup', 'service', SERVICE, 'account', user, 'item', item,
|
|
304
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
305
|
+
if (r.status === 0) {
|
|
306
|
+
const v = r.stdout?.toString().trim();
|
|
307
|
+
return v && v.length ? { value: v } : {};
|
|
308
|
+
}
|
|
309
|
+
if (isLockedCollectionError(r.stderr?.toString() ?? '')) {
|
|
310
|
+
nativeUnreachable = true;
|
|
311
|
+
return { locked: true };
|
|
312
|
+
}
|
|
313
|
+
return {};
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Enumerate agents-cli items in the keyring whose name starts with `prefix`.
|
|
317
|
+
* `available` is false when secret-tool isn't installed; `locked` is true when
|
|
318
|
+
* the collection is locked.
|
|
319
|
+
*/
|
|
320
|
+
function listNativeItemsRaw(prefix) {
|
|
321
|
+
if (!secretToolAvailable())
|
|
322
|
+
return { items: [], locked: false, available: false };
|
|
323
|
+
const r = spawnSync('secret-tool', [
|
|
324
|
+
'search', '--all', 'service', SERVICE,
|
|
325
|
+
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
326
|
+
if (r.status !== 0) {
|
|
327
|
+
const locked = isLockedCollectionError(r.stderr?.toString() ?? '');
|
|
328
|
+
if (locked)
|
|
329
|
+
nativeUnreachable = true;
|
|
330
|
+
return { items: [], locked, available: true };
|
|
331
|
+
}
|
|
332
|
+
const output = `${r.stdout?.toString() || ''}\n${r.stderr?.toString() || ''}`;
|
|
333
|
+
return { items: parseSecretToolItems(output, prefix), locked: false, available: true };
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Copy agents-cli items from the keyring into the file store (the `import-keyring`
|
|
337
|
+
* backend for Linux). Requires an unlocked keyring; items already in the file
|
|
338
|
+
* store are left untouched. With `commit=false` it reports what it *would* do.
|
|
339
|
+
*/
|
|
340
|
+
export function importNativeSecretToolItems(prefix, commit) {
|
|
341
|
+
const { items, locked, available } = listNativeItemsRaw(prefix);
|
|
342
|
+
if (!available || locked)
|
|
343
|
+
return { available, locked, results: [] };
|
|
344
|
+
const results = [];
|
|
345
|
+
for (const item of items) {
|
|
346
|
+
if (fileStore.has(item)) {
|
|
347
|
+
results.push({ item, status: 'exists' });
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const probe = readNativeItemRaw(item);
|
|
351
|
+
if (probe.value === undefined) {
|
|
352
|
+
results.push({ item, status: 'failed', detail: probe.locked ? 'keyring locked' : 'unreadable' });
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (commit)
|
|
356
|
+
fileStore.set(item, probe.value);
|
|
357
|
+
results.push({ item, status: commit ? 'imported' : 'would-import' });
|
|
358
|
+
}
|
|
359
|
+
return { available, locked, results };
|
|
360
|
+
}
|
|
255
361
|
/** KeychainBackend implementation for Linux. Routes through secret-tool
|
|
256
362
|
* with a transparent encrypted-file fallback when the default Secret
|
|
257
363
|
* Service collection is locked (or libsecret-tools is not installed but
|
|
@@ -280,6 +386,8 @@ export function _resetForTest(opts = {}) {
|
|
|
280
386
|
_resetFileStoreForTest({ fileDir: opts.fileDir ?? null, passphrase: opts.passphrase ?? null });
|
|
281
387
|
useFileFallback = opts.forceFileFallback ?? false;
|
|
282
388
|
warnedFallback = false;
|
|
389
|
+
nativeUnreachable = false;
|
|
283
390
|
checkedAvailability = false;
|
|
284
391
|
isAvailable = false;
|
|
392
|
+
_resetFallbackNoticeForTest();
|
|
285
393
|
}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* names directly.
|
|
28
28
|
*/
|
|
29
29
|
import type { KeychainBackend } from './index.js';
|
|
30
|
+
import { type NativeImportReport } from './fallback.js';
|
|
30
31
|
export { encryptForFallback, decryptForFallback, fileBackend, type EncFile, } from './filestore.js';
|
|
31
32
|
/**
|
|
32
33
|
* CRED_MAX_CREDENTIAL_BLOB_SIZE — Credential Manager rejects a generic
|
|
@@ -53,6 +54,12 @@ export declare function listCredManItems(prefix: string): string[];
|
|
|
53
54
|
* parseSecretToolItems (linux.ts). Exported for tests.
|
|
54
55
|
*/
|
|
55
56
|
export declare function parseWindowsCredList(output: string, prefix: string): string[];
|
|
57
|
+
/**
|
|
58
|
+
* Copy agents-cli credentials from Credential Manager into the file store (the
|
|
59
|
+
* `import-keyring` backend for Windows). Requires a reachable store; items
|
|
60
|
+
* already in the file store are left untouched.
|
|
61
|
+
*/
|
|
62
|
+
export declare function importNativeCredManItems(prefix: string, commit: boolean): NativeImportReport;
|
|
56
63
|
/**
|
|
57
64
|
* KeychainBackend implementation for Windows. Routes through Windows Credential
|
|
58
65
|
* Manager (via PowerShell P/Invoke) with a transparent encrypted-file fallback
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import { spawnSync } from 'child_process';
|
|
30
30
|
import { encodePwshBase64 } from '../pwsh.js';
|
|
31
31
|
import { fileStore, fileDir, fileStoreHasItems, machinePassphraseExists, _resetFileStoreForTest, } from './filestore.js';
|
|
32
|
+
import { noteNativeShadow, _resetFallbackNoticeForTest, } from './fallback.js';
|
|
32
33
|
// Re-exported so importers (and tests) can keep reaching these via './windows.js'.
|
|
33
34
|
export { encryptForFallback, decryptForFallback, fileBackend, } from './filestore.js';
|
|
34
35
|
const POWERSHELL = 'powershell.exe';
|
|
@@ -250,13 +251,16 @@ let isAvailable = false;
|
|
|
250
251
|
// ---------- file fallback state ----------
|
|
251
252
|
let useFileFallback = false;
|
|
252
253
|
let warnedFallback = false;
|
|
254
|
+
// Set once Credential Manager is observed unreachable in this process, so the
|
|
255
|
+
// read-through in get/has stops re-probing it (and re-emitting notices).
|
|
256
|
+
let nativeUnreachable = false;
|
|
253
257
|
function activateFileFallback() {
|
|
254
258
|
if (useFileFallback)
|
|
255
259
|
return;
|
|
256
260
|
useFileFallback = true;
|
|
257
261
|
if (!warnedFallback) {
|
|
258
262
|
warnedFallback = true;
|
|
259
|
-
process.stderr.write(`[agents]
|
|
263
|
+
process.stderr.write(`[agents] using the encrypted file store at ${fileDir()}\n`);
|
|
260
264
|
}
|
|
261
265
|
}
|
|
262
266
|
/**
|
|
@@ -316,22 +320,45 @@ export function usesFileFallback() {
|
|
|
316
320
|
}
|
|
317
321
|
// ---------- Credential Manager ops with fallback ----------
|
|
318
322
|
export function hasCredManToken(item) {
|
|
319
|
-
if (preflight() === 'file')
|
|
320
|
-
|
|
323
|
+
if (preflight() === 'file') {
|
|
324
|
+
if (fileStore.has(item))
|
|
325
|
+
return true;
|
|
326
|
+
// Read through to Credential Manager so an item that predates the fallback
|
|
327
|
+
// isn't silently shadowed by the file store.
|
|
328
|
+
const probe = readNativeCredItemRaw(item);
|
|
329
|
+
if (probe.value !== undefined) {
|
|
330
|
+
noteNativeShadow('shadowed', fileDir());
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
if (probe.unavailable)
|
|
334
|
+
noteNativeShadow('locked', fileDir());
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
321
337
|
const r = runCred('has', { target: item });
|
|
322
338
|
if (r.status === 0)
|
|
323
339
|
return true;
|
|
324
340
|
if (r.status === 3)
|
|
325
341
|
return false;
|
|
326
342
|
if (isCredManUnavailableError(r)) {
|
|
343
|
+
nativeUnreachable = true;
|
|
327
344
|
activateFileFallback();
|
|
328
345
|
return fileStore.has(item);
|
|
329
346
|
}
|
|
330
347
|
return false;
|
|
331
348
|
}
|
|
332
349
|
export function getCredManToken(item) {
|
|
333
|
-
if (preflight() === 'file')
|
|
334
|
-
|
|
350
|
+
if (preflight() === 'file') {
|
|
351
|
+
if (fileStore.has(item))
|
|
352
|
+
return fileStore.get(item);
|
|
353
|
+
const probe = readNativeCredItemRaw(item);
|
|
354
|
+
if (probe.value !== undefined) {
|
|
355
|
+
noteNativeShadow('shadowed', fileDir());
|
|
356
|
+
return probe.value;
|
|
357
|
+
}
|
|
358
|
+
if (probe.unavailable)
|
|
359
|
+
noteNativeShadow('locked', fileDir());
|
|
360
|
+
throw new Error(`Secret '${item}' not found in the file store or Credential Manager.`);
|
|
361
|
+
}
|
|
335
362
|
const r = runCred('get', { target: item });
|
|
336
363
|
if (r.status === 0) {
|
|
337
364
|
// stdout is base64 of the raw UTF-8 blob (dodges PowerShell encoding corruption).
|
|
@@ -340,6 +367,7 @@ export function getCredManToken(item) {
|
|
|
340
367
|
if (r.status === 3)
|
|
341
368
|
throw new Error(`Secret '${item}' not found in Credential Manager.`);
|
|
342
369
|
if (isCredManUnavailableError(r)) {
|
|
370
|
+
nativeUnreachable = true;
|
|
343
371
|
activateFileFallback();
|
|
344
372
|
return fileStore.get(item);
|
|
345
373
|
}
|
|
@@ -362,6 +390,7 @@ export function setCredManToken(item, value) {
|
|
|
362
390
|
if (r.status === 0)
|
|
363
391
|
return;
|
|
364
392
|
if (isCredManUnavailableError(r)) {
|
|
393
|
+
nativeUnreachable = true;
|
|
365
394
|
activateFileFallback();
|
|
366
395
|
fileStore.set(item, value);
|
|
367
396
|
return;
|
|
@@ -377,6 +406,7 @@ export function deleteCredManToken(item) {
|
|
|
377
406
|
if (r.status === 3)
|
|
378
407
|
return false;
|
|
379
408
|
if (isCredManUnavailableError(r)) {
|
|
409
|
+
nativeUnreachable = true;
|
|
380
410
|
activateFileFallback();
|
|
381
411
|
return fileStore.delete(item);
|
|
382
412
|
}
|
|
@@ -389,6 +419,7 @@ export function listCredManItems(prefix) {
|
|
|
389
419
|
if (r.status === 0)
|
|
390
420
|
return parseWindowsCredList(r.stdout, prefix);
|
|
391
421
|
if (isCredManUnavailableError(r)) {
|
|
422
|
+
nativeUnreachable = true;
|
|
392
423
|
activateFileFallback();
|
|
393
424
|
return fileStore.list(prefix);
|
|
394
425
|
}
|
|
@@ -407,6 +438,78 @@ export function parseWindowsCredList(output, prefix) {
|
|
|
407
438
|
.filter((s) => s.startsWith(prefix));
|
|
408
439
|
return [...new Set(items)]; // dedupe
|
|
409
440
|
}
|
|
441
|
+
// ---------- native-direct helpers (bypass preflight routing) ----------
|
|
442
|
+
//
|
|
443
|
+
// Always talk to Credential Manager regardless of the file fallback. They power
|
|
444
|
+
// (a) the read-through that keeps the file store from shadowing credman items,
|
|
445
|
+
// and (b) `import-keyring`.
|
|
446
|
+
/**
|
|
447
|
+
* Read one item straight from Credential Manager. `{value}` on hit,
|
|
448
|
+
* `{unavailable:true}` when the store is unreachable, `{}` on a plain miss.
|
|
449
|
+
* Never throws, never emits.
|
|
450
|
+
*/
|
|
451
|
+
function readNativeCredItemRaw(item) {
|
|
452
|
+
if (nativeUnreachable)
|
|
453
|
+
return { unavailable: true };
|
|
454
|
+
if (!powershellAvailable())
|
|
455
|
+
return {};
|
|
456
|
+
const r = runCred('get', { target: item });
|
|
457
|
+
if (r.status === 0)
|
|
458
|
+
return { value: Buffer.from(r.stdout.trim(), 'base64').toString('utf8') };
|
|
459
|
+
if (r.status === 3)
|
|
460
|
+
return {};
|
|
461
|
+
if (isCredManUnavailableError(r)) {
|
|
462
|
+
nativeUnreachable = true;
|
|
463
|
+
return { unavailable: true };
|
|
464
|
+
}
|
|
465
|
+
return {};
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Enumerate agents-cli credentials under `prefix`. Windows credentials have no
|
|
469
|
+
* service scoping — the target IS the identifier — so we NEVER enumerate with an
|
|
470
|
+
* empty filter (that returns unrelated machine credentials). The filter is
|
|
471
|
+
* floored to the `agents-cli.` namespace; bare items (unprefixed targets) are
|
|
472
|
+
* therefore out of scope for auto-discovery on Windows.
|
|
473
|
+
*/
|
|
474
|
+
function listNativeCredItemsRaw(prefix) {
|
|
475
|
+
if (!powershellAvailable())
|
|
476
|
+
return { items: [], locked: false, available: false };
|
|
477
|
+
const floor = prefix && prefix.startsWith('agents-cli.') ? prefix : 'agents-cli.';
|
|
478
|
+
const r = runCred('list', { prefix: floor });
|
|
479
|
+
if (r.status === 0)
|
|
480
|
+
return { items: parseWindowsCredList(r.stdout, floor), locked: false, available: true };
|
|
481
|
+
if (isCredManUnavailableError(r)) {
|
|
482
|
+
nativeUnreachable = true;
|
|
483
|
+
return { items: [], locked: true, available: true };
|
|
484
|
+
}
|
|
485
|
+
return { items: [], locked: false, available: true };
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Copy agents-cli credentials from Credential Manager into the file store (the
|
|
489
|
+
* `import-keyring` backend for Windows). Requires a reachable store; items
|
|
490
|
+
* already in the file store are left untouched.
|
|
491
|
+
*/
|
|
492
|
+
export function importNativeCredManItems(prefix, commit) {
|
|
493
|
+
const { items, locked, available } = listNativeCredItemsRaw(prefix);
|
|
494
|
+
if (!available || locked)
|
|
495
|
+
return { available, locked, results: [] };
|
|
496
|
+
const results = [];
|
|
497
|
+
for (const item of items) {
|
|
498
|
+
if (fileStore.has(item)) {
|
|
499
|
+
results.push({ item, status: 'exists' });
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const probe = readNativeCredItemRaw(item);
|
|
503
|
+
if (probe.value === undefined) {
|
|
504
|
+
results.push({ item, status: 'failed', detail: probe.unavailable ? 'credential manager unavailable' : 'unreadable' });
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (commit)
|
|
508
|
+
fileStore.set(item, probe.value);
|
|
509
|
+
results.push({ item, status: commit ? 'imported' : 'would-import' });
|
|
510
|
+
}
|
|
511
|
+
return { available, locked, results };
|
|
512
|
+
}
|
|
410
513
|
/**
|
|
411
514
|
* KeychainBackend implementation for Windows. Routes through Windows Credential
|
|
412
515
|
* Manager (via PowerShell P/Invoke) with a transparent encrypted-file fallback
|
|
@@ -440,6 +543,8 @@ export function _resetForTest(opts = {}) {
|
|
|
440
543
|
_resetFileStoreForTest({ fileDir: opts.fileDir ?? null, passphrase: opts.passphrase ?? null });
|
|
441
544
|
useFileFallback = opts.forceFileFallback ?? false;
|
|
442
545
|
warnedFallback = false;
|
|
546
|
+
nativeUnreachable = false;
|
|
547
|
+
_resetFallbackNoticeForTest();
|
|
443
548
|
if (opts.forceAvailable === undefined || opts.forceAvailable === null) {
|
|
444
549
|
checkedAvailability = false;
|
|
445
550
|
isAvailable = false;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// background so the shim schema settles without user-facing churn.
|
|
5
5
|
import { resultOf } from '../types.js';
|
|
6
6
|
import { AGENTS } from '../../agents.js';
|
|
7
|
-
import { ensureShimCurrent, ensureVersionedAliasCurrent, isShimCurrent, isVersionedAliasCurrent, removeLegacyUserShim, listAgentsWithInstalledVersions, } from '../../shims.js';
|
|
7
|
+
import { createShim, ensureShimCurrent, ensureVersionedAliasCurrent, isShimCurrent, isVersionedAliasCurrent, shimPointsAtLiveInstall, removeLegacyUserShim, listAgentsWithInstalledVersions, listShimFileNames, pruneOrphanedCommandShim, } from '../../shims.js';
|
|
8
8
|
import { listInstalledVersions } from '../../versions.js';
|
|
9
9
|
export const shimsCheck = {
|
|
10
10
|
id: 'shims',
|
|
@@ -19,6 +19,14 @@ export const shimsCheck = {
|
|
|
19
19
|
ensureShimCurrent(agent);
|
|
20
20
|
fixed.push(`${cmd} shim`);
|
|
21
21
|
}
|
|
22
|
+
else if (!shimPointsAtLiveInstall(agent)) {
|
|
23
|
+
// Schema is current but the baked AGENTS_BIN points at a different, removed
|
|
24
|
+
// install (dev build, old npm-global, rotated version dir). ensureShimCurrent
|
|
25
|
+
// would no-op on a schema-current shim, so force a rewrite to the current install.
|
|
26
|
+
if (!ctx.dryRun)
|
|
27
|
+
createShim(agent);
|
|
28
|
+
fixed.push(`${cmd} shim (repointed to current install)`);
|
|
29
|
+
}
|
|
22
30
|
for (const version of listInstalledVersions(agent)) {
|
|
23
31
|
if (!isVersionedAliasCurrent(agent, version)) {
|
|
24
32
|
if (!ctx.dryRun)
|
|
@@ -30,6 +38,16 @@ export const shimsCheck = {
|
|
|
30
38
|
if (!ctx.dryRun && removeLegacyUserShim(agent))
|
|
31
39
|
fixed.push(`removed legacy ${cmd} shim`);
|
|
32
40
|
}
|
|
41
|
+
// Prune orphaned legacy command shims (browser/secrets/sessions/… left by a
|
|
42
|
+
// removed install) whose baked AGENTS_BIN is dead — the current source never
|
|
43
|
+
// regenerates them, and they either die with exit 127 or shadow the real
|
|
44
|
+
// package bin on PATH. Only removes shims whose target install is gone.
|
|
45
|
+
if (!ctx.dryRun) {
|
|
46
|
+
for (const name of listShimFileNames()) {
|
|
47
|
+
if (pruneOrphanedCommandShim(name))
|
|
48
|
+
fixed.push(`pruned orphaned ${name} shim`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
33
51
|
return resultOf(fixed, []);
|
|
34
52
|
},
|
|
35
53
|
};
|
|
@@ -2456,6 +2456,8 @@ export function readKimiMeta(filePath) {
|
|
|
2456
2456
|
project = parts.slice(0, -1).join('/');
|
|
2457
2457
|
}
|
|
2458
2458
|
}
|
|
2459
|
+
// Parse wire.jsonl to extract message count and token usage
|
|
2460
|
+
const { messageCount, tokenCount } = parseKimiWireMetrics(sessionDir);
|
|
2459
2461
|
const meta = {
|
|
2460
2462
|
id: sessionId,
|
|
2461
2463
|
shortId,
|
|
@@ -2464,9 +2466,48 @@ export function readKimiMeta(filePath) {
|
|
|
2464
2466
|
project,
|
|
2465
2467
|
filePath,
|
|
2466
2468
|
topic,
|
|
2469
|
+
messageCount,
|
|
2470
|
+
tokenCount: tokenCount > 0 ? tokenCount : undefined,
|
|
2467
2471
|
};
|
|
2468
2472
|
return { meta, content: lastPrompt || '' };
|
|
2469
2473
|
}
|
|
2474
|
+
/** Parse Kimi's wire.jsonl to extract message count and token usage.
|
|
2475
|
+
* TODO: optimize to stream (like scanClaudeSession) to avoid loading large files into memory.
|
|
2476
|
+
* For now, synchronous readFileSync matches the pattern of reading state.json and is acceptable
|
|
2477
|
+
* since session dirs are usually fresh in FS cache during incremental scans. */
|
|
2478
|
+
function parseKimiWireMetrics(sessionDir) {
|
|
2479
|
+
const wirePath = path.join(sessionDir, 'agents', 'main', 'wire.jsonl');
|
|
2480
|
+
let messageCount = 0;
|
|
2481
|
+
let tokenCount = 0;
|
|
2482
|
+
if (!fs.existsSync(wirePath)) {
|
|
2483
|
+
return { messageCount: 0, tokenCount: 0 };
|
|
2484
|
+
}
|
|
2485
|
+
try {
|
|
2486
|
+
const lines = fs.readFileSync(wirePath, 'utf-8').split('\n');
|
|
2487
|
+
for (const line of lines) {
|
|
2488
|
+
if (!line.trim())
|
|
2489
|
+
continue;
|
|
2490
|
+
try {
|
|
2491
|
+
const event = JSON.parse(line);
|
|
2492
|
+
if (event.type === 'context.append_message') {
|
|
2493
|
+
messageCount++;
|
|
2494
|
+
}
|
|
2495
|
+
else if (event.type === 'usage.record' && event.usage) {
|
|
2496
|
+
// Kimi usage structure: inputOther + output + inputCacheRead + inputCacheCreation
|
|
2497
|
+
const u = event.usage;
|
|
2498
|
+
tokenCount += (u.inputOther || 0) + (u.output || 0) + (u.inputCacheRead || 0) + (u.inputCacheCreation || 0);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
catch {
|
|
2502
|
+
// Malformed line, skip
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
catch {
|
|
2507
|
+
// If wire.jsonl can't be read, return 0s (graceful degradation)
|
|
2508
|
+
}
|
|
2509
|
+
return { messageCount, tokenCount };
|
|
2510
|
+
}
|
|
2470
2511
|
/** Parse a time filter string (relative like '7d' or ISO timestamp) into epoch milliseconds. */
|
|
2471
2512
|
export function parseTimeFilter(input) {
|
|
2472
2513
|
const relativeMatch = input.match(/^(\d+)([mhdw])$/i);
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -284,6 +284,28 @@ export declare function shimExists(agent: AgentId): boolean;
|
|
|
284
284
|
* older version that needs regeneration.
|
|
285
285
|
*/
|
|
286
286
|
export declare function isShimCurrent(agent: AgentId): boolean;
|
|
287
|
+
/**
|
|
288
|
+
* True when the agent shim's baked `AGENTS_BIN` is fine to keep: it either already
|
|
289
|
+
* points at the install we'd generate now, OR points at some OTHER install that
|
|
290
|
+
* still exists on disk (leave it — regenerating could ping-pong two live installs
|
|
291
|
+
* sharing the shims dir). Returns FALSE only when the shim points at a DIFFERENT,
|
|
292
|
+
* now-removed install — the exact drift a deleted dev build (`~/.local/agents-cli-dev`),
|
|
293
|
+
* an old npm-global (`/opt/homebrew`), or a rotated version dir leaves behind. A shim
|
|
294
|
+
* can pass the schema check (`isShimCurrent`) yet still carry that stale path.
|
|
295
|
+
*/
|
|
296
|
+
export declare function shimPointsAtLiveInstall(agent: AgentId): boolean;
|
|
297
|
+
/** Shim files in the shims dir, excluding the hooks/ subdir and @-versioned aliases. */
|
|
298
|
+
export declare function listShimFileNames(): string[];
|
|
299
|
+
/**
|
|
300
|
+
* Prune a stale, orphaned shim: one that is NOT a managed agent shim and NOT a user
|
|
301
|
+
* alias, whose baked `AGENTS_BIN` points at an install that no longer exists. These
|
|
302
|
+
* are legacy `exec "$AGENTS_BIN" <cmd>` command shims (browser/secrets/sessions/…)
|
|
303
|
+
* left behind by a removed install — the current source never generates them, and
|
|
304
|
+
* they either die with `exit 127` or shadow the real package bin on PATH. Only
|
|
305
|
+
* removed when the baked target is gone, so a working shim is never touched.
|
|
306
|
+
* Returns true if removed.
|
|
307
|
+
*/
|
|
308
|
+
export declare function pruneOrphanedCommandShim(fileName: string): boolean;
|
|
287
309
|
/**
|
|
288
310
|
* Regenerate the shim if it's missing or outdated. Returns a status describing
|
|
289
311
|
* what happened — callers can surface a one-line notice to the user ("Updated
|