@phnx-labs/agents-cli 1.20.0 → 1.20.3
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 +73 -0
- package/README.md +4 -4
- package/dist/commands/cli.js +3 -3
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +24 -7
- package/dist/commands/exec.js +36 -16
- package/dist/commands/feedback.d.ts +7 -0
- package/dist/commands/feedback.js +89 -0
- package/dist/commands/helper.d.ts +12 -0
- package/dist/commands/helper.js +87 -0
- package/dist/commands/hooks.js +86 -7
- package/dist/commands/mcp.js +166 -10
- package/dist/commands/packages.js +196 -27
- package/dist/commands/permissions.js +21 -6
- package/dist/commands/profiles.d.ts +8 -0
- package/dist/commands/profiles.js +117 -4
- package/dist/commands/pull.js +4 -4
- package/dist/commands/routines.js +6 -6
- package/dist/commands/rules.js +8 -4
- package/dist/commands/secrets-migrate.d.ts +24 -0
- package/dist/commands/secrets-migrate.js +198 -0
- package/dist/commands/secrets-sync.d.ts +11 -0
- package/dist/commands/secrets-sync.js +155 -0
- package/dist/commands/secrets.js +74 -39
- package/dist/commands/skills.js +22 -5
- package/dist/commands/subagents.js +69 -49
- package/dist/commands/teams.js +48 -10
- package/dist/commands/utils.d.ts +33 -0
- package/dist/commands/utils.js +139 -0
- package/dist/commands/versions.js +4 -4
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +164 -8
- package/dist/commands/workflows.js +29 -6
- package/dist/index.js +4 -0
- package/dist/lib/acp/client.js +6 -1
- package/dist/lib/agents.d.ts +4 -0
- package/dist/lib/agents.js +18 -14
- package/dist/lib/auto-pull-worker.js +18 -1
- package/dist/lib/browser/chrome.js +4 -0
- package/dist/lib/browser/drivers/ssh.js +1 -1
- package/dist/lib/browser/profiles.d.ts +3 -3
- package/dist/lib/browser/profiles.js +3 -3
- package/dist/lib/browser/service.js +19 -0
- package/dist/lib/browser/types.d.ts +4 -4
- package/dist/lib/cli-resources.d.ts +36 -8
- package/dist/lib/cli-resources.js +268 -46
- package/dist/lib/cloud/factory.d.ts +1 -1
- package/dist/lib/cloud/factory.js +1 -1
- package/dist/lib/events.d.ts +16 -2
- package/dist/lib/events.js +33 -2
- package/dist/lib/exec.d.ts +39 -11
- package/dist/lib/exec.js +90 -31
- package/dist/lib/help.js +11 -5
- package/dist/lib/hooks/cache.d.ts +38 -0
- package/dist/lib/hooks/cache.js +242 -0
- package/dist/lib/hooks/profile.d.ts +33 -0
- package/dist/lib/hooks/profile.js +129 -0
- package/dist/lib/hooks.d.ts +0 -10
- package/dist/lib/hooks.js +68 -15
- package/dist/lib/mcp.d.ts +15 -0
- package/dist/lib/mcp.js +40 -0
- package/dist/lib/permissions.d.ts +13 -0
- package/dist/lib/permissions.js +51 -1
- package/dist/lib/plugins.js +15 -1
- package/dist/lib/profiles-presets.d.ts +26 -0
- package/dist/lib/profiles-presets.js +187 -8
- package/dist/lib/profiles.d.ts +34 -0
- package/dist/lib/profiles.js +112 -1
- package/dist/lib/routines-format.d.ts +17 -5
- package/dist/lib/routines-format.js +37 -16
- package/dist/lib/routines.d.ts +1 -1
- package/dist/lib/routines.js +2 -2
- package/dist/lib/runner.js +64 -10
- 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 +1 -9
- package/dist/lib/secrets/bundles.d.ts +18 -22
- package/dist/lib/secrets/bundles.js +75 -99
- package/dist/lib/secrets/index.d.ts +51 -27
- package/dist/lib/secrets/index.js +147 -156
- package/dist/lib/secrets/install-helper.d.ts +45 -0
- package/dist/lib/secrets/install-helper.js +165 -0
- package/dist/lib/secrets/linux.js +4 -4
- package/dist/lib/secrets/sync.d.ts +56 -0
- package/dist/lib/secrets/sync.js +180 -0
- package/dist/lib/session/render.js +4 -4
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/shims.d.ts +4 -1
- package/dist/lib/shims.js +5 -35
- package/dist/lib/state.d.ts +14 -1
- package/dist/lib/state.js +49 -5
- package/dist/lib/teams/agents.d.ts +5 -4
- package/dist/lib/teams/agents.js +47 -21
- package/dist/lib/teams/api.d.ts +2 -1
- package/dist/lib/teams/api.js +4 -3
- package/dist/lib/types.d.ts +57 -1
- package/dist/lib/types.js +2 -0
- package/dist/lib/usage.d.ts +27 -2
- package/dist/lib/usage.js +100 -17
- package/dist/lib/versions.d.ts +35 -1
- package/dist/lib/versions.js +267 -64
- package/package.json +9 -8
- package/scripts/install-helper.js +97 -0
- package/scripts/postinstall.js +16 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable install location for the signed macOS Keychain helper.
|
|
3
|
+
*
|
|
4
|
+
* Why a stable path: every npm publish re-signs `Agents CLI.app` with a fresh
|
|
5
|
+
* timestamp, producing a new code signature. macOS Keychain trusted-app ACLs
|
|
6
|
+
* are pinned to the exact signature, so the ACL invalidates on every release
|
|
7
|
+
* when the helper lives inside the npm package directory. Copying the .app
|
|
8
|
+
* once to `~/Library/Application Support/agents-cli/` gives it a path that
|
|
9
|
+
* survives `npm i -g`, `scripts/install.sh`, version bumps, etc.
|
|
10
|
+
*
|
|
11
|
+
* This module is the SINGLE SOURCE OF TRUTH for the helper path. Other
|
|
12
|
+
* modules in `src/lib/secrets/` must import `getKeychainHelperPath()` rather
|
|
13
|
+
* than recomputing it.
|
|
14
|
+
*/
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import { spawnSync } from 'child_process';
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as os from 'os';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
const APP_BUNDLE_NAME = 'Agents CLI.app';
|
|
21
|
+
const INSTALL_DIR_NAME = 'agents-cli';
|
|
22
|
+
/** Absolute path to the installed `.app` bundle directory (not the executable). */
|
|
23
|
+
function installedAppPath() {
|
|
24
|
+
return path.join(os.homedir(), 'Library', 'Application Support', INSTALL_DIR_NAME, APP_BUNDLE_NAME);
|
|
25
|
+
}
|
|
26
|
+
/** Absolute path to the executable inside the installed `.app` bundle. */
|
|
27
|
+
function installedExecutablePath() {
|
|
28
|
+
return path.join(installedAppPath(), 'Contents', 'MacOS', 'Agents CLI');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Locate the source `.app` bundle shipped alongside the compiled JS.
|
|
32
|
+
*
|
|
33
|
+
* Resolution order:
|
|
34
|
+
* 1. dist/lib/secrets/Agents CLI.app — sibling of this compiled file (npm install layout)
|
|
35
|
+
* 2. <repo>/bin/Agents CLI.app — raw working tree (`bun run dev`, tsx from src/)
|
|
36
|
+
*
|
|
37
|
+
* Throws if neither exists.
|
|
38
|
+
*/
|
|
39
|
+
function sourceAppPath() {
|
|
40
|
+
const candidates = [];
|
|
41
|
+
try {
|
|
42
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
43
|
+
candidates.push(path.join(here, APP_BUNDLE_NAME));
|
|
44
|
+
// tsx/src case: src/lib/secrets/install-helper.ts -> ../../../bin/Agents CLI.app
|
|
45
|
+
candidates.push(path.resolve(here, '..', '..', '..', 'bin', APP_BUNDLE_NAME));
|
|
46
|
+
}
|
|
47
|
+
catch { /* import.meta.url unavailable */ }
|
|
48
|
+
for (const c of candidates) {
|
|
49
|
+
if (fs.existsSync(c))
|
|
50
|
+
return c;
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`Source ${APP_BUNDLE_NAME} not found. Looked in:\n ${candidates.join('\n ')}\n` +
|
|
53
|
+
'The npm package may have been built without the signed helper bundle. Reinstall agents-cli.');
|
|
54
|
+
}
|
|
55
|
+
function assertDarwin() {
|
|
56
|
+
if (process.platform !== 'darwin') {
|
|
57
|
+
throw new Error('Keychain helper is macOS only.');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function codesignVerify(appPath) {
|
|
61
|
+
const r = spawnSync('codesign', ['--verify', '--deep', '--strict', appPath], {
|
|
62
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
63
|
+
encoding: 'utf-8',
|
|
64
|
+
});
|
|
65
|
+
return { ok: r.status === 0, output: (r.stderr || r.stdout || '').toString().trim() };
|
|
66
|
+
}
|
|
67
|
+
function spctlAssess(appPath) {
|
|
68
|
+
const r = spawnSync('spctl', ['--assess', '--type', 'execute', '--verbose=2', appPath], {
|
|
69
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
70
|
+
encoding: 'utf-8',
|
|
71
|
+
});
|
|
72
|
+
return { ok: r.status === 0, output: (r.stderr || r.stdout || '').toString().trim() };
|
|
73
|
+
}
|
|
74
|
+
function copyAppBundle(src, dest) {
|
|
75
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
76
|
+
if (fs.existsSync(dest))
|
|
77
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
78
|
+
// `cp -R` preserves the bundle's signature, symlinks, and resource forks.
|
|
79
|
+
// `fs.cpSync({recursive: true})` works on simple trees but has historically
|
|
80
|
+
// mishandled extended attributes on `.app` bundles, breaking codesign.
|
|
81
|
+
const r = spawnSync('cp', ['-R', src, dest], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8' });
|
|
82
|
+
if (r.status !== 0) {
|
|
83
|
+
const msg = (r.stderr || r.stdout || '').toString().trim();
|
|
84
|
+
throw new Error(`Failed to copy ${src} -> ${dest}: ${msg || 'unknown error'}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Idempotent install. Copies the bundled `.app` to the stable user path. Skips
|
|
89
|
+
* if the destination already exists and `codesign --verify` passes, unless
|
|
90
|
+
* `forceReinstall=true`.
|
|
91
|
+
*
|
|
92
|
+
* Notarization is checked via `spctl --assess` after install — a failure is
|
|
93
|
+
* logged as a warning but does NOT throw. Notarization checks require network
|
|
94
|
+
* access (Gatekeeper ticket lookup) and are not load-bearing for the helper's
|
|
95
|
+
* keychain ACL semantics.
|
|
96
|
+
*/
|
|
97
|
+
export function ensureKeychainHelperInstalled(opts = {}) {
|
|
98
|
+
assertDarwin();
|
|
99
|
+
const dest = installedAppPath();
|
|
100
|
+
if (!opts.forceReinstall && fs.existsSync(dest)) {
|
|
101
|
+
const { ok } = codesignVerify(dest);
|
|
102
|
+
if (ok)
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const src = sourceAppPath();
|
|
106
|
+
copyAppBundle(src, dest);
|
|
107
|
+
const verify = codesignVerify(dest);
|
|
108
|
+
if (!verify.ok) {
|
|
109
|
+
throw new Error(`Installed helper failed codesign verification at ${dest}.\n${verify.output}\n` +
|
|
110
|
+
'The bundle may be corrupted. Try `agents helper install` to reinstall, or reinstall agents-cli.');
|
|
111
|
+
}
|
|
112
|
+
const assess = spctlAssess(dest);
|
|
113
|
+
if (!assess.ok) {
|
|
114
|
+
// Warn, do not fail. Gatekeeper ticket lookup needs network; offline
|
|
115
|
+
// installs and CI runners commonly fail this check. The ACL semantics
|
|
116
|
+
// we care about depend on codesign, not spctl.
|
|
117
|
+
process.stderr.write(`agents-cli: notarization check (spctl) did not pass for ${dest}: ${assess.output}\n`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Return the absolute path to the helper executable. If the installed bundle
|
|
122
|
+
* is missing, performs a lazy install first.
|
|
123
|
+
*
|
|
124
|
+
* Throws on non-darwin.
|
|
125
|
+
*/
|
|
126
|
+
export function getKeychainHelperPath() {
|
|
127
|
+
assertDarwin();
|
|
128
|
+
const exec = installedExecutablePath();
|
|
129
|
+
if (!fs.existsSync(exec)) {
|
|
130
|
+
ensureKeychainHelperInstalled();
|
|
131
|
+
}
|
|
132
|
+
return exec;
|
|
133
|
+
}
|
|
134
|
+
export function getKeychainHelperStatus() {
|
|
135
|
+
assertDarwin();
|
|
136
|
+
const destApp = installedAppPath();
|
|
137
|
+
let src = null;
|
|
138
|
+
try {
|
|
139
|
+
src = sourceAppPath();
|
|
140
|
+
}
|
|
141
|
+
catch { /* missing source is reported as null */ }
|
|
142
|
+
const installed = fs.existsSync(destApp);
|
|
143
|
+
if (!installed) {
|
|
144
|
+
return {
|
|
145
|
+
source: src,
|
|
146
|
+
destination: destApp,
|
|
147
|
+
installed: false,
|
|
148
|
+
codesignOk: false,
|
|
149
|
+
codesignOutput: 'not installed',
|
|
150
|
+
spctlOk: false,
|
|
151
|
+
spctlOutput: 'not installed',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const cs = codesignVerify(destApp);
|
|
155
|
+
const sp = spctlAssess(destApp);
|
|
156
|
+
return {
|
|
157
|
+
source: src,
|
|
158
|
+
destination: destApp,
|
|
159
|
+
installed: true,
|
|
160
|
+
codesignOk: cs.ok,
|
|
161
|
+
codesignOutput: cs.output || 'ok',
|
|
162
|
+
spctlOk: sp.ok,
|
|
163
|
+
spctlOutput: sp.output || 'ok',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -143,16 +143,16 @@ export function listSecretToolItems(prefix) {
|
|
|
143
143
|
}
|
|
144
144
|
/** KeychainBackend implementation for Linux using secret-tool */
|
|
145
145
|
export const linuxBackend = {
|
|
146
|
-
has(item
|
|
146
|
+
has(item) {
|
|
147
147
|
return hasSecretToolToken(item);
|
|
148
148
|
},
|
|
149
|
-
get(item
|
|
149
|
+
get(item) {
|
|
150
150
|
return getSecretToolToken(item);
|
|
151
151
|
},
|
|
152
|
-
set(item, value
|
|
152
|
+
set(item, value) {
|
|
153
153
|
setSecretToolToken(item, value);
|
|
154
154
|
},
|
|
155
|
-
delete(item
|
|
155
|
+
delete(item) {
|
|
156
156
|
return deleteSecretToolToken(item);
|
|
157
157
|
},
|
|
158
158
|
list(prefix) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote sync client for secrets bundles.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the previous "leave it to iCloud Keychain" model with explicit
|
|
5
|
+
* push/pull against api.prix.dev. Bundle contents (vars + secret values) are
|
|
6
|
+
* encrypted client-side with AES-256-GCM under a key derived from a
|
|
7
|
+
* user-supplied passphrase via PBKDF2-SHA256. Plaintext never leaves the
|
|
8
|
+
* machine — api.prix.dev only ever sees the ciphertext + KDF parameters.
|
|
9
|
+
*/
|
|
10
|
+
import { type SecretsBundle } from './bundles.js';
|
|
11
|
+
export declare const MIN_PASSPHRASE_LEN = 12;
|
|
12
|
+
/** Envelope for an encrypted bundle. All byte fields are base64. */
|
|
13
|
+
export interface EncryptedEnvelope {
|
|
14
|
+
v: 1;
|
|
15
|
+
kdf: 'pbkdf2-sha256';
|
|
16
|
+
iter: number;
|
|
17
|
+
salt: string;
|
|
18
|
+
iv: string;
|
|
19
|
+
ct: string;
|
|
20
|
+
tag: string;
|
|
21
|
+
}
|
|
22
|
+
interface RemoteBundleSummary {
|
|
23
|
+
name: string;
|
|
24
|
+
updated_at: string;
|
|
25
|
+
}
|
|
26
|
+
/** Encrypt a JSON-serializable payload with a passphrase. */
|
|
27
|
+
export declare function encryptBlob(plaintext: string, passphrase: string): EncryptedEnvelope;
|
|
28
|
+
/** Decrypt an envelope. Throws on bad passphrase (auth tag mismatch). */
|
|
29
|
+
export declare function decryptBlob(envelope: EncryptedEnvelope, passphrase: string): string;
|
|
30
|
+
/** The plaintext we serialize before encrypting: bundle metadata + secret values. */
|
|
31
|
+
export interface BundleSnapshot {
|
|
32
|
+
bundle: SecretsBundle;
|
|
33
|
+
/** keychain shortId -> plaintext value. Only present for keychain: refs. */
|
|
34
|
+
secrets: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
/** Options for pushBundle. */
|
|
37
|
+
export interface PushOptions {
|
|
38
|
+
passphrase: string;
|
|
39
|
+
}
|
|
40
|
+
/** Push a local bundle to api.prix.dev. Encrypts client-side; server only sees ciphertext. */
|
|
41
|
+
export declare function pushBundle(name: string, opts: PushOptions): Promise<{
|
|
42
|
+
updated_at: string;
|
|
43
|
+
}>;
|
|
44
|
+
/** Options for pullBundle. */
|
|
45
|
+
export interface PullOptions {
|
|
46
|
+
passphrase: string;
|
|
47
|
+
/** When true, overwrite an existing local bundle. */
|
|
48
|
+
force?: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** Pull a bundle by name from api.prix.dev and materialize it locally. */
|
|
51
|
+
export declare function pullBundle(name: string, opts: PullOptions): Promise<SecretsBundle>;
|
|
52
|
+
/** Delete a bundle on the remote. */
|
|
53
|
+
export declare function deleteRemoteBundle(name: string): Promise<boolean>;
|
|
54
|
+
/** List bundles currently stored on api.prix.dev for this user. */
|
|
55
|
+
export declare function listRemoteBundles(): Promise<RemoteBundleSummary[]>;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote sync client for secrets bundles.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the previous "leave it to iCloud Keychain" model with explicit
|
|
5
|
+
* push/pull against api.prix.dev. Bundle contents (vars + secret values) are
|
|
6
|
+
* encrypted client-side with AES-256-GCM under a key derived from a
|
|
7
|
+
* user-supplied passphrase via PBKDF2-SHA256. Plaintext never leaves the
|
|
8
|
+
* machine — api.prix.dev only ever sees the ciphertext + KDF parameters.
|
|
9
|
+
*/
|
|
10
|
+
import * as crypto from 'crypto';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as os from 'os';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import * as yaml from 'yaml';
|
|
15
|
+
import { getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from './index.js';
|
|
16
|
+
import { readBundle, writeBundle, keychainItemsForBundle, validateBundleName, } from './bundles.js';
|
|
17
|
+
const PROXY_BASE = 'https://api.prix.dev';
|
|
18
|
+
const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
|
|
19
|
+
const BUNDLE_ENDPOINT = '/api/v1/secrets/bundles';
|
|
20
|
+
// PBKDF2 cost. 600k SHA-256 iters matches OWASP 2023+ guidance and keeps a
|
|
21
|
+
// passphrase prompt under a second on the hardware the CLI targets.
|
|
22
|
+
const PBKDF2_ITER = 600_000;
|
|
23
|
+
export const MIN_PASSPHRASE_LEN = 12;
|
|
24
|
+
const KEY_LEN = 32;
|
|
25
|
+
const SALT_LEN = 16;
|
|
26
|
+
const IV_LEN = 12;
|
|
27
|
+
function readRushToken() {
|
|
28
|
+
if (!fs.existsSync(USER_YAML)) {
|
|
29
|
+
throw new Error('Not logged in to Rush. Run `rush login` first.');
|
|
30
|
+
}
|
|
31
|
+
const raw = fs.readFileSync(USER_YAML, 'utf-8');
|
|
32
|
+
const data = yaml.parse(raw);
|
|
33
|
+
const token = data?.session?.access_token;
|
|
34
|
+
if (!token) {
|
|
35
|
+
throw new Error('No session token in ~/.rush/user.yaml. Run `rush login` first.');
|
|
36
|
+
}
|
|
37
|
+
return token;
|
|
38
|
+
}
|
|
39
|
+
async function api(method, endpoint, body) {
|
|
40
|
+
const token = readRushToken();
|
|
41
|
+
const url = endpoint.startsWith('http') ? endpoint : `${PROXY_BASE}${endpoint}`;
|
|
42
|
+
return fetch(url, {
|
|
43
|
+
method,
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: `Bearer ${token}`,
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
},
|
|
48
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function deriveKey(passphrase, salt) {
|
|
52
|
+
return crypto.pbkdf2Sync(passphrase, salt, PBKDF2_ITER, KEY_LEN, 'sha256');
|
|
53
|
+
}
|
|
54
|
+
/** Encrypt a JSON-serializable payload with a passphrase. */
|
|
55
|
+
export function encryptBlob(plaintext, passphrase) {
|
|
56
|
+
if (!passphrase || passphrase.length < MIN_PASSPHRASE_LEN) {
|
|
57
|
+
throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
|
|
58
|
+
}
|
|
59
|
+
const salt = crypto.randomBytes(SALT_LEN);
|
|
60
|
+
const iv = crypto.randomBytes(IV_LEN);
|
|
61
|
+
const key = deriveKey(passphrase, salt);
|
|
62
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
63
|
+
const ct = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
|
|
64
|
+
const tag = cipher.getAuthTag();
|
|
65
|
+
return {
|
|
66
|
+
v: 1,
|
|
67
|
+
kdf: 'pbkdf2-sha256',
|
|
68
|
+
iter: PBKDF2_ITER,
|
|
69
|
+
salt: salt.toString('base64'),
|
|
70
|
+
iv: iv.toString('base64'),
|
|
71
|
+
ct: ct.toString('base64'),
|
|
72
|
+
tag: tag.toString('base64'),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Decrypt an envelope. Throws on bad passphrase (auth tag mismatch). */
|
|
76
|
+
export function decryptBlob(envelope, passphrase) {
|
|
77
|
+
if (envelope.v !== 1 || envelope.kdf !== 'pbkdf2-sha256') {
|
|
78
|
+
throw new Error(`Unsupported envelope version (v${envelope.v}, kdf=${envelope.kdf}).`);
|
|
79
|
+
}
|
|
80
|
+
const salt = Buffer.from(envelope.salt, 'base64');
|
|
81
|
+
const iv = Buffer.from(envelope.iv, 'base64');
|
|
82
|
+
const ct = Buffer.from(envelope.ct, 'base64');
|
|
83
|
+
const tag = Buffer.from(envelope.tag, 'base64');
|
|
84
|
+
const key = deriveKey(passphrase, salt);
|
|
85
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
86
|
+
decipher.setAuthTag(tag);
|
|
87
|
+
try {
|
|
88
|
+
return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf-8');
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new Error('Decryption failed — wrong passphrase or corrupt blob.');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function snapshotBundle(name) {
|
|
95
|
+
const bundle = readBundle(name);
|
|
96
|
+
const secrets = {};
|
|
97
|
+
for (const { key, item } of keychainItemsForBundle(bundle)) {
|
|
98
|
+
if (!hasKeychainToken(item)) {
|
|
99
|
+
throw new Error(`Bundle '${name}' key '${key}': keychain item '${item}' missing — cannot push incomplete bundle.`);
|
|
100
|
+
}
|
|
101
|
+
const raw = bundle.vars[key];
|
|
102
|
+
if (typeof raw !== 'string' || !raw.startsWith('keychain:'))
|
|
103
|
+
continue;
|
|
104
|
+
const shortId = raw.slice('keychain:'.length);
|
|
105
|
+
secrets[shortId] = getKeychainToken(item);
|
|
106
|
+
}
|
|
107
|
+
return { bundle, secrets };
|
|
108
|
+
}
|
|
109
|
+
function restoreSnapshot(snap) {
|
|
110
|
+
const bundle = snap.bundle;
|
|
111
|
+
validateBundleName(bundle.name);
|
|
112
|
+
for (const [shortId, value] of Object.entries(snap.secrets)) {
|
|
113
|
+
const item = secretsKeychainItem(bundle.name, shortId);
|
|
114
|
+
setKeychainToken(item, value);
|
|
115
|
+
}
|
|
116
|
+
writeBundle(bundle);
|
|
117
|
+
}
|
|
118
|
+
/** Push a local bundle to api.prix.dev. Encrypts client-side; server only sees ciphertext. */
|
|
119
|
+
export async function pushBundle(name, opts) {
|
|
120
|
+
validateBundleName(name);
|
|
121
|
+
const snap = snapshotBundle(name);
|
|
122
|
+
const envelope = encryptBlob(JSON.stringify(snap), opts.passphrase);
|
|
123
|
+
const updated_at = new Date().toISOString();
|
|
124
|
+
const payload = { envelope, updated_at };
|
|
125
|
+
const res = await api('PUT', `${BUNDLE_ENDPOINT}/${encodeURIComponent(name)}`, payload);
|
|
126
|
+
if (!res.ok) {
|
|
127
|
+
const body = await res.text().catch(() => '');
|
|
128
|
+
throw new Error(`Push failed (${res.status} ${res.statusText}): ${body}`);
|
|
129
|
+
}
|
|
130
|
+
return { updated_at };
|
|
131
|
+
}
|
|
132
|
+
/** Pull a bundle by name from api.prix.dev and materialize it locally. */
|
|
133
|
+
export async function pullBundle(name, opts) {
|
|
134
|
+
validateBundleName(name);
|
|
135
|
+
const res = await api('GET', `${BUNDLE_ENDPOINT}/${encodeURIComponent(name)}`);
|
|
136
|
+
if (res.status === 404) {
|
|
137
|
+
throw new Error(`Remote bundle '${name}' not found on api.prix.dev.`);
|
|
138
|
+
}
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
const body = await res.text().catch(() => '');
|
|
141
|
+
throw new Error(`Pull failed (${res.status} ${res.statusText}): ${body}`);
|
|
142
|
+
}
|
|
143
|
+
const data = await res.json();
|
|
144
|
+
const plaintext = decryptBlob(data.envelope, opts.passphrase);
|
|
145
|
+
const snap = JSON.parse(plaintext);
|
|
146
|
+
if (!snap || !snap.bundle || snap.bundle.name !== name) {
|
|
147
|
+
throw new Error(`Decrypted payload for '${name}' is malformed (bundle name mismatch).`);
|
|
148
|
+
}
|
|
149
|
+
// existence check is the caller's responsibility; we trust opts.force.
|
|
150
|
+
if (!opts.force) {
|
|
151
|
+
const { bundleExists } = await import('./bundles.js');
|
|
152
|
+
if (bundleExists(name)) {
|
|
153
|
+
throw new Error(`Local bundle '${name}' already exists. Re-run with --force to overwrite.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
restoreSnapshot(snap);
|
|
157
|
+
return snap.bundle;
|
|
158
|
+
}
|
|
159
|
+
/** Delete a bundle on the remote. */
|
|
160
|
+
export async function deleteRemoteBundle(name) {
|
|
161
|
+
validateBundleName(name);
|
|
162
|
+
const res = await api('DELETE', `${BUNDLE_ENDPOINT}/${encodeURIComponent(name)}`);
|
|
163
|
+
if (res.status === 404)
|
|
164
|
+
return false;
|
|
165
|
+
if (!res.ok) {
|
|
166
|
+
const body = await res.text().catch(() => '');
|
|
167
|
+
throw new Error(`Delete failed (${res.status} ${res.statusText}): ${body}`);
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
/** List bundles currently stored on api.prix.dev for this user. */
|
|
172
|
+
export async function listRemoteBundles() {
|
|
173
|
+
const res = await api('GET', BUNDLE_ENDPOINT);
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
const body = await res.text().catch(() => '');
|
|
176
|
+
throw new Error(`List failed (${res.status} ${res.statusText}): ${body}`);
|
|
177
|
+
}
|
|
178
|
+
const data = await res.json();
|
|
179
|
+
return data.bundles ?? [];
|
|
180
|
+
}
|
|
@@ -806,15 +806,15 @@ export function renderConversationMarkdown(events, opts = {}) {
|
|
|
806
806
|
for (const event of events) {
|
|
807
807
|
if (event.type === 'message') {
|
|
808
808
|
if (event.role === 'user') {
|
|
809
|
-
parts.push(`## User\n\n${event.content ?? ''}`);
|
|
809
|
+
parts.push(`## User\n\n${sanitize(event.content ?? '')}`);
|
|
810
810
|
}
|
|
811
811
|
else if (event.role === 'assistant') {
|
|
812
|
-
parts.push(`## Assistant\n\n${event.content ?? ''}`);
|
|
812
|
+
parts.push(`## Assistant\n\n${sanitize(event.content ?? '')}`);
|
|
813
813
|
}
|
|
814
814
|
}
|
|
815
815
|
else if (event.type === 'thinking') {
|
|
816
816
|
if (event.content)
|
|
817
|
-
parts.push(`### Thinking\n\n${event.content}`);
|
|
817
|
+
parts.push(`### Thinking\n\n${sanitize(event.content)}`);
|
|
818
818
|
}
|
|
819
819
|
else if (event.type === 'tool_use') {
|
|
820
820
|
const tool = event.tool || 'unknown';
|
|
@@ -837,7 +837,7 @@ export function renderConversationMarkdown(events, opts = {}) {
|
|
|
837
837
|
}
|
|
838
838
|
}
|
|
839
839
|
else if (event.type === 'error') {
|
|
840
|
-
parts.push(`### Error\n\n${event.content
|
|
840
|
+
parts.push(`### Error\n\n${event.content ? sanitize(event.content) : (event.tool || 'Unknown error')}`);
|
|
841
841
|
}
|
|
842
842
|
}
|
|
843
843
|
return parts.join('\n\n');
|
|
@@ -37,7 +37,7 @@ export interface SessionEvent {
|
|
|
37
37
|
export interface TeamOrigin {
|
|
38
38
|
/** Teammate name if set, otherwise first 8 chars of the agent UUID. */
|
|
39
39
|
handle?: string;
|
|
40
|
-
/** Agent mode: 'plan', 'edit', or 'full'. */
|
|
40
|
+
/** Agent mode: 'plan', 'edit', 'auto', or 'skip' ('full' accepted as legacy alias for 'skip'). */
|
|
41
41
|
mode?: string;
|
|
42
42
|
}
|
|
43
43
|
/** Lightweight metadata for a discovered session, used in listings and pickers. */
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -59,8 +59,11 @@ export interface ConflictInfo {
|
|
|
59
59
|
* instead of hardcoding `.${agent}`. Backwards-compatible for every
|
|
60
60
|
* existing agent (their configDir is `~/.{agent}`); enables nested
|
|
61
61
|
* layouts like Antigravity's `~/.gemini/antigravity-cli/`.
|
|
62
|
+
* v15 — remove foreground resource sync / rules refresh from launch shims.
|
|
63
|
+
* Version homes are reconciled by agents-cli management commands; the
|
|
64
|
+
* shim hot path only resolves a version and execs the agent binary.
|
|
62
65
|
*/
|
|
63
|
-
export declare const SHIM_SCHEMA_VERSION =
|
|
66
|
+
export declare const SHIM_SCHEMA_VERSION = 15;
|
|
64
67
|
/**
|
|
65
68
|
* Generate the full bash shim script for the given agent. The returned string
|
|
66
69
|
* is written to ~/.agents/shims/{cliCommand} and made executable.
|
package/dist/lib/shims.js
CHANGED
|
@@ -183,8 +183,11 @@ async function promptConflictStrategy(conflictInfos) {
|
|
|
183
183
|
* instead of hardcoding `.${agent}`. Backwards-compatible for every
|
|
184
184
|
* existing agent (their configDir is `~/.{agent}`); enables nested
|
|
185
185
|
* layouts like Antigravity's `~/.gemini/antigravity-cli/`.
|
|
186
|
+
* v15 — remove foreground resource sync / rules refresh from launch shims.
|
|
187
|
+
* Version homes are reconciled by agents-cli management commands; the
|
|
188
|
+
* shim hot path only resolves a version and execs the agent binary.
|
|
186
189
|
*/
|
|
187
|
-
export const SHIM_SCHEMA_VERSION =
|
|
190
|
+
export const SHIM_SCHEMA_VERSION = 15;
|
|
188
191
|
/** Internal marker string used to embed the schema version in shim scripts. */
|
|
189
192
|
const SHIM_VERSION_MARKER = 'agents-shim-version:';
|
|
190
193
|
function shellQuote(value) {
|
|
@@ -242,18 +245,6 @@ export COPILOT_HOME="$VERSION_DIR/home/${configDirName}"
|
|
|
242
245
|
export GROK_HOME="$VERSION_DIR/home/.grok"
|
|
243
246
|
`
|
|
244
247
|
: '';
|
|
245
|
-
// Agents that don't natively resolve @-imports in their rules file need
|
|
246
|
-
// agents-cli to recompile when the user edits a rule/preset file. The
|
|
247
|
-
// check is fast (sha256 of ~8 small files) and skips the recompile when
|
|
248
|
-
// sources haven't changed.
|
|
249
|
-
const refreshRulesCall = !agentConfig.capabilities.rulesImports
|
|
250
|
-
? `
|
|
251
|
-
# Recompile rules if any rule/preset source has changed since last sync.
|
|
252
|
-
# Fast-path check (~10-20ms) when nothing changed; full recompile only on
|
|
253
|
-
# actual diff. Non-blocking failure — if the refresh errors, we still launch.
|
|
254
|
-
"$AGENTS_BIN" refresh-rules --agent "$AGENT" --agent-version "$VERSION" --quiet 2>/dev/null || true
|
|
255
|
-
`
|
|
256
|
-
: '';
|
|
257
248
|
const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
|
|
258
249
|
return `#!/bin/bash
|
|
259
250
|
# Auto-generated by agents-cli - do not edit
|
|
@@ -307,22 +298,6 @@ resolve_default_version() {
|
|
|
307
298
|
fi
|
|
308
299
|
}
|
|
309
300
|
|
|
310
|
-
# Find project-scoped .agents directory (stop at agents.yaml or .git)
|
|
311
|
-
find_project_agents_dir() {
|
|
312
|
-
local dir="$PWD"
|
|
313
|
-
while [ "$dir" != "/" ]; do
|
|
314
|
-
if [ -d "$dir/.agents" ]; then
|
|
315
|
-
echo "$dir/.agents"
|
|
316
|
-
return 0
|
|
317
|
-
fi
|
|
318
|
-
if [ -f "$dir/agents.yaml" ] || [ -d "$dir/.git" ] || [ -f "$dir/.git" ]; then
|
|
319
|
-
break
|
|
320
|
-
fi
|
|
321
|
-
dir=$(dirname "$dir")
|
|
322
|
-
done
|
|
323
|
-
return 1
|
|
324
|
-
}
|
|
325
|
-
|
|
326
301
|
# Find the latest installed version by numeric component comparison.
|
|
327
302
|
# Handles both semver (2.1.138) and date-based (2026.5.7) version strings.
|
|
328
303
|
find_latest_installed() {
|
|
@@ -474,12 +449,7 @@ if [ ! -x "$BINARY" ]; then
|
|
|
474
449
|
fi
|
|
475
450
|
fi
|
|
476
451
|
|
|
477
|
-
|
|
478
|
-
PROJECT_AGENTS_DIR=$(find_project_agents_dir)
|
|
479
|
-
if [ -n "$PROJECT_AGENTS_DIR" ]; then
|
|
480
|
-
"$AGENTS_BIN" sync --agent "$AGENT" --agent-version "$VERSION" --project-dir "$PROJECT_AGENTS_DIR" --quiet >/dev/null 2>&1
|
|
481
|
-
fi
|
|
482
|
-
${refreshRulesCall}${managedEnv}
|
|
452
|
+
${managedEnv}
|
|
483
453
|
|
|
484
454
|
exec "$BINARY"${launchArgs} "$@"
|
|
485
455
|
`;
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -107,6 +107,10 @@ export declare function getRunsDir(): string;
|
|
|
107
107
|
export declare function getVersionsDir(): string;
|
|
108
108
|
/** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
|
|
109
109
|
export declare function getShimsDir(): string;
|
|
110
|
+
/** Path to generated per-hook caching/timing shims (~/.agents/.cache/shims/hooks/). */
|
|
111
|
+
export declare function getHookShimsDir(): string;
|
|
112
|
+
/** Path to per-hook stdout cache files (~/.agents/.cache/state/hooks/). */
|
|
113
|
+
export declare function getHookCacheDir(): string;
|
|
110
114
|
/** Path to per-agent installed CLI binaries (~/.agents/.cache/bin/). */
|
|
111
115
|
export declare function getBinDir(): string;
|
|
112
116
|
/** Path to config backups (~/.agents/.history/backups/). */
|
|
@@ -191,7 +195,16 @@ export declare function getEnabledExtraRepos(): Array<{
|
|
|
191
195
|
export declare function ensureAgentsDir(): void;
|
|
192
196
|
/** Return an empty Meta object used when no agents.yaml exists yet. */
|
|
193
197
|
export declare function createDefaultMeta(): Meta;
|
|
194
|
-
/**
|
|
198
|
+
/**
|
|
199
|
+
* Read and cache ~/.agents/agents.yaml, migrating from legacy locations if needed.
|
|
200
|
+
*
|
|
201
|
+
* Cache invariants:
|
|
202
|
+
* - Cache key is the mtime of the user agents.yaml.
|
|
203
|
+
* - `writeMetaUnlocked` clears the cache; in-process callers always see fresh state.
|
|
204
|
+
* - If the file is mutated by ANOTHER process while we hold a stale cache, the
|
|
205
|
+
* mtime check below catches it on the next read (assuming the mtime advanced).
|
|
206
|
+
* - The cache stores the merged system+user meta; both files' mtimes contribute.
|
|
207
|
+
*/
|
|
195
208
|
export declare function readMeta(): Meta;
|
|
196
209
|
/** Serialize and write agents.yaml to the user repo, invalidating the in-memory cache. */
|
|
197
210
|
export declare function writeMeta(meta: Meta): void;
|
package/dist/lib/state.js
CHANGED
|
@@ -64,6 +64,8 @@ const BACKUPS_DIR = path.join(HISTORY_DIR, 'backups');
|
|
|
64
64
|
const TRASH_DIR = path.join(HISTORY_DIR, 'trash');
|
|
65
65
|
// Cache bucket (regenerable).
|
|
66
66
|
const SHIMS_DIR = path.join(CACHE_DIR, 'shims');
|
|
67
|
+
const HOOK_SHIMS_DIR = path.join(SHIMS_DIR, 'hooks');
|
|
68
|
+
const HOOK_CACHE_DIR = path.join(CACHE_DIR, 'state', 'hooks');
|
|
67
69
|
const BIN_DIR = path.join(CACHE_DIR, 'bin');
|
|
68
70
|
const PACKAGES_DIR = path.join(CACHE_DIR, 'packages');
|
|
69
71
|
// Plugins are user-authored resources, alongside skills/, commands/, hooks/.
|
|
@@ -265,6 +267,10 @@ export function getRunsDir() { return RUNS_DIR; }
|
|
|
265
267
|
export function getVersionsDir() { return VERSIONS_DIR; }
|
|
266
268
|
/** Path to version-switching shim scripts (~/.agents/.cache/shims/). */
|
|
267
269
|
export function getShimsDir() { return SHIMS_DIR; }
|
|
270
|
+
/** Path to generated per-hook caching/timing shims (~/.agents/.cache/shims/hooks/). */
|
|
271
|
+
export function getHookShimsDir() { return HOOK_SHIMS_DIR; }
|
|
272
|
+
/** Path to per-hook stdout cache files (~/.agents/.cache/state/hooks/). */
|
|
273
|
+
export function getHookCacheDir() { return HOOK_CACHE_DIR; }
|
|
268
274
|
/** Path to per-agent installed CLI binaries (~/.agents/.cache/bin/). */
|
|
269
275
|
export function getBinDir() { return BIN_DIR; }
|
|
270
276
|
/** Path to config backups (~/.agents/.history/backups/). */
|
|
@@ -413,6 +419,24 @@ export function createDefaultMeta() {
|
|
|
413
419
|
}
|
|
414
420
|
let metaCache = null;
|
|
415
421
|
let metaLockDepth = 0;
|
|
422
|
+
/** Return mtimeMs for a file path, or 0 if the file is absent or unreadable. */
|
|
423
|
+
function safeMtimeMs(filePath) {
|
|
424
|
+
try {
|
|
425
|
+
return fs.statSync(filePath).mtimeMs;
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
return 0;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/** Compute the combined cache stamp for the user + system agents.yaml files. */
|
|
432
|
+
function currentMetaStamp() {
|
|
433
|
+
return safeMtimeMs(META_FILE) + safeMtimeMs(SYSTEM_META_FILE) * 1e-3;
|
|
434
|
+
}
|
|
435
|
+
/** Memoize a parsed Meta against the current file mtimes. */
|
|
436
|
+
function rememberMeta(meta) {
|
|
437
|
+
metaCache = { mtime: currentMetaStamp(), meta };
|
|
438
|
+
return meta;
|
|
439
|
+
}
|
|
416
440
|
function withMetaLock(fn) {
|
|
417
441
|
ensureAgentsDir();
|
|
418
442
|
if (metaLockDepth > 0) {
|
|
@@ -483,9 +507,29 @@ function migrateSystemMetaToUser() {
|
|
|
483
507
|
// Best-effort; proceed with fresh state if it fails.
|
|
484
508
|
}
|
|
485
509
|
}
|
|
486
|
-
/**
|
|
510
|
+
/**
|
|
511
|
+
* Read and cache ~/.agents/agents.yaml, migrating from legacy locations if needed.
|
|
512
|
+
*
|
|
513
|
+
* Cache invariants:
|
|
514
|
+
* - Cache key is the mtime of the user agents.yaml.
|
|
515
|
+
* - `writeMetaUnlocked` clears the cache; in-process callers always see fresh state.
|
|
516
|
+
* - If the file is mutated by ANOTHER process while we hold a stale cache, the
|
|
517
|
+
* mtime check below catches it on the next read (assuming the mtime advanced).
|
|
518
|
+
* - The cache stores the merged system+user meta; both files' mtimes contribute.
|
|
519
|
+
*/
|
|
487
520
|
export function readMeta() {
|
|
488
521
|
ensureAgentsDir();
|
|
522
|
+
// Fast path: serve from cache when both source files are byte-identical to
|
|
523
|
+
// what we last parsed. Reduces N readMeta calls per CLI invocation to ~2 stat
|
|
524
|
+
// syscalls plus an in-memory object spread.
|
|
525
|
+
if (metaCache) {
|
|
526
|
+
const userMtime = safeMtimeMs(META_FILE);
|
|
527
|
+
const systemMtime = safeMtimeMs(SYSTEM_META_FILE);
|
|
528
|
+
const stamp = userMtime + systemMtime * 1e-3;
|
|
529
|
+
if (stamp === metaCache.mtime) {
|
|
530
|
+
return metaCache.meta;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
489
533
|
// NOTE: agents.yaml migration from ~/.agents-system/ to ~/.agents/ is handled
|
|
490
534
|
// exclusively by runMigration() in migrate.ts, called from postinstall and
|
|
491
535
|
// from a one-shot bootstrap step in src/index.ts. Calling it here would
|
|
@@ -515,7 +559,7 @@ export function readMeta() {
|
|
|
515
559
|
fs.unlinkSync(oldMetaFile);
|
|
516
560
|
}
|
|
517
561
|
catch { /* non-critical */ }
|
|
518
|
-
return meta;
|
|
562
|
+
return rememberMeta(meta);
|
|
519
563
|
}
|
|
520
564
|
catch {
|
|
521
565
|
/* meta.yaml migration failed */
|
|
@@ -557,15 +601,15 @@ export function readMeta() {
|
|
|
557
601
|
}
|
|
558
602
|
if (applyRegistrySeeds(meta)) {
|
|
559
603
|
writeMeta(meta);
|
|
560
|
-
return meta;
|
|
604
|
+
return rememberMeta(meta);
|
|
561
605
|
}
|
|
562
|
-
return meta;
|
|
606
|
+
return rememberMeta(meta);
|
|
563
607
|
}
|
|
564
608
|
const meta = createDefaultMeta();
|
|
565
609
|
if (applyRegistrySeeds(meta)) {
|
|
566
610
|
writeMeta(meta);
|
|
567
611
|
}
|
|
568
|
-
return meta;
|
|
612
|
+
return rememberMeta(meta);
|
|
569
613
|
}
|
|
570
614
|
/** Serialize and write agents.yaml to the user repo, invalidating the in-memory cache. */
|
|
571
615
|
export function writeMeta(meta) {
|