@phnx-labs/agents-cli 1.20.51 → 1.20.52
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 +16 -0
- package/dist/commands/browser.js +215 -7
- package/dist/commands/cloud.js +6 -0
- package/dist/commands/events.d.ts +1 -1
- package/dist/commands/events.js +2 -3
- package/dist/commands/exec.js +17 -2
- package/dist/commands/factory.js +8 -0
- package/dist/commands/feed.d.ts +9 -0
- package/dist/commands/feed.js +69 -0
- package/dist/commands/logs.d.ts +5 -1
- package/dist/commands/logs.js +248 -3
- package/dist/commands/mcp.js +7 -0
- package/dist/commands/secrets.d.ts +22 -0
- package/dist/commands/secrets.js +173 -42
- package/dist/commands/teams.js +4 -0
- package/dist/index.js +6 -2
- package/dist/lib/browser/login-detection.d.ts +94 -0
- package/dist/lib/browser/login-detection.js +274 -0
- package/dist/lib/browser/profiles.d.ts +17 -8
- package/dist/lib/browser/profiles.js +27 -8
- package/dist/lib/browser/secret-ref.d.ts +10 -0
- package/dist/lib/browser/secret-ref.js +14 -0
- package/dist/lib/browser/service.js +14 -12
- package/dist/lib/cloud/rush.d.ts +15 -0
- package/dist/lib/cloud/rush.js +7 -1
- package/dist/lib/crabbox/lease.d.ts +6 -0
- package/dist/lib/crabbox/lease.js +11 -9
- package/dist/lib/crabbox/runtimes.d.ts +38 -1
- package/dist/lib/crabbox/runtimes.js +98 -5
- package/dist/lib/daemon.d.ts +12 -9
- package/dist/lib/daemon.js +32 -17
- package/dist/lib/events.d.ts +31 -5
- package/dist/lib/events.js +288 -101
- package/dist/lib/exec.js +1 -0
- package/dist/lib/feed.d.ts +56 -0
- package/dist/lib/feed.js +251 -0
- package/dist/lib/hooks.js +7 -2
- package/dist/lib/hosts/passthrough.js +1 -0
- package/dist/lib/rotate.js +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +21 -0
- package/dist/lib/secrets/agent.js +63 -1
- package/dist/lib/secrets/bundles.d.ts +33 -1
- package/dist/lib/secrets/bundles.js +38 -8
- package/dist/lib/secrets/icloud-import.d.ts +70 -0
- package/dist/lib/secrets/icloud-import.js +173 -0
- package/dist/lib/secrets/index.d.ts +36 -0
- package/dist/lib/secrets/index.js +99 -9
- package/dist/lib/secrets/remote.js +1 -1
- package/dist/lib/secrets/sync.js +1 -1
- package/dist/lib/session/discover.js +1 -2
- package/dist/lib/session/state.js +13 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +25 -8
- package/dist/lib/teams/agents.js +6 -3
- package/dist/lib/types.d.ts +10 -0
- package/dist/lib/whats-new.d.ts +5 -3
- package/dist/lib/whats-new.js +25 -5
- package/package.json +1 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recovery for LEGACY SYNCHRONIZABLE (iCloud Keychain) bundles.
|
|
3
|
+
*
|
|
4
|
+
* The pre-biometry helper era defaulted bundles to iCloud Keychain sync. The
|
|
5
|
+
* device-local cutover (biometry ACL + kSecAttrSynchronizable false on every
|
|
6
|
+
* query) orphaned those items: they still sync back via iCloud Keychain, but
|
|
7
|
+
* neither `secrets list` nor `migrate-acl` can see them. This module powers
|
|
8
|
+
* `agents secrets import --from icloud` — discover the orphaned bundles,
|
|
9
|
+
* re-import them as normal device-local bundles, and optionally purge the
|
|
10
|
+
* iCloud copies.
|
|
11
|
+
*
|
|
12
|
+
* The item-name scheme is the same one the modern store uses (see bundles.ts):
|
|
13
|
+
* metadata under `agents-cli.bundles.<name>`, one value per key under
|
|
14
|
+
* `agents-cli.secrets.<bundle>.<key>`. Env keys can never contain a dot
|
|
15
|
+
* (ENV_KEY_PATTERN), so splitting a secret service at its LAST dot recovers
|
|
16
|
+
* the bundle/key boundary even for dotted bundle names like `hetzner.com`.
|
|
17
|
+
*/
|
|
18
|
+
import { deleteSyncedKeychainItem, getSyncedKeychainTokens, listSyncedKeychainItems, parseBundleValue, secretsKeychainItem, serializeRef, SECRETS_ITEM_PREFIX, } from './index.js';
|
|
19
|
+
import { BUNDLE_META_PREFIX, BUNDLE_NAME_PATTERN, ENV_KEY_PATTERN, bundleExists, bundleItemStore, bundlePolicy, keychainRef, readBundle, writeBundle, } from './bundles.js';
|
|
20
|
+
/**
|
|
21
|
+
* Group raw synced service names into per-bundle candidates. Pure — separated
|
|
22
|
+
* from discovery so the parsing rules are unit-testable without a keychain.
|
|
23
|
+
*
|
|
24
|
+
* A bundle can surface as metadata only (`agents-cli.bundles.<name>`), as
|
|
25
|
+
* secret items only (`agents-cli.secrets.<bundle>.<KEY>` — metadata never
|
|
26
|
+
* synced), or both; all three shapes appear in real iCloud strays, so every
|
|
27
|
+
* one becomes a candidate.
|
|
28
|
+
*/
|
|
29
|
+
export function groupSyncedServices(services) {
|
|
30
|
+
const byName = new Map();
|
|
31
|
+
const claim = (name) => {
|
|
32
|
+
let c = byName.get(name);
|
|
33
|
+
if (!c) {
|
|
34
|
+
c = { name, keys: [], hasMeta: false, services: [] };
|
|
35
|
+
byName.set(name, c);
|
|
36
|
+
}
|
|
37
|
+
return c;
|
|
38
|
+
};
|
|
39
|
+
for (const svc of services) {
|
|
40
|
+
if (svc.startsWith(BUNDLE_META_PREFIX)) {
|
|
41
|
+
const name = svc.slice(BUNDLE_META_PREFIX.length);
|
|
42
|
+
if (!BUNDLE_NAME_PATTERN.test(name))
|
|
43
|
+
continue;
|
|
44
|
+
const c = claim(name);
|
|
45
|
+
c.hasMeta = true;
|
|
46
|
+
c.services.push(svc);
|
|
47
|
+
}
|
|
48
|
+
else if (svc.startsWith(SECRETS_ITEM_PREFIX)) {
|
|
49
|
+
const rest = svc.slice(SECRETS_ITEM_PREFIX.length);
|
|
50
|
+
const cut = rest.lastIndexOf('.');
|
|
51
|
+
if (cut <= 0)
|
|
52
|
+
continue;
|
|
53
|
+
const name = rest.slice(0, cut);
|
|
54
|
+
const key = rest.slice(cut + 1);
|
|
55
|
+
if (!BUNDLE_NAME_PATTERN.test(name) || !ENV_KEY_PATTERN.test(key))
|
|
56
|
+
continue;
|
|
57
|
+
const c = claim(name);
|
|
58
|
+
if (!c.keys.includes(key))
|
|
59
|
+
c.keys.push(key);
|
|
60
|
+
c.services.push(svc);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
64
|
+
}
|
|
65
|
+
/** Enumerate the iCloud keychain and return every orphaned bundle candidate. */
|
|
66
|
+
export function discoverSyncedBundles() {
|
|
67
|
+
return groupSyncedServices(listSyncedKeychainItems('agents-cli.'));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Import one discovered iCloud bundle into the local (device-local) store.
|
|
71
|
+
*
|
|
72
|
+
* Values come from the synced secret items; the synced metadata item, when
|
|
73
|
+
* present, contributes the description, literal vars, and non-keychain refs
|
|
74
|
+
* (env:/file:/exec: refs carry no stored secret, so copying the ref preserves
|
|
75
|
+
* them exactly). Existing local keys are skipped unless `force`. With `purge`,
|
|
76
|
+
* only services whose value was successfully read are deleted from iCloud —
|
|
77
|
+
* an unreadable item is never destroyed.
|
|
78
|
+
*/
|
|
79
|
+
export function importSyncedBundle(candidate, opts = {}) {
|
|
80
|
+
const values = getSyncedKeychainTokens(candidate.services);
|
|
81
|
+
let bundle;
|
|
82
|
+
if (bundleExists(candidate.name)) {
|
|
83
|
+
bundle = readBundle(candidate.name);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
bundle = {
|
|
87
|
+
name: candidate.name,
|
|
88
|
+
backend: opts.backend === 'file' ? 'file' : undefined,
|
|
89
|
+
vars: {},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const metaService = BUNDLE_META_PREFIX + candidate.name;
|
|
93
|
+
let metaVars = {};
|
|
94
|
+
const metaJson = values.get(metaService);
|
|
95
|
+
if (metaJson !== undefined) {
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(metaJson);
|
|
98
|
+
if (parsed && typeof parsed === 'object') {
|
|
99
|
+
if (typeof parsed.description === 'string' && !bundle.description) {
|
|
100
|
+
bundle.description = parsed.description;
|
|
101
|
+
}
|
|
102
|
+
if (parsed.vars && typeof parsed.vars === 'object') {
|
|
103
|
+
metaVars = parsed.vars;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Corrupt legacy metadata — the per-key secret items still import.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const store = bundleItemStore(bundle.backend, { noAcl: bundlePolicy(bundle) === 'never' });
|
|
112
|
+
let added = 0;
|
|
113
|
+
let skipped = 0;
|
|
114
|
+
const missing = [];
|
|
115
|
+
// Keys with a synced secret item: re-store the value device-locally.
|
|
116
|
+
for (const key of candidate.keys) {
|
|
117
|
+
const value = values.get(secretsKeychainItem(candidate.name, key));
|
|
118
|
+
if (value === undefined) {
|
|
119
|
+
missing.push(key);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!opts.force && key in bundle.vars) {
|
|
123
|
+
skipped++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (opts.allPlaintext) {
|
|
127
|
+
bundle.vars[key] = { value };
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
store.set(secretsKeychainItem(candidate.name, key), value);
|
|
131
|
+
bundle.vars[key] = keychainRef(key);
|
|
132
|
+
}
|
|
133
|
+
added++;
|
|
134
|
+
}
|
|
135
|
+
// Vars declared only in the synced metadata: literals and non-keychain refs
|
|
136
|
+
// carry everything they need; a keychain ref without its synced item is
|
|
137
|
+
// unrecoverable.
|
|
138
|
+
for (const [key, raw] of Object.entries(metaVars)) {
|
|
139
|
+
if (!ENV_KEY_PATTERN.test(key))
|
|
140
|
+
continue;
|
|
141
|
+
if (candidate.keys.includes(key))
|
|
142
|
+
continue; // the secret item already covered it
|
|
143
|
+
let parsed;
|
|
144
|
+
try {
|
|
145
|
+
parsed = parseBundleValue(raw);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
continue; // malformed legacy entry
|
|
149
|
+
}
|
|
150
|
+
if ('ref' in parsed && parsed.ref.provider === 'keychain') {
|
|
151
|
+
if (!missing.includes(key))
|
|
152
|
+
missing.push(key);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!opts.force && key in bundle.vars) {
|
|
156
|
+
skipped++;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
bundle.vars[key] = 'literal' in parsed ? { value: parsed.literal } : serializeRef(parsed.ref);
|
|
160
|
+
added++;
|
|
161
|
+
}
|
|
162
|
+
writeBundle(bundle);
|
|
163
|
+
let purged = 0;
|
|
164
|
+
if (opts.purge) {
|
|
165
|
+
for (const svc of candidate.services) {
|
|
166
|
+
if (!values.has(svc))
|
|
167
|
+
continue; // never destroy an item we couldn't read
|
|
168
|
+
if (deleteSyncedKeychainItem(svc))
|
|
169
|
+
purged++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { name: candidate.name, added, skipped, missing, purged };
|
|
173
|
+
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import type { NativeImportReport } from './fallback.js';
|
|
26
26
|
export type { NativeImportReport, NativeImportResult, NativeImportStatus } from './fallback.js';
|
|
27
|
+
export declare const SECRETS_ITEM_PREFIX = "agents-cli.secrets.";
|
|
27
28
|
/** Supported secret resolution backends. */
|
|
28
29
|
export type SecretProvider = 'keychain' | 'env' | 'file' | 'exec';
|
|
29
30
|
/** A typed reference to a secret, consisting of a provider and a provider-specific value. */
|
|
@@ -141,6 +142,41 @@ export declare function listKeychainItems(prefix: string): string[];
|
|
|
141
142
|
* stragglers instead of every item (which would be a Touch ID storm).
|
|
142
143
|
*/
|
|
143
144
|
export declare function listLegacyKeychainItems(prefix: string): string[];
|
|
145
|
+
/**
|
|
146
|
+
* Test seam for the LEGACY SYNCHRONIZABLE (iCloud Keychain) recovery path.
|
|
147
|
+
* The main `KeychainBackend` seam models the live device-local store; this one
|
|
148
|
+
* models the orphaned iCloud items that `secrets import --from icloud` reads.
|
|
149
|
+
* Kept separate so a test can populate both sides independently.
|
|
150
|
+
*/
|
|
151
|
+
export interface SyncedKeychainBackend {
|
|
152
|
+
list(prefix: string): string[];
|
|
153
|
+
getBatch(items: string[]): Map<string, string>;
|
|
154
|
+
delete(item: string): boolean;
|
|
155
|
+
}
|
|
156
|
+
export declare function setSyncedKeychainBackendForTest(b: SyncedKeychainBackend | null): SyncedKeychainBackend | null;
|
|
157
|
+
/**
|
|
158
|
+
* Enumerate LEGACY SYNCHRONIZABLE (iCloud Keychain) item names with the given
|
|
159
|
+
* prefix — bundles written by the pre-biometry helper era, which defaulted
|
|
160
|
+
* secrets to iCloud Keychain sync. The device-local cutover orphaned them:
|
|
161
|
+
* every modern query pins synchronizable=false, so only the helper's
|
|
162
|
+
* `list-synced` verb can see them. Silent (attributes only, never decrypts).
|
|
163
|
+
* macOS only — Linux/Windows never had iCloud Keychain sync, so this returns [].
|
|
164
|
+
*/
|
|
165
|
+
export declare function listSyncedKeychainItems(prefix: string): string[];
|
|
166
|
+
/**
|
|
167
|
+
* Batch-read LEGACY SYNCHRONIZABLE (iCloud Keychain) items. Returns a map of
|
|
168
|
+
* item name → value; missing items are simply absent. Pre-biometry items carry
|
|
169
|
+
* no biometry ACL, so this does not normally prompt. macOS only — returns an
|
|
170
|
+
* empty map on Linux/Windows.
|
|
171
|
+
*/
|
|
172
|
+
export declare function getSyncedKeychainTokens(items: string[]): Map<string, string>;
|
|
173
|
+
/**
|
|
174
|
+
* Delete a LEGACY SYNCHRONIZABLE (iCloud Keychain) item after a successful
|
|
175
|
+
* import (`--purge`). Matches synchronizable items only — the device-local
|
|
176
|
+
* copy the import wrote is untouched. iCloud propagates the deletion to the
|
|
177
|
+
* user's other devices. Returns true if a copy was removed.
|
|
178
|
+
*/
|
|
179
|
+
export declare function deleteSyncedKeychainItem(item: string): boolean;
|
|
144
180
|
/**
|
|
145
181
|
* One-time upgrade for a keychain item that was written by a previous helper
|
|
146
182
|
* generation with a trusted-app ACL. The helper reads the legacy item
|
|
@@ -30,7 +30,7 @@ import { linuxBackend, usesFileFallback as linuxUsesFileFallback, importNativeSe
|
|
|
30
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
|
-
const SECRETS_ITEM_PREFIX = `${SERVICE_PREFIX}.secrets.`;
|
|
33
|
+
export const SECRETS_ITEM_PREFIX = `${SERVICE_PREFIX}.secrets.`;
|
|
34
34
|
const BUNDLES_ITEM_PREFIX = `${SERVICE_PREFIX}.bundles.`;
|
|
35
35
|
const REF_PATTERN = /^(keychain|env|file|exec):(.+)$/s;
|
|
36
36
|
/** Parse a bundle value into either a literal string or a typed secret ref. */
|
|
@@ -234,12 +234,20 @@ export function getKeychainTokens(items) {
|
|
|
234
234
|
throw new Error(msg || `Failed to batch-read ${items.length} keychain items.`);
|
|
235
235
|
}
|
|
236
236
|
const out = child.stdout?.toString() ?? '';
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
237
|
+
parseBatchRecords(out, result);
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Parse the helper's batch-read output into `into`. The format is shared by
|
|
242
|
+
* `get-batch` and `get-batch-synced` — a sequence of records, one per service
|
|
243
|
+
* in input order:
|
|
244
|
+
* "V <service>\n<value>\n" (present)
|
|
245
|
+
* "M <service>\n" (missing)
|
|
246
|
+
* Service names are validated newline/'='-free by setKeychainToken below
|
|
247
|
+
* and values are rejected if they contain newlines — so splitting on '\n'
|
|
248
|
+
* and walking line-by-line is unambiguous.
|
|
249
|
+
*/
|
|
250
|
+
function parseBatchRecords(out, into) {
|
|
243
251
|
const lines = out.split('\n');
|
|
244
252
|
let i = 0;
|
|
245
253
|
while (i < lines.length) {
|
|
@@ -249,7 +257,7 @@ export function getKeychainTokens(items) {
|
|
|
249
257
|
if (line.startsWith('V ')) {
|
|
250
258
|
const service = line.slice(2);
|
|
251
259
|
const value = lines[i + 1] ?? '';
|
|
252
|
-
|
|
260
|
+
into.set(service, value);
|
|
253
261
|
i += 2;
|
|
254
262
|
}
|
|
255
263
|
else if (line.startsWith('M ')) {
|
|
@@ -262,7 +270,6 @@ export function getKeychainTokens(items) {
|
|
|
262
270
|
throw new Error(`Malformed get-batch output line: ${JSON.stringify(line)}`);
|
|
263
271
|
}
|
|
264
272
|
}
|
|
265
|
-
return result;
|
|
266
273
|
}
|
|
267
274
|
/** Store or update a secret value in the keychain/keyring. Device-local;
|
|
268
275
|
* biometry-gated on macOS. `opts.noAcl` (the `never` prompt-policy) writes our
|
|
@@ -408,6 +415,89 @@ export function listLegacyKeychainItems(prefix) {
|
|
|
408
415
|
const out = result.stdout?.toString() || '';
|
|
409
416
|
return out.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
410
417
|
}
|
|
418
|
+
let syncedBackend = null;
|
|
419
|
+
export function setSyncedKeychainBackendForTest(b) {
|
|
420
|
+
const prev = syncedBackend;
|
|
421
|
+
syncedBackend = b;
|
|
422
|
+
return prev;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Enumerate LEGACY SYNCHRONIZABLE (iCloud Keychain) item names with the given
|
|
426
|
+
* prefix — bundles written by the pre-biometry helper era, which defaulted
|
|
427
|
+
* secrets to iCloud Keychain sync. The device-local cutover orphaned them:
|
|
428
|
+
* every modern query pins synchronizable=false, so only the helper's
|
|
429
|
+
* `list-synced` verb can see them. Silent (attributes only, never decrypts).
|
|
430
|
+
* macOS only — Linux/Windows never had iCloud Keychain sync, so this returns [].
|
|
431
|
+
*/
|
|
432
|
+
export function listSyncedKeychainItems(prefix) {
|
|
433
|
+
if (syncedBackend)
|
|
434
|
+
return syncedBackend.list(prefix);
|
|
435
|
+
if (backend)
|
|
436
|
+
return [];
|
|
437
|
+
assertSupportedPlatform();
|
|
438
|
+
if (isLinux() || isWindows())
|
|
439
|
+
return [];
|
|
440
|
+
const bin = getKeychainHelperPath();
|
|
441
|
+
const result = spawnSync(bin, ['list-synced', prefix], {
|
|
442
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
443
|
+
});
|
|
444
|
+
if (result.status !== 0) {
|
|
445
|
+
const msg = result.stderr?.toString().trim();
|
|
446
|
+
throw new Error(msg || `Failed to enumerate iCloud keychain items with prefix '${prefix}'.`);
|
|
447
|
+
}
|
|
448
|
+
const out = result.stdout?.toString() || '';
|
|
449
|
+
return out.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Batch-read LEGACY SYNCHRONIZABLE (iCloud Keychain) items. Returns a map of
|
|
453
|
+
* item name → value; missing items are simply absent. Pre-biometry items carry
|
|
454
|
+
* no biometry ACL, so this does not normally prompt. macOS only — returns an
|
|
455
|
+
* empty map on Linux/Windows.
|
|
456
|
+
*/
|
|
457
|
+
export function getSyncedKeychainTokens(items) {
|
|
458
|
+
const result = new Map();
|
|
459
|
+
if (items.length === 0)
|
|
460
|
+
return result;
|
|
461
|
+
if (syncedBackend)
|
|
462
|
+
return syncedBackend.getBatch(items);
|
|
463
|
+
if (backend)
|
|
464
|
+
return result;
|
|
465
|
+
assertSupportedPlatform();
|
|
466
|
+
if (isLinux() || isWindows())
|
|
467
|
+
return result;
|
|
468
|
+
const bin = getKeychainHelperPath();
|
|
469
|
+
const child = spawnSync(bin, ['get-batch-synced', os.userInfo().username, ...items], {
|
|
470
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
471
|
+
});
|
|
472
|
+
if (child.status === 4) {
|
|
473
|
+
throw new Error(`Auth cancelled while reading ${items.length} iCloud keychain item(s).`);
|
|
474
|
+
}
|
|
475
|
+
if (child.status !== 0) {
|
|
476
|
+
const msg = child.stderr?.toString().trim();
|
|
477
|
+
throw new Error(msg || `Failed to batch-read ${items.length} iCloud keychain items.`);
|
|
478
|
+
}
|
|
479
|
+
parseBatchRecords(child.stdout?.toString() ?? '', result);
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Delete a LEGACY SYNCHRONIZABLE (iCloud Keychain) item after a successful
|
|
484
|
+
* import (`--purge`). Matches synchronizable items only — the device-local
|
|
485
|
+
* copy the import wrote is untouched. iCloud propagates the deletion to the
|
|
486
|
+
* user's other devices. Returns true if a copy was removed.
|
|
487
|
+
*/
|
|
488
|
+
export function deleteSyncedKeychainItem(item) {
|
|
489
|
+
if (syncedBackend)
|
|
490
|
+
return syncedBackend.delete(item);
|
|
491
|
+
if (backend)
|
|
492
|
+
return false;
|
|
493
|
+
assertSupportedPlatform();
|
|
494
|
+
if (isLinux() || isWindows())
|
|
495
|
+
return false;
|
|
496
|
+
const bin = getKeychainHelperPath();
|
|
497
|
+
return spawnSync(bin, ['delete-synced', item, os.userInfo().username], {
|
|
498
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
499
|
+
}).status === 0;
|
|
500
|
+
}
|
|
411
501
|
/**
|
|
412
502
|
* One-time upgrade for a keychain item that was written by a previous helper
|
|
413
503
|
* generation with a trusted-app ACL. The helper reads the legacy item
|
package/dist/lib/secrets/sync.js
CHANGED
|
@@ -193,7 +193,7 @@ export async function pushBundle(name, opts) {
|
|
|
193
193
|
emit('secrets.get', {
|
|
194
194
|
module: 'secrets',
|
|
195
195
|
bundle: name,
|
|
196
|
-
|
|
196
|
+
operation: 'sync push',
|
|
197
197
|
source: 'sync-push',
|
|
198
198
|
status: 'success',
|
|
199
199
|
keyCount: Object.keys(snap.secrets).length,
|
|
@@ -1826,8 +1826,7 @@ export async function scanClaudeSession(filePath) {
|
|
|
1826
1826
|
pendingTicketTools.add(b.id);
|
|
1827
1827
|
}
|
|
1828
1828
|
// ExitPlanMode plan markdown — last one wins so a re-planned session
|
|
1829
|
-
// reports its most recent plan
|
|
1830
|
-
// implemented in the extension).
|
|
1829
|
+
// reports its most recent plan.
|
|
1831
1830
|
if (b?.name === 'ExitPlanMode' && typeof b?.input?.plan === 'string') {
|
|
1832
1831
|
const p = b.input.plan.trim();
|
|
1833
1832
|
if (p)
|
|
@@ -17,6 +17,14 @@
|
|
|
17
17
|
import { summarizeToolUse } from './parse.js';
|
|
18
18
|
/** A healthy live session writes several times a minute; 2 min ⇒ "recently active". */
|
|
19
19
|
const ACTIVE_WINDOW_MS = 2 * 60_000;
|
|
20
|
+
/**
|
|
21
|
+
* A prose trailing question ("…?") is a HEURISTIC, so it decays: past this long
|
|
22
|
+
* with no session writes it stops classifying as waiting_input — otherwise a
|
|
23
|
+
* finished session that signed off with "anything else?" reads as needing input
|
|
24
|
+
* forever (RUSH-1522). The structural ExitPlanMode / AskUserQuestion signals are
|
|
25
|
+
* exempt: they are precise, still-unanswered decisions.
|
|
26
|
+
*/
|
|
27
|
+
const PROSE_QUESTION_FRESH_MS = 30 * 60_000;
|
|
20
28
|
/** Claude tool names that structurally mean "the agent handed control back to you". */
|
|
21
29
|
const PLAN_TOOL = 'ExitPlanMode';
|
|
22
30
|
const ASK_TOOL = 'AskUserQuestion';
|
|
@@ -299,7 +307,11 @@ export function inferActivity(events, ctx = {}) {
|
|
|
299
307
|
}
|
|
300
308
|
// Assistant spoke last and stopped. A trailing question → waiting; else idle.
|
|
301
309
|
// A prose question takes a free-text reply (no select-list), so no options/keys.
|
|
302
|
-
|
|
310
|
+
// Unlike the structural plan/ask signals above, the prose heuristic DECAYS: a
|
|
311
|
+
// question nobody answered within PROSE_QUESTION_FRESH_MS is a session that
|
|
312
|
+
// ended, not one that needs you (RUSH-1522). Unknown mtime keeps the question.
|
|
313
|
+
const questionFresh = ctx.mtimeMs == null || Date.now() - ctx.mtimeMs < PROSE_QUESTION_FRESH_MS;
|
|
314
|
+
if (questionFresh && looksLikeQuestion(last.content ?? '')) {
|
|
303
315
|
const text = oneLine(last.content ?? '');
|
|
304
316
|
return { ...base, activity: 'waiting_input', awaitingReason: 'question', question: { text, reason: 'question' } };
|
|
305
317
|
}
|
|
@@ -83,6 +83,7 @@ export declare const loadSessions: ModuleLoader;
|
|
|
83
83
|
export declare const loadTeams: ModuleLoader;
|
|
84
84
|
export declare const loadCloud: ModuleLoader;
|
|
85
85
|
export declare const loadMessage: ModuleLoader;
|
|
86
|
+
export declare const loadFeed: ModuleLoader;
|
|
86
87
|
export declare const loadServe: ModuleLoader;
|
|
87
88
|
export declare const loadAudit: ModuleLoader;
|
|
88
89
|
/**
|
|
@@ -61,6 +61,7 @@ export const loadSessions = async () => (await import('../../commands/sessions.j
|
|
|
61
61
|
export const loadTeams = async () => (await import('../../commands/teams.js')).registerTeamsCommands;
|
|
62
62
|
export const loadCloud = async () => (await import('../../commands/cloud.js')).registerCloudCommands;
|
|
63
63
|
export const loadMessage = async () => (await import('../../commands/message.js')).registerMessageCommand;
|
|
64
|
+
export const loadFeed = async () => (await import('../../commands/feed.js')).registerFeedCommand;
|
|
64
65
|
export const loadServe = async () => (await import('../../commands/serve.js')).registerServeCommand;
|
|
65
66
|
export const loadAudit = async () => (await import('../../commands/audit.js')).registerAuditCommands;
|
|
66
67
|
/**
|
|
@@ -154,6 +155,7 @@ export const COMMAND_LOADERS = {
|
|
|
154
155
|
teams: [loadTeams],
|
|
155
156
|
cloud: [loadCloud],
|
|
156
157
|
message: [loadMessage],
|
|
158
|
+
feed: [loadFeed],
|
|
157
159
|
serve: [loadServe],
|
|
158
160
|
audit: [loadAudit],
|
|
159
161
|
};
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -122,6 +122,8 @@ export declare function getProjectRoutinesDir(cwd?: string): string | null;
|
|
|
122
122
|
export declare function getRunsDir(): string;
|
|
123
123
|
/** Root for per-agent mailboxes (~/.agents/.history/mailbox/). */
|
|
124
124
|
export declare function getMailboxRootDir(): string;
|
|
125
|
+
/** Root for open-block feed records (~/.agents/.history/feed/). */
|
|
126
|
+
export declare function getFeedDir(): string;
|
|
125
127
|
/** Path to installed agent CLI binaries (~/.agents/.history/versions/). */
|
|
126
128
|
export declare function getVersionsDir(): string;
|
|
127
129
|
/** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
|
package/dist/lib/state.js
CHANGED
|
@@ -89,6 +89,7 @@ const TEAMS_AGENTS_DIR = path.join(HISTORY_DIR, 'teams', 'agents');
|
|
|
89
89
|
const BACKUPS_DIR = path.join(HISTORY_DIR, 'backups');
|
|
90
90
|
const TRASH_DIR = path.join(HISTORY_DIR, 'trash');
|
|
91
91
|
const MAILBOX_DIR = path.join(HISTORY_DIR, 'mailbox');
|
|
92
|
+
const FEED_DIR = path.join(HISTORY_DIR, 'feed');
|
|
92
93
|
// Cache bucket (regenerable).
|
|
93
94
|
const SHIMS_DIR = path.join(CACHE_DIR, 'shims');
|
|
94
95
|
const HOOK_SHIMS_DIR = path.join(SHIMS_DIR, 'hooks');
|
|
@@ -313,6 +314,8 @@ export function getProjectRoutinesDir(cwd = process.cwd()) {
|
|
|
313
314
|
export function getRunsDir() { return RUNS_DIR; }
|
|
314
315
|
/** Root for per-agent mailboxes (~/.agents/.history/mailbox/). */
|
|
315
316
|
export function getMailboxRootDir() { return MAILBOX_DIR; }
|
|
317
|
+
/** Root for open-block feed records (~/.agents/.history/feed/). */
|
|
318
|
+
export function getFeedDir() { return FEED_DIR; }
|
|
316
319
|
/** Path to installed agent CLI binaries (~/.agents/.history/versions/). */
|
|
317
320
|
export function getVersionsDir() { return VERSIONS_DIR; }
|
|
318
321
|
/** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
|
|
@@ -569,25 +572,35 @@ function writeIfChanged(filePath, content) {
|
|
|
569
572
|
/**
|
|
570
573
|
* Partition the in-memory Meta across three files by sync-domain:
|
|
571
574
|
* - central `~/.agents/agents.yaml` — portable, everything else
|
|
572
|
-
* - device `~/.agents/devices/<machine>/agents.yaml` — `agents:` pins
|
|
575
|
+
* - device `~/.agents/devices/<machine>/agents.yaml` — `agents:` pins +
|
|
576
|
+
* `defaultBrowserProfile:` (both per-device)
|
|
573
577
|
* - history `~/.agents/.history/version-resources.json` — `versions:` (machine-local)
|
|
574
578
|
* All callers funnel through writeMeta → here, so nothing else changes. Empty
|
|
575
579
|
* `agents:` / `versions:` are not written (no empty committed files).
|
|
576
580
|
*/
|
|
577
581
|
function writeMetaUnlocked(meta) {
|
|
578
|
-
const { agents, versions, ...central } = meta;
|
|
582
|
+
const { agents, versions, defaultBrowserProfile, ...central } = meta;
|
|
579
583
|
// Write the machine-local files FIRST, then strip central — so a crash mid-write
|
|
580
584
|
// never removes pins/versions from central before they're persisted elsewhere.
|
|
581
585
|
const devicePath = getDeviceMetaPath();
|
|
582
|
-
|
|
586
|
+
const hasAgents = !!agents && Object.keys(agents).length > 0;
|
|
587
|
+
const hasDefaultBrowser = !!defaultBrowserProfile;
|
|
588
|
+
if (hasAgents || hasDefaultBrowser) {
|
|
589
|
+
// Device-local doc carries `agents:` pins and `defaultBrowserProfile:` — both
|
|
590
|
+
// are per-machine and must never land in central agents.yaml (which syncs).
|
|
591
|
+
const deviceDoc = {};
|
|
592
|
+
if (hasAgents)
|
|
593
|
+
deviceDoc.agents = agents;
|
|
594
|
+
if (hasDefaultBrowser)
|
|
595
|
+
deviceDoc.defaultBrowserProfile = defaultBrowserProfile;
|
|
583
596
|
fs.mkdirSync(path.dirname(devicePath), { recursive: true });
|
|
584
|
-
writeIfChanged(devicePath, META_HEADER + yaml.stringify(
|
|
597
|
+
writeIfChanged(devicePath, META_HEADER + yaml.stringify(deviceDoc));
|
|
585
598
|
}
|
|
586
599
|
else if (fs.existsSync(devicePath)) {
|
|
587
|
-
// Every
|
|
588
|
-
// write — otherwise the stale device file survives and
|
|
589
|
-
// re-applies the removed pin on the next read,
|
|
590
|
-
// (e.g. a
|
|
600
|
+
// Every device-local value was cleared. Persist the emptied doc instead of
|
|
601
|
+
// skipping the write — otherwise the stale device file survives and
|
|
602
|
+
// overlayMachineLocal re-applies the removed pin/default on the next read,
|
|
603
|
+
// leaving a dangling value (e.g. a default pointing at a deleted profile).
|
|
591
604
|
writeIfChanged(devicePath, META_HEADER + yaml.stringify({ agents: {} }));
|
|
592
605
|
}
|
|
593
606
|
if (versions && Object.keys(versions).length > 0) {
|
|
@@ -602,6 +615,8 @@ function writeMetaUnlocked(meta) {
|
|
|
602
615
|
* Overlay this machine's local state onto a central-portable Meta:
|
|
603
616
|
* - `agents:` from the device file (device wins; the union both preserves the
|
|
604
617
|
* one-level merge and self-heals a pre-migration central that still has pins)
|
|
618
|
+
* - `defaultBrowserProfile:` from the device file (device is the sole source;
|
|
619
|
+
* the field is stripped from central on write, so nothing to merge against)
|
|
605
620
|
* - `versions:` from the history JSON (wholesale replace; falls back to
|
|
606
621
|
* whatever central carried when the history file doesn't exist yet)
|
|
607
622
|
*/
|
|
@@ -612,6 +627,8 @@ function overlayMachineLocal(meta) {
|
|
|
612
627
|
const dm = yaml.parse(fs.readFileSync(devicePath, 'utf-8'));
|
|
613
628
|
if (dm?.agents)
|
|
614
629
|
meta.agents = { ...meta.agents, ...dm.agents };
|
|
630
|
+
if (dm?.defaultBrowserProfile)
|
|
631
|
+
meta.defaultBrowserProfile = dm.defaultBrowserProfile;
|
|
615
632
|
}
|
|
616
633
|
catch { /* ignore malformed device file */ }
|
|
617
634
|
}
|
package/dist/lib/teams/agents.js
CHANGED
|
@@ -1693,6 +1693,7 @@ export class AgentManager {
|
|
|
1693
1693
|
];
|
|
1694
1694
|
if (model)
|
|
1695
1695
|
args.push('--model', model);
|
|
1696
|
+
args.push('--env', 'AGENTS_RUNTIME=teams');
|
|
1696
1697
|
return args;
|
|
1697
1698
|
}
|
|
1698
1699
|
buildCommand(agentType, prompt, mode, model, cwd = null, sessionId = null, effort = 'medium', version = null, profileName = null) {
|
|
@@ -1704,9 +1705,11 @@ export class AgentManager {
|
|
|
1704
1705
|
];
|
|
1705
1706
|
if (cwd)
|
|
1706
1707
|
cmd.push('--cwd', cwd);
|
|
1707
|
-
// Pin
|
|
1708
|
-
//
|
|
1709
|
-
|
|
1708
|
+
// Pin the session UUID to our agent_id so buildExecEnv keys
|
|
1709
|
+
// AGENTS_MAILBOX_DIR by the same id mailboxIdForActiveSession returns.
|
|
1710
|
+
// Claude also forwards --session-id to its CLI (unified identity);
|
|
1711
|
+
// other agents ignore the flag but still get the correct mailbox dir.
|
|
1712
|
+
if (sessionId) {
|
|
1710
1713
|
cmd.push('--session-id', sessionId);
|
|
1711
1714
|
}
|
|
1712
1715
|
// Claude: grant access to the teammate's working directory.
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -652,6 +652,16 @@ export interface Meta {
|
|
|
652
652
|
* lives separately in ~/.agents/.cache/browser/<profile>/.
|
|
653
653
|
*/
|
|
654
654
|
browser?: Record<string, BrowserProfileConfig>;
|
|
655
|
+
/**
|
|
656
|
+
* Device-local pointer: the browser profile `agents browser start` resolves to
|
|
657
|
+
* when no `--profile` is passed (and what an explicit `--profile default`
|
|
658
|
+
* re-points to). Stored per-machine in `~/.agents/devices/<machine>/agents.yaml`,
|
|
659
|
+
* NOT central — the target profile may carry machine-local logins, so the choice
|
|
660
|
+
* must not ride `agents repo push/pull` to other machines. Unset = auto-detect an
|
|
661
|
+
* installed Chromium-family browser (legacy behavior). Set via
|
|
662
|
+
* `agents browser profiles set-default <name>`.
|
|
663
|
+
*/
|
|
664
|
+
defaultBrowserProfile?: string;
|
|
655
665
|
/**
|
|
656
666
|
* Agent-host registry keyed by host name (`agents hosts`). Portable user
|
|
657
667
|
* config synced with `agents repo push/pull`. For `ssh-config` hosts this is
|
package/dist/lib/whats-new.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Render a compact "What's new" summary from a CHANGELOG.md body: one bullet
|
|
3
|
-
* per feature/fix heading
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* per feature/fix heading for each version in the range the user actually
|
|
4
|
+
* moved through, `(fromVersion, toVersion]`. Headings are recognized in both
|
|
5
|
+
* changelog formats — the current `- **Title.** prose…` single-line bullets
|
|
6
|
+
* and the older standalone `**Heading**` lines. The verbose prose/sub-bullets
|
|
7
|
+
* are intentionally dropped — the full notes live in the changelog.
|
|
6
8
|
*
|
|
7
9
|
* Returns colored lines ready to print, empty when nothing is in range.
|
|
8
10
|
*/
|
package/dist/lib/whats-new.js
CHANGED
|
@@ -2,15 +2,23 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { compareVersions } from './agent-spec/primitives.js';
|
|
3
3
|
/**
|
|
4
4
|
* Render a compact "What's new" summary from a CHANGELOG.md body: one bullet
|
|
5
|
-
* per feature/fix heading
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* per feature/fix heading for each version in the range the user actually
|
|
6
|
+
* moved through, `(fromVersion, toVersion]`. Headings are recognized in both
|
|
7
|
+
* changelog formats — the current `- **Title.** prose…` single-line bullets
|
|
8
|
+
* and the older standalone `**Heading**` lines. The verbose prose/sub-bullets
|
|
9
|
+
* are intentionally dropped — the full notes live in the changelog.
|
|
8
10
|
*
|
|
9
11
|
* Returns colored lines ready to print, empty when nothing is in range.
|
|
10
12
|
*/
|
|
11
13
|
export function renderWhatsNew(changelog, fromVersion, toVersion) {
|
|
12
14
|
const out = [];
|
|
13
15
|
let inRelevantSection = false;
|
|
16
|
+
// Whether the CURRENT version section uses the old standalone-heading format.
|
|
17
|
+
// Old sections nest `-` sub-bullets under each `**Heading**` line, and some
|
|
18
|
+
// sub-bullets are themselves bold-led (`- **Claim.** detail…`) — once a
|
|
19
|
+
// standalone heading is seen, `- **` lines in that section are sub-bullets,
|
|
20
|
+
// not entries, and must not render.
|
|
21
|
+
let sectionUsesStandaloneHeadings = false;
|
|
14
22
|
for (const line of changelog.split('\n')) {
|
|
15
23
|
const versionMatch = line.match(/^## (\d+\.\d+\.\d+)/);
|
|
16
24
|
if (versionMatch) {
|
|
@@ -20,15 +28,27 @@ export function renderWhatsNew(changelog, fromVersion, toVersion) {
|
|
|
20
28
|
inRelevantSection =
|
|
21
29
|
compareVersions(currentVersion, fromVersion) > 0 &&
|
|
22
30
|
compareVersions(currentVersion, toVersion) <= 0;
|
|
31
|
+
sectionUsesStandaloneHeadings = false;
|
|
23
32
|
if (inRelevantSection) {
|
|
24
33
|
out.push('');
|
|
25
34
|
out.push(chalk.bold(`v${currentVersion}`));
|
|
26
35
|
}
|
|
27
36
|
continue;
|
|
28
37
|
}
|
|
29
|
-
// Only the
|
|
30
|
-
|
|
38
|
+
// Only the entry headings — one bullet per feature/fix. Two formats exist
|
|
39
|
+
// across the changelog's history: the current single-line bullets
|
|
40
|
+
// (`- **Title.** verbose prose…`, heading kept, prose dropped) and the
|
|
41
|
+
// older standalone `**Heading**` lines with `-` sub-bullets beneath.
|
|
42
|
+
if (!inRelevantSection)
|
|
43
|
+
continue;
|
|
44
|
+
if (line.startsWith('**') && line.endsWith('**')) {
|
|
45
|
+
sectionUsesStandaloneHeadings = true;
|
|
31
46
|
out.push(` ${chalk.cyan('•')} ${line.replace(/\*\*/g, '')}`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const entryBullet = sectionUsesStandaloneHeadings ? null : line.match(/^- \*\*(.+?)\*\*/);
|
|
50
|
+
if (entryBullet) {
|
|
51
|
+
out.push(` ${chalk.cyan('•')} ${entryBullet[1].replace(/\*\*/g, '')}`);
|
|
32
52
|
}
|
|
33
53
|
}
|
|
34
54
|
return out;
|