@phnx-labs/agents-cli 1.19.2 → 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 +140 -0
- package/README.md +72 -12
- package/dist/browser.js +0 -0
- package/dist/commands/browser.js +88 -16
- package/dist/commands/cli.d.ts +14 -0
- package/dist/commands/cli.js +244 -0
- package/dist/commands/cloud.js +1 -1
- package/dist/commands/commands.js +27 -10
- package/dist/commands/computer.js +18 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/exec.js +38 -18
- package/dist/commands/factory.d.ts +3 -14
- package/dist/commands/factory.js +3 -3
- 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 +89 -10
- package/dist/commands/mcp.js +166 -10
- package/dist/commands/packages.js +196 -27
- package/dist/commands/permissions.js +21 -6
- package/dist/commands/plugins.js +11 -4
- package/dist/commands/profiles.d.ts +8 -0
- package/dist/commands/profiles.js +118 -5
- package/dist/commands/prune.js +39 -160
- package/dist/commands/pull.js +58 -5
- package/dist/commands/routines.js +107 -14
- 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 +79 -46
- package/dist/commands/sessions.d.ts +28 -0
- package/dist/commands/sessions.js +98 -33
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +37 -28
- package/dist/commands/skills.js +25 -8
- package/dist/commands/subagents.js +69 -49
- package/dist/commands/teams.js +61 -10
- package/dist/commands/utils.d.ts +33 -0
- package/dist/commands/utils.js +139 -0
- package/dist/commands/versions.d.ts +4 -3
- package/dist/commands/versions.js +134 -130
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +175 -19
- package/dist/commands/workflows.js +29 -6
- package/dist/computer.js +0 -0
- package/dist/index.js +38 -6
- package/dist/lib/acp/client.js +6 -1
- package/dist/lib/acp/harnesses.js +8 -0
- package/dist/lib/agents.d.ts +4 -0
- package/dist/lib/agents.js +125 -34
- package/dist/lib/auto-pull-worker.js +18 -1
- package/dist/lib/browser/cdp.d.ts +8 -1
- package/dist/lib/browser/cdp.js +40 -3
- package/dist/lib/browser/chrome.d.ts +13 -0
- package/dist/lib/browser/chrome.js +46 -3
- package/dist/lib/browser/domain-skills.d.ts +51 -0
- package/dist/lib/browser/domain-skills.js +157 -0
- package/dist/lib/browser/drivers/local.js +45 -4
- package/dist/lib/browser/drivers/ssh.js +2 -2
- package/dist/lib/browser/ipc.d.ts +8 -1
- package/dist/lib/browser/ipc.js +37 -28
- package/dist/lib/browser/profiles.d.ts +16 -3
- package/dist/lib/browser/profiles.js +44 -4
- package/dist/lib/browser/service.d.ts +3 -0
- package/dist/lib/browser/service.js +40 -5
- package/dist/lib/browser/types.d.ts +11 -4
- package/dist/lib/cli-resources.d.ts +137 -0
- package/dist/lib/cli-resources.js +477 -0
- package/dist/lib/cloud/factory.d.ts +1 -1
- package/dist/lib/cloud/factory.js +1 -1
- package/dist/lib/cloud/rush.js +5 -5
- package/dist/lib/command-skills.js +0 -2
- package/dist/lib/computer-rpc.d.ts +3 -0
- package/dist/lib/computer-rpc.js +53 -0
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/events.d.ts +16 -2
- package/dist/lib/events.js +33 -2
- package/dist/lib/exec.d.ts +42 -13
- package/dist/lib/exec.js +127 -33
- 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 +246 -11
- package/dist/lib/mcp.d.ts +15 -0
- package/dist/lib/mcp.js +46 -0
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/overdue.d.ts +26 -0
- package/dist/lib/overdue.js +101 -0
- package/dist/lib/permissions.d.ts +13 -0
- package/dist/lib/permissions.js +55 -1
- package/dist/lib/plugin-marketplace.js +1 -1
- package/dist/lib/plugins.js +15 -1
- package/dist/lib/profiles-presets.d.ts +26 -0
- package/dist/lib/profiles-presets.js +216 -0
- package/dist/lib/profiles.d.ts +34 -0
- package/dist/lib/profiles.js +112 -1
- package/dist/lib/resources/mcp.js +37 -0
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/rotate.js +10 -4
- package/dist/lib/routines-format.d.ts +47 -0
- package/dist/lib/routines-format.js +194 -0
- package/dist/lib/routines.d.ts +8 -2
- package/dist/lib/routines.js +34 -14
- package/dist/lib/runner.js +83 -15
- package/dist/lib/scheduler.js +8 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
- package/dist/lib/secrets/bundles.d.ts +34 -17
- package/dist/lib/secrets/bundles.js +210 -36
- package/dist/lib/secrets/index.d.ts +49 -30
- package/dist/lib/secrets/index.js +126 -115
- 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/active.d.ts +8 -0
- package/dist/lib/session/active.js +3 -2
- package/dist/lib/session/db.d.ts +0 -4
- package/dist/lib/session/db.js +0 -26
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +44 -0
- package/dist/lib/session/render.js +4 -4
- package/dist/lib/session/types.d.ts +2 -2
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +5 -2
- package/dist/lib/shims.js +70 -38
- package/dist/lib/state.d.ts +14 -2
- package/dist/lib/state.js +51 -20
- package/dist/lib/teams/agents.d.ts +5 -4
- package/dist/lib/teams/agents.js +48 -22
- package/dist/lib/teams/api.d.ts +2 -1
- package/dist/lib/teams/api.js +4 -3
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +153 -3
- package/dist/lib/teams/summarizer.js +18 -2
- package/dist/lib/teams/worktree.js +14 -3
- package/dist/lib/types.d.ts +63 -4
- package/dist/lib/types.js +8 -3
- package/dist/lib/usage.d.ts +27 -2
- package/dist/lib/usage.js +100 -17
- package/dist/lib/versions.d.ts +45 -3
- package/dist/lib/versions.js +455 -60
- package/package.json +15 -14
- 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
- package/npm-shrinkwrap.json +0 -3162
|
@@ -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
|
+
}
|
|
@@ -20,6 +20,14 @@ export interface ActiveSession {
|
|
|
20
20
|
cloudProvider?: string;
|
|
21
21
|
cloudTaskId?: string;
|
|
22
22
|
cloudStatus?: string;
|
|
23
|
+
/**
|
|
24
|
+
* IDE window that owns this terminal. Source of truth is the per-window
|
|
25
|
+
* slice key in `live-terminals.json` (computeWindowId in the swarmify
|
|
26
|
+
* extension): `${vscode.env.sessionId}-${extension-host pid}`. Lets the
|
|
27
|
+
* renderer cluster terminals that belong to the same IDE window even when
|
|
28
|
+
* two windows have the same cwd open. Only populated for `terminal` context.
|
|
29
|
+
*/
|
|
30
|
+
windowId?: string;
|
|
23
31
|
}
|
|
24
32
|
export interface ActiveQueryOptions {
|
|
25
33
|
/** Skip the `ps` scan for ad-hoc headless agents. */
|
|
@@ -73,11 +73,11 @@ function readLiveTerminals() {
|
|
|
73
73
|
if (!parsed || typeof parsed !== 'object')
|
|
74
74
|
return [];
|
|
75
75
|
const merged = new Map();
|
|
76
|
-
for (const slice of Object.
|
|
76
|
+
for (const [windowId, slice] of Object.entries(parsed)) {
|
|
77
77
|
for (const e of (slice?.entries ?? [])) {
|
|
78
78
|
if (!e?.sessionId || !isPidAlive(e.pid))
|
|
79
79
|
continue;
|
|
80
|
-
merged.set(e.sessionId, e);
|
|
80
|
+
merged.set(e.sessionId, { ...e, windowId });
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
return Array.from(merged.values());
|
|
@@ -259,6 +259,7 @@ export async function listTerminalsActive() {
|
|
|
259
259
|
sessionFile,
|
|
260
260
|
startedAtMs: t.startedAtMs,
|
|
261
261
|
status: classifyActivity(sessionFile),
|
|
262
|
+
windowId: t.windowId,
|
|
262
263
|
};
|
|
263
264
|
});
|
|
264
265
|
}
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -155,8 +155,6 @@ export declare function getRowCount(): {
|
|
|
155
155
|
sessions: number;
|
|
156
156
|
textRows: number;
|
|
157
157
|
};
|
|
158
|
-
/** Count sessions older than the given timestamp (for dry-run previews). */
|
|
159
|
-
export declare function countSessionsOlderThan(cutoffMs: number): number;
|
|
160
158
|
/**
|
|
161
159
|
* Rewrite file_path for all sessions whose path starts with oldPrefix, replacing
|
|
162
160
|
* it with newPrefix + the unchanged suffix. Also clears the matching scan_ledger
|
|
@@ -167,5 +165,3 @@ export declare function countSessionsOlderThan(cutoffMs: number): number;
|
|
|
167
165
|
* Returns the number of session rows updated.
|
|
168
166
|
*/
|
|
169
167
|
export declare function updateSessionFilePaths(oldPrefix: string, newPrefix: string): number;
|
|
170
|
-
/** Delete sessions older than the given timestamp. Returns the number of rows deleted. */
|
|
171
|
-
export declare function deleteSessionsOlderThan(cutoffMs: number): number;
|
package/dist/lib/session/db.js
CHANGED
|
@@ -742,13 +742,6 @@ export function getRowCount() {
|
|
|
742
742
|
const textRows = db.prepare(`SELECT COUNT(*) AS c FROM session_text`).get().c;
|
|
743
743
|
return { sessions, textRows };
|
|
744
744
|
}
|
|
745
|
-
/** Count sessions older than the given timestamp (for dry-run previews). */
|
|
746
|
-
export function countSessionsOlderThan(cutoffMs) {
|
|
747
|
-
const db = getDB();
|
|
748
|
-
const cutoffIso = new Date(cutoffMs).toISOString();
|
|
749
|
-
const row = db.prepare(`SELECT COUNT(*) AS n FROM sessions WHERE timestamp < ?`).get(cutoffIso);
|
|
750
|
-
return row.n;
|
|
751
|
-
}
|
|
752
745
|
/**
|
|
753
746
|
* Rewrite file_path for all sessions whose path starts with oldPrefix, replacing
|
|
754
747
|
* it with newPrefix + the unchanged suffix. Also clears the matching scan_ledger
|
|
@@ -775,22 +768,3 @@ export function updateSessionFilePaths(oldPrefix, newPrefix) {
|
|
|
775
768
|
txn();
|
|
776
769
|
return rows.length;
|
|
777
770
|
}
|
|
778
|
-
/** Delete sessions older than the given timestamp. Returns the number of rows deleted. */
|
|
779
|
-
export function deleteSessionsOlderThan(cutoffMs) {
|
|
780
|
-
const db = getDB();
|
|
781
|
-
const cutoffIso = new Date(cutoffMs).toISOString();
|
|
782
|
-
const rows = db.prepare(`SELECT id, file_path FROM sessions WHERE timestamp < ?`).all(cutoffIso);
|
|
783
|
-
if (rows.length === 0)
|
|
784
|
-
return 0;
|
|
785
|
-
const txn = db.transaction(() => {
|
|
786
|
-
for (const { id, file_path } of rows) {
|
|
787
|
-
db.prepare(`DELETE FROM session_text WHERE session_id = ?`).run(id);
|
|
788
|
-
db.prepare(`DELETE FROM sessions WHERE id = ?`).run(id);
|
|
789
|
-
if (file_path) {
|
|
790
|
-
db.prepare(`DELETE FROM scan_ledger WHERE file_path = ?`).run(canonicalLedgerKey(file_path));
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
});
|
|
794
|
-
txn();
|
|
795
|
-
return rows.length;
|
|
796
|
-
}
|
|
@@ -43,6 +43,7 @@ export declare function parseGemini(filePath: string): SessionEvent[];
|
|
|
43
43
|
* Messages have role (user/assistant) and metadata.
|
|
44
44
|
* Parts contain the actual content: text, tool, reasoning, patch, step-start/finish.
|
|
45
45
|
*/
|
|
46
|
+
export declare function parseGrok(filePath: string): SessionEvent[];
|
|
46
47
|
export declare function parseOpenCode(filePath: string): SessionEvent[];
|
|
47
48
|
/** Parse a Rush JSONL session file into normalized events. */
|
|
48
49
|
export declare function parseRush(filePath: string): SessionEvent[];
|
|
@@ -93,6 +93,9 @@ export function parseSession(filePath, agent) {
|
|
|
93
93
|
case 'opencode':
|
|
94
94
|
events = parseOpenCode(filePath);
|
|
95
95
|
break;
|
|
96
|
+
case 'grok':
|
|
97
|
+
events = parseGrok(filePath);
|
|
98
|
+
break;
|
|
96
99
|
case 'rush':
|
|
97
100
|
events = parseRush(filePath);
|
|
98
101
|
break;
|
|
@@ -118,6 +121,8 @@ export function detectAgent(filePath) {
|
|
|
118
121
|
return 'codex';
|
|
119
122
|
if (filePath.includes('/.gemini/') || filePath.includes('\\.gemini\\'))
|
|
120
123
|
return 'gemini';
|
|
124
|
+
if (filePath.includes('/.grok/') || filePath.includes('\\.grok\\'))
|
|
125
|
+
return 'grok';
|
|
121
126
|
if (filePath.includes('/.rush/') || filePath.includes('\\.rush\\'))
|
|
122
127
|
return 'rush';
|
|
123
128
|
if (filePath.includes('/.hermes/') || filePath.includes('\\.hermes\\'))
|
|
@@ -645,6 +650,45 @@ function extractGeminiContent(content) {
|
|
|
645
650
|
* Messages have role (user/assistant) and metadata.
|
|
646
651
|
* Parts contain the actual content: text, tool, reasoning, patch, step-start/finish.
|
|
647
652
|
*/
|
|
653
|
+
export function parseGrok(filePath) {
|
|
654
|
+
// Grok sessions are rich (summary.json + events.jsonl + chat_history.jsonl + updates.jsonl)
|
|
655
|
+
// This is a minimal stub for now so grok appears in `agents sessions`.
|
|
656
|
+
// Full parser (with subagents, tool calls, etc.) can be expanded later.
|
|
657
|
+
try {
|
|
658
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
659
|
+
// If it's a summary.json, create a basic event
|
|
660
|
+
if (filePath.endsWith('summary.json')) {
|
|
661
|
+
const summary = JSON.parse(content);
|
|
662
|
+
return [{
|
|
663
|
+
timestamp: summary.created_at || new Date().toISOString(),
|
|
664
|
+
type: 'session_start',
|
|
665
|
+
content: summary.session_summary || 'Grok session',
|
|
666
|
+
agent: 'grok',
|
|
667
|
+
metadata: { sessionId: summary.id, cwd: summary.cwd },
|
|
668
|
+
}];
|
|
669
|
+
}
|
|
670
|
+
// For JSONL files (events, chat_history, updates), return basic parsed lines
|
|
671
|
+
if (filePath.endsWith('.jsonl')) {
|
|
672
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
673
|
+
return lines.slice(0, 50).map((line, i) => {
|
|
674
|
+
try {
|
|
675
|
+
const obj = JSON.parse(line);
|
|
676
|
+
return {
|
|
677
|
+
timestamp: obj.timestamp || obj.ts || new Date().toISOString(),
|
|
678
|
+
type: obj.type || obj.method || 'grok_event',
|
|
679
|
+
content: typeof obj.content === 'string' ? obj.content : JSON.stringify(obj).slice(0, 200),
|
|
680
|
+
agent: 'grok',
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
return { timestamp: new Date().toISOString(), type: 'raw', content: line.slice(0, 200), agent: 'grok' };
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
catch { }
|
|
690
|
+
return [];
|
|
691
|
+
}
|
|
648
692
|
export function parseOpenCode(filePath) {
|
|
649
693
|
const [dbPath, sessionId] = filePath.split('#');
|
|
650
694
|
if (!dbPath || !sessionId)
|
|
@@ -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');
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* speaks these types.
|
|
8
8
|
*/
|
|
9
9
|
/** Agents that store session data on disk and can be discovered by `agents sessions`. */
|
|
10
|
-
export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'openclaw' | 'rush' | 'hermes';
|
|
10
|
+
export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok';
|
|
11
11
|
/** All agents with session discovery support, in display order. */
|
|
12
12
|
export declare const SESSION_AGENTS: SessionAgentId[];
|
|
13
13
|
/** A single normalized event within a session (message, tool call, thinking, etc.). */
|
|
@@ -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. */
|
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
* speaks these types.
|
|
8
8
|
*/
|
|
9
9
|
/** All agents with session discovery support, in display order. */
|
|
10
|
-
export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes'];
|
|
10
|
+
export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes', 'grok'];
|
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.
|
|
@@ -213,7 +216,7 @@ export declare function getPathShadowingExecutable(agent: AgentId): string | nul
|
|
|
213
216
|
* Delete the legacy ~/.agents/shims/<cli> file if it exists, returning whether
|
|
214
217
|
* anything was removed. Pre-split installs put shims under ~/.agents/shims/;
|
|
215
218
|
* the new layout uses ~/.agents-system/shims/. The leftover file causes the
|
|
216
|
-
* repair-prompt loop reported in
|
|
219
|
+
* repair-prompt loop reported in PROJ-789 — `getPathShadowingExecutable` flags
|
|
217
220
|
* it as a shadow but `addShimsToPath` only edits rc files, never the file
|
|
218
221
|
* itself. Removing it ends the loop.
|
|
219
222
|
*/
|
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) {
|
|
@@ -226,19 +229,22 @@ fi
|
|
|
226
229
|
# written by agents-cli actually take effect.
|
|
227
230
|
export CODEX_HOME="$VERSION_DIR/home/${configDirName}"
|
|
228
231
|
`
|
|
229
|
-
: ''
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
232
|
+
: agent === 'copilot'
|
|
233
|
+
? `
|
|
234
|
+
# GitHub Copilot CLI honors COPILOT_HOME to relocate its config and state
|
|
235
|
+
# (settings.json, mcp-config.json, session-state/, logs/, plugins/). Point
|
|
236
|
+
# it at the versioned home so MCP servers, custom agents, and session
|
|
237
|
+
# history are isolated per copilot version.
|
|
238
|
+
export COPILOT_HOME="$VERSION_DIR/home/${configDirName}"
|
|
239
|
+
`
|
|
240
|
+
: agent === 'grok'
|
|
241
|
+
? `
|
|
242
|
+
# Grok Build uses GROK_HOME to isolate its entire configuration tree
|
|
243
|
+
# (skills, hooks, plugins, agents, memory, sessions, config.toml, MCP, etc.).
|
|
244
|
+
# This gives agents-cli full versioned isolation + resource sync for grok.
|
|
245
|
+
export GROK_HOME="$VERSION_DIR/home/.grok"
|
|
240
246
|
`
|
|
241
|
-
|
|
247
|
+
: '';
|
|
242
248
|
const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
|
|
243
249
|
return `#!/bin/bash
|
|
244
250
|
# Auto-generated by agents-cli - do not edit
|
|
@@ -292,22 +298,6 @@ resolve_default_version() {
|
|
|
292
298
|
fi
|
|
293
299
|
}
|
|
294
300
|
|
|
295
|
-
# Find project-scoped .agents directory (stop at agents.yaml or .git)
|
|
296
|
-
find_project_agents_dir() {
|
|
297
|
-
local dir="$PWD"
|
|
298
|
-
while [ "$dir" != "/" ]; do
|
|
299
|
-
if [ -d "$dir/.agents" ]; then
|
|
300
|
-
echo "$dir/.agents"
|
|
301
|
-
return 0
|
|
302
|
-
fi
|
|
303
|
-
if [ -f "$dir/agents.yaml" ] || [ -d "$dir/.git" ] || [ -f "$dir/.git" ]; then
|
|
304
|
-
break
|
|
305
|
-
fi
|
|
306
|
-
dir=$(dirname "$dir")
|
|
307
|
-
done
|
|
308
|
-
return 1
|
|
309
|
-
}
|
|
310
|
-
|
|
311
301
|
# Find the latest installed version by numeric component comparison.
|
|
312
302
|
# Handles both semver (2.1.138) and date-based (2026.5.7) version strings.
|
|
313
303
|
find_latest_installed() {
|
|
@@ -376,7 +366,27 @@ if [[ ! "$VERSION" =~ ^(latest|[A-Za-z0-9._+-]{1,64})$ || "$VERSION" == *..* ]];
|
|
|
376
366
|
fi
|
|
377
367
|
|
|
378
368
|
VERSION_DIR="$AGENTS_USER_DIR/.history/versions/$AGENT/$VERSION"
|
|
379
|
-
|
|
369
|
+
|
|
370
|
+
# Grok special case: binary lives in ~/.grok/downloads/, not node_modules.
|
|
371
|
+
# We still use the agents-cli version dir purely for GROK_HOME isolation.
|
|
372
|
+
if [ "$AGENT" = "grok" ]; then
|
|
373
|
+
# Try to find a matching binary for the pinned version in the global grok downloads dir.
|
|
374
|
+
GROK_DOWNLOADS="$HOME/.grok/downloads"
|
|
375
|
+
if [ -d "$GROK_DOWNLOADS" ]; then
|
|
376
|
+
# Prefer a binary whose filename contains the exact version
|
|
377
|
+
BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | grep -i "$VERSION" | head -1)
|
|
378
|
+
if [ -z "$BINARY" ]; then
|
|
379
|
+
# Fallback to the "current" grok binary (symlink or latest)
|
|
380
|
+
BINARY=$(ls "$GROK_DOWNLOADS"/grok-* 2>/dev/null | head -1)
|
|
381
|
+
fi
|
|
382
|
+
fi
|
|
383
|
+
if [ -z "$BINARY" ] || [ ! -x "$BINARY" ]; then
|
|
384
|
+
# Last resort: whatever is on PATH (user may have installed grok globally)
|
|
385
|
+
BINARY=$(command -v grok 2>/dev/null || echo "")
|
|
386
|
+
fi
|
|
387
|
+
else
|
|
388
|
+
BINARY="$VERSION_DIR/node_modules/.bin/$CLI_COMMAND"
|
|
389
|
+
fi
|
|
380
390
|
|
|
381
391
|
# Auto-install if not present
|
|
382
392
|
if [ ! -x "$BINARY" ]; then
|
|
@@ -439,12 +449,7 @@ if [ ! -x "$BINARY" ]; then
|
|
|
439
449
|
fi
|
|
440
450
|
fi
|
|
441
451
|
|
|
442
|
-
|
|
443
|
-
PROJECT_AGENTS_DIR=$(find_project_agents_dir)
|
|
444
|
-
if [ -n "$PROJECT_AGENTS_DIR" ]; then
|
|
445
|
-
"$AGENTS_BIN" sync --agent "$AGENT" --agent-version "$VERSION" --project-dir "$PROJECT_AGENTS_DIR" --quiet >/dev/null 2>&1
|
|
446
|
-
fi
|
|
447
|
-
${refreshRulesCall}${managedEnv}
|
|
452
|
+
${managedEnv}
|
|
448
453
|
|
|
449
454
|
exec "$BINARY"${launchArgs} "$@"
|
|
450
455
|
`;
|
|
@@ -528,7 +533,14 @@ export CLAUDE_CONFIG_DIR="$HOME/.agents/.history/versions/${agent}/${version}/ho
|
|
|
528
533
|
# and rules written by agents-cli actually take effect.
|
|
529
534
|
export CODEX_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
|
|
530
535
|
`
|
|
531
|
-
: ''
|
|
536
|
+
: agent === 'copilot'
|
|
537
|
+
? `
|
|
538
|
+
# Copilot honors COPILOT_HOME to relocate ~/.copilot (settings, mcp-config.json,
|
|
539
|
+
# session-state, logs). Point direct aliases at the versioned home so per-
|
|
540
|
+
# version MCP and session state are isolated.
|
|
541
|
+
export COPILOT_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
|
|
542
|
+
`
|
|
543
|
+
: '';
|
|
532
544
|
const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
|
|
533
545
|
return `#!/bin/bash
|
|
534
546
|
# Auto-generated by agents-cli - do not edit
|
|
@@ -719,6 +731,26 @@ export async function switchConfigSymlink(agent, version) {
|
|
|
719
731
|
// Already pointing to correct target, no-op
|
|
720
732
|
return { success: true };
|
|
721
733
|
}
|
|
734
|
+
// openclaw mixes user data (openclaw.json, openclaw.db, per-agent
|
|
735
|
+
// workspaces under ~/.openclaw/{agentId}/, memory/) with the version
|
|
736
|
+
// home — silently swapping the symlink to a fresh version home strips
|
|
737
|
+
// every running agent's config + workspace + memory. Carry the user
|
|
738
|
+
// data forward into the new version home before flipping the symlink
|
|
739
|
+
// (keep-dest preserves anything the new version already shipped).
|
|
740
|
+
// Other agents (Claude, Codex, etc.) keep user data outside the
|
|
741
|
+
// version-home dir, so this is openclaw-only by design.
|
|
742
|
+
if (agent === 'openclaw') {
|
|
743
|
+
try {
|
|
744
|
+
if (fs.existsSync(resolvedCurrent) && fs.statSync(resolvedCurrent).isDirectory()) {
|
|
745
|
+
await copyDirContents(resolvedCurrent, versionConfigPath, 'keep-dest');
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
catch (migrationErr) {
|
|
749
|
+
console.error(`Warning: openclaw data migration from ${resolvedCurrent} -> ${versionConfigPath} ` +
|
|
750
|
+
`failed: ${migrationErr.message}. The previous version's data is intact ` +
|
|
751
|
+
`at the old path; you can copy it manually if needed.`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
722
754
|
// Different target - update it
|
|
723
755
|
fs.unlinkSync(configPath);
|
|
724
756
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
@@ -1182,7 +1214,7 @@ export function getPathShadowingExecutable(agent) {
|
|
|
1182
1214
|
* Delete the legacy ~/.agents/shims/<cli> file if it exists, returning whether
|
|
1183
1215
|
* anything was removed. Pre-split installs put shims under ~/.agents/shims/;
|
|
1184
1216
|
* the new layout uses ~/.agents-system/shims/. The leftover file causes the
|
|
1185
|
-
* repair-prompt loop reported in
|
|
1217
|
+
* repair-prompt loop reported in PROJ-789 — `getPathShadowingExecutable` flags
|
|
1186
1218
|
* it as a shadow but `addShimsToPath` only edits rc files, never the file
|
|
1187
1219
|
* itself. Removing it ends the loop.
|
|
1188
1220
|
*/
|