@phnx-labs/agents-cli 1.20.72 → 1.20.74
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 +395 -0
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +12 -0
- package/dist/commands/activity.js +68 -0
- package/dist/commands/apply.d.ts +13 -0
- package/dist/commands/apply.js +28 -3
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cloud.js +21 -11
- package/dist/commands/doctor.js +21 -5
- package/dist/commands/events.d.ts +11 -10
- package/dist/commands/events.js +30 -21
- package/dist/commands/exec.d.ts +34 -0
- package/dist/commands/exec.js +337 -39
- package/dist/commands/export.d.ts +11 -0
- package/dist/commands/export.js +215 -0
- package/dist/commands/feed.d.ts +13 -0
- package/dist/commands/feed.js +130 -29
- package/dist/commands/hq.d.ts +2 -0
- package/dist/commands/hq.js +58 -0
- package/dist/commands/import.js +51 -3
- package/dist/commands/lease.d.ts +19 -0
- package/dist/commands/lease.js +138 -11
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +123 -0
- package/dist/commands/logs.js +1 -1
- package/dist/commands/mine.d.ts +27 -0
- package/dist/commands/mine.js +207 -0
- package/dist/commands/monitors.js +79 -49
- package/dist/commands/plugins.js +1 -0
- package/dist/commands/profiles.js +158 -0
- package/dist/commands/repo.js +2 -60
- package/dist/commands/resources.d.ts +5 -0
- package/dist/commands/resources.js +95 -0
- package/dist/commands/routines.d.ts +2 -0
- package/dist/commands/routines.js +139 -37
- package/dist/commands/secrets.js +234 -47
- package/dist/commands/send.d.ts +15 -0
- package/dist/commands/send.js +62 -0
- package/dist/commands/sessions-tail.d.ts +1 -0
- package/dist/commands/sessions-tail.js +20 -9
- package/dist/commands/sessions.js +18 -3
- package/dist/commands/setup-mine.d.ts +18 -0
- package/dist/commands/setup-mine.js +106 -0
- package/dist/commands/setup-share.js +2 -2
- package/dist/commands/setup.js +9 -2
- package/dist/commands/share.d.ts +8 -1
- package/dist/commands/share.js +122 -37
- package/dist/commands/ssh.d.ts +9 -0
- package/dist/commands/ssh.js +196 -26
- package/dist/commands/sync.js +1 -1
- package/dist/commands/teams.js +11 -2
- package/dist/commands/versions.js +36 -5
- package/dist/commands/view.js +75 -27
- package/dist/index.js +80 -10
- package/dist/lib/activity.d.ts +124 -0
- package/dist/lib/activity.js +542 -0
- package/dist/lib/agents.d.ts +35 -0
- package/dist/lib/agents.js +79 -6
- package/dist/lib/artifact-actions.d.ts +1 -1
- package/dist/lib/artifact-actions.js +1 -1
- package/dist/lib/ask-classifier.d.ts +2 -1
- package/dist/lib/ask-classifier.js +3 -3
- package/dist/lib/auth-health.d.ts +32 -4
- package/dist/lib/auth-health.js +40 -1
- package/dist/lib/auto-pull.js +8 -1
- package/dist/lib/brand.d.ts +40 -0
- package/dist/lib/brand.js +122 -0
- package/dist/lib/browser/drivers/ssh.js +4 -7
- package/dist/lib/browser/service.js +6 -8
- package/dist/lib/channels/providers/index.d.ts +2 -0
- package/dist/lib/channels/providers/index.js +20 -0
- package/dist/lib/channels/providers/mailbox.d.ts +2 -0
- package/dist/lib/channels/providers/mailbox.js +26 -0
- package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
- package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
- package/dist/lib/channels/providers/rush.d.ts +6 -0
- package/dist/lib/channels/providers/rush.js +55 -0
- package/dist/lib/channels/registry.d.ts +42 -0
- package/dist/lib/channels/registry.js +20 -0
- package/dist/lib/channels/resolve.d.ts +11 -0
- package/dist/lib/channels/resolve.js +12 -0
- package/dist/lib/cli-resources.d.ts +18 -0
- package/dist/lib/cli-resources.js +53 -4
- package/dist/lib/cloud/codex.js +6 -3
- package/dist/lib/cloud/factory.d.ts +1 -0
- package/dist/lib/cloud/factory.js +10 -4
- package/dist/lib/cloud/rush.d.ts +3 -1
- package/dist/lib/cloud/rush.js +6 -1
- package/dist/lib/cloud/types.d.ts +2 -0
- package/dist/lib/codex-home.d.ts +35 -0
- package/dist/lib/codex-home.js +136 -0
- package/dist/lib/config-transfer.d.ts +16 -0
- package/dist/lib/config-transfer.js +56 -0
- package/dist/lib/crabbox/cli.d.ts +44 -0
- package/dist/lib/crabbox/cli.js +120 -20
- package/dist/lib/crabbox/lease.d.ts +52 -1
- package/dist/lib/crabbox/lease.js +117 -16
- package/dist/lib/crabbox/progress.d.ts +31 -0
- package/dist/lib/crabbox/progress.js +76 -0
- package/dist/lib/crabbox/runtimes.d.ts +4 -0
- package/dist/lib/crabbox/runtimes.js +33 -8
- package/dist/lib/crabbox/setup-copy.d.ts +87 -0
- package/dist/lib/crabbox/setup-copy.js +127 -0
- package/dist/lib/daemon.d.ts +5 -5
- package/dist/lib/daemon.js +9 -9
- package/dist/lib/drive-sync.js +6 -3
- package/dist/lib/event-stream.d.ts +36 -0
- package/dist/lib/event-stream.js +68 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +11 -0
- package/dist/lib/exec.js +23 -2
- package/dist/lib/export.d.ts +72 -0
- package/dist/lib/export.js +269 -0
- package/dist/lib/feed-policy.d.ts +1 -1
- package/dist/lib/feed-policy.js +14 -9
- package/dist/lib/feed-ranking.d.ts +32 -0
- package/dist/lib/feed-ranking.js +224 -0
- package/dist/lib/feed.d.ts +35 -2
- package/dist/lib/feed.js +66 -3
- package/dist/lib/fleet/auth-sync.d.ts +53 -0
- package/dist/lib/fleet/auth-sync.js +92 -0
- package/dist/lib/fleet/remote-login.d.ts +187 -0
- package/dist/lib/fleet/remote-login.js +556 -0
- package/dist/lib/format.d.ts +30 -3
- package/dist/lib/format.js +34 -5
- package/dist/lib/fs-atomic.d.ts +7 -2
- package/dist/lib/fs-atomic.js +8 -12
- package/dist/lib/git.d.ts +16 -0
- package/dist/lib/git.js +79 -2
- package/dist/lib/heal.js +11 -3
- package/dist/lib/hosts/progress.d.ts +28 -10
- package/dist/lib/hosts/progress.js +79 -22
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/hq/floor.d.ts +87 -0
- package/dist/lib/hq/floor.js +226 -0
- package/dist/lib/import.d.ts +23 -0
- package/dist/lib/import.js +74 -2
- package/dist/lib/isolation-boundary-report.d.ts +10 -0
- package/dist/lib/isolation-boundary-report.js +35 -0
- package/dist/lib/menubar/install-menubar.js +14 -20
- package/dist/lib/notify.d.ts +1 -0
- package/dist/lib/notify.js +3 -3
- package/dist/lib/open-url.d.ts +2 -0
- package/dist/lib/open-url.js +19 -0
- package/dist/lib/openclaw-keychain.d.ts +56 -0
- package/dist/lib/openclaw-keychain.js +236 -0
- package/dist/lib/overdue.js +10 -0
- package/dist/lib/permissions.d.ts +44 -1
- package/dist/lib/permissions.js +284 -7
- package/dist/lib/project-launch.d.ts +6 -12
- package/dist/lib/project-launch.js +13 -228
- package/dist/lib/project-resources.d.ts +7 -0
- package/dist/lib/project-resources.js +306 -0
- package/dist/lib/pty-client.d.ts +27 -0
- package/dist/lib/pty-client.js +136 -10
- package/dist/lib/refresh.js +14 -10
- package/dist/lib/resource-profiles.d.ts +26 -0
- package/dist/lib/resource-profiles.js +157 -0
- package/dist/lib/resources/permissions.js +7 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/resources.js +32 -3
- package/dist/lib/routines.d.ts +11 -0
- package/dist/lib/routines.js +56 -15
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +66 -5
- package/dist/lib/secrets/bundles.d.ts +16 -3
- package/dist/lib/secrets/bundles.js +206 -37
- package/dist/lib/secrets/icloud-import.d.ts +2 -2
- package/dist/lib/secrets/icloud-import.js +9 -6
- package/dist/lib/secrets/mcp.js +1 -1
- package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
- package/dist/lib/secrets/vault-age-helper.js +34 -0
- package/dist/lib/secrets/vault.d.ts +49 -0
- package/dist/lib/secrets/vault.js +397 -0
- package/dist/lib/self-heal/checks/path.js +3 -1
- package/dist/lib/self-heal/checks/shadowing.js +10 -2
- package/dist/lib/self-heal/checks/shims.js +19 -11
- package/dist/lib/session/cloud.d.ts +2 -2
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +44 -7
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +114 -3
- package/dist/lib/session/stream-render.d.ts +3 -0
- package/dist/lib/session/stream-render.js +130 -0
- package/dist/lib/session/types.d.ts +6 -0
- package/dist/lib/share/analytics.d.ts +13 -0
- package/dist/lib/share/analytics.js +45 -0
- package/dist/lib/share/config.d.ts +19 -5
- package/dist/lib/share/config.js +44 -8
- package/dist/lib/share/provision.d.ts +58 -6
- package/dist/lib/share/provision.js +97 -22
- package/dist/lib/share/publish.d.ts +36 -13
- package/dist/lib/share/publish.js +55 -8
- package/dist/lib/share/worker-template.js +83 -17
- package/dist/lib/shims.d.ts +35 -0
- package/dist/lib/shims.js +159 -13
- package/dist/lib/ssh-exec.d.ts +14 -0
- package/dist/lib/ssh-exec.js +57 -0
- package/dist/lib/staleness/detectors/hooks.js +25 -1
- package/dist/lib/staleness/detectors/permissions.js +66 -0
- package/dist/lib/staleness/detectors/workflows.js +20 -0
- package/dist/lib/staleness/writers/hooks.js +58 -4
- package/dist/lib/staleness/writers/permissions.js +2 -2
- package/dist/lib/staleness/writers/skills.js +1 -1
- package/dist/lib/staleness/writers/sources.d.ts +10 -1
- package/dist/lib/staleness/writers/sources.js +68 -1
- package/dist/lib/star-nudge.d.ts +45 -0
- package/dist/lib/star-nudge.js +91 -0
- package/dist/lib/startup/command-registry.d.ts +7 -1
- package/dist/lib/startup/command-registry.js +19 -3
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +17 -4
- package/dist/lib/subagents-registry.d.ts +2 -0
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/teams/parsers.js +214 -6
- package/dist/lib/teams/supervisor.d.ts +7 -0
- package/dist/lib/teams/supervisor.js +2 -1
- package/dist/lib/template.d.ts +1 -1
- package/dist/lib/template.js +1 -1
- package/dist/lib/triggers/webhook.js +36 -3
- package/dist/lib/types.d.ts +84 -0
- package/dist/lib/uninstall.js +1 -45
- package/dist/lib/version.d.ts +40 -0
- package/dist/lib/version.js +94 -16
- package/dist/lib/versions.d.ts +24 -0
- package/dist/lib/versions.js +269 -78
- package/dist/lib/workflows.d.ts +2 -0
- package/dist/lib/workflows.js +88 -0
- package/package.json +2 -1
- package/dist/commands/daemon.d.ts +0 -10
- package/dist/commands/daemon.js +0 -121
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
import { armor } from 'age-encryption';
|
|
5
|
+
import { getUserAgentsDir } from '../state.js';
|
|
6
|
+
import { getCliLaunch } from '../cli-entry.js';
|
|
7
|
+
import { atomicWriteFileSync, withFileLock } from '../fs-atomic.js';
|
|
8
|
+
import { deleteKeychainToken, getKeychainToken, setKeychainToken, } from './index.js';
|
|
9
|
+
const VAULT_FILE_NAME = 'vault.age';
|
|
10
|
+
const VAULT_SESSION_ITEM = 'agents-cli.vault.session';
|
|
11
|
+
export const VAULT_SESSION_TTL_MS = 8 * 60 * 60 * 1000;
|
|
12
|
+
export const VAULT_SCRYPT_WORK_FACTOR = 18;
|
|
13
|
+
const VAULT_LOCK_STALE_MS = 120_000;
|
|
14
|
+
const VAULT_LOCK_ACQUIRE_TIMEOUT_MS = 180_000;
|
|
15
|
+
let overrideVaultPath = null;
|
|
16
|
+
let vaultDataCache = null;
|
|
17
|
+
let ageOperationCountForTest = 0;
|
|
18
|
+
let ageHelperLaunchForTest = null;
|
|
19
|
+
export function vaultPath() {
|
|
20
|
+
return overrideVaultPath ?? process.env.AGENTS_VAULT_PATH ?? path.join(getUserAgentsDir(), VAULT_FILE_NAME);
|
|
21
|
+
}
|
|
22
|
+
export function vaultExists() {
|
|
23
|
+
return fs.existsSync(vaultPath());
|
|
24
|
+
}
|
|
25
|
+
export function _setVaultPathForTest(filePath) {
|
|
26
|
+
overrideVaultPath = filePath;
|
|
27
|
+
clearVaultDataCache();
|
|
28
|
+
}
|
|
29
|
+
export function _clearVaultDataCacheForTest() {
|
|
30
|
+
clearVaultDataCache();
|
|
31
|
+
}
|
|
32
|
+
export function _resetVaultAgeOperationCountForTest() {
|
|
33
|
+
ageOperationCountForTest = 0;
|
|
34
|
+
}
|
|
35
|
+
export function _getVaultAgeOperationCountForTest() {
|
|
36
|
+
return ageOperationCountForTest;
|
|
37
|
+
}
|
|
38
|
+
export function _setVaultAgeHelperLaunchForTest(launch) {
|
|
39
|
+
ageHelperLaunchForTest = launch;
|
|
40
|
+
}
|
|
41
|
+
function emptyVault() {
|
|
42
|
+
return { v: 1, bundles: {} };
|
|
43
|
+
}
|
|
44
|
+
function parseVault(raw) {
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(raw);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new Error('Vault content is malformed.');
|
|
51
|
+
}
|
|
52
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
53
|
+
throw new Error('Vault content has an invalid shape.');
|
|
54
|
+
}
|
|
55
|
+
const obj = parsed;
|
|
56
|
+
if (obj.v !== 1 || !obj.bundles || typeof obj.bundles !== 'object' || Array.isArray(obj.bundles)) {
|
|
57
|
+
throw new Error('Vault content has an invalid shape.');
|
|
58
|
+
}
|
|
59
|
+
const bundles = {};
|
|
60
|
+
for (const [name, record] of Object.entries(obj.bundles)) {
|
|
61
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
62
|
+
throw new Error(`Vault bundle '${name}' has an invalid shape.`);
|
|
63
|
+
}
|
|
64
|
+
const rec = record;
|
|
65
|
+
if (rec.metadata !== undefined && typeof rec.metadata !== 'string') {
|
|
66
|
+
throw new Error(`Vault bundle '${name}' metadata is invalid.`);
|
|
67
|
+
}
|
|
68
|
+
if (!rec.keys || typeof rec.keys !== 'object' || Array.isArray(rec.keys)) {
|
|
69
|
+
throw new Error(`Vault bundle '${name}' keys are invalid.`);
|
|
70
|
+
}
|
|
71
|
+
const keys = {};
|
|
72
|
+
for (const [key, value] of Object.entries(rec.keys)) {
|
|
73
|
+
if (typeof value !== 'string')
|
|
74
|
+
throw new Error(`Vault key '${name}.${key}' is invalid.`);
|
|
75
|
+
keys[key] = value;
|
|
76
|
+
}
|
|
77
|
+
bundles[name] = { metadata: rec.metadata, keys };
|
|
78
|
+
}
|
|
79
|
+
return { v: 1, bundles };
|
|
80
|
+
}
|
|
81
|
+
function runAgeSync(input) {
|
|
82
|
+
ageOperationCountForTest++;
|
|
83
|
+
const launch = ageHelperLaunchForTest ?? getCliLaunch(['__vault-age-helper']);
|
|
84
|
+
const child = spawnSync(launch.command, launch.args, {
|
|
85
|
+
input: JSON.stringify(input),
|
|
86
|
+
encoding: 'utf-8',
|
|
87
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
88
|
+
});
|
|
89
|
+
if (child.status !== 0) {
|
|
90
|
+
const msg = child.stderr.trim();
|
|
91
|
+
throw new Error(msg || child.error?.message || 'age operation failed');
|
|
92
|
+
}
|
|
93
|
+
return child.stdout;
|
|
94
|
+
}
|
|
95
|
+
export function encrypt(key, json) {
|
|
96
|
+
return runAgeSync({
|
|
97
|
+
action: 'encrypt',
|
|
98
|
+
passphrase: key.passphrase,
|
|
99
|
+
plaintext: JSON.stringify(json),
|
|
100
|
+
scryptWorkFactor: VAULT_SCRYPT_WORK_FACTOR,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export function decrypt(key, blob) {
|
|
104
|
+
try {
|
|
105
|
+
// Validate armor in-process so malformed files fail before spawning.
|
|
106
|
+
armor.decode(blob);
|
|
107
|
+
const plaintext = runAgeSync({
|
|
108
|
+
action: 'decrypt',
|
|
109
|
+
passphrase: key.passphrase,
|
|
110
|
+
blob,
|
|
111
|
+
});
|
|
112
|
+
return parseVault(plaintext);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
throw new Error(`Failed to decrypt vault. Wrong password or tampered vault file. (${err.message})`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function vaultFileStat(file) {
|
|
119
|
+
const stat = fs.statSync(file);
|
|
120
|
+
return { mtimeMs: stat.mtimeMs, size: stat.size };
|
|
121
|
+
}
|
|
122
|
+
function sameVaultFileStat(a, b) {
|
|
123
|
+
return a.mtimeMs === b.mtimeMs && a.size === b.size;
|
|
124
|
+
}
|
|
125
|
+
function clearVaultDataCache() {
|
|
126
|
+
vaultDataCache = null;
|
|
127
|
+
}
|
|
128
|
+
function cacheVaultData(file, key, data) {
|
|
129
|
+
vaultDataCache = {
|
|
130
|
+
file,
|
|
131
|
+
passphrase: key.passphrase,
|
|
132
|
+
stat: vaultFileStat(file),
|
|
133
|
+
data,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function vaultAlreadyExistsError(file) {
|
|
137
|
+
return new Error(`Vault file already exists: ${file}. Use --force to replace it.`);
|
|
138
|
+
}
|
|
139
|
+
export function createVault(password, opts = {}) {
|
|
140
|
+
if (!password)
|
|
141
|
+
throw new Error('Password is required.');
|
|
142
|
+
const file = vaultPath();
|
|
143
|
+
if (!opts.overwrite && fs.existsSync(file))
|
|
144
|
+
throw vaultAlreadyExistsError(file);
|
|
145
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
146
|
+
const key = { passphrase: password };
|
|
147
|
+
const data = emptyVault();
|
|
148
|
+
const blob = encrypt(key, data);
|
|
149
|
+
try {
|
|
150
|
+
fs.writeFileSync(file, blob, { mode: 0o600, flag: opts.overwrite ? 'w' : 'wx' });
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
if (err.code === 'EEXIST')
|
|
154
|
+
throw vaultAlreadyExistsError(file);
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
cacheVaultData(file, key, data);
|
|
158
|
+
cacheVaultKey(key);
|
|
159
|
+
return key;
|
|
160
|
+
}
|
|
161
|
+
export function joinVault(password, sourcePath, opts = {}) {
|
|
162
|
+
if (!password)
|
|
163
|
+
throw new Error('Password is required.');
|
|
164
|
+
const source = path.resolve(sourcePath);
|
|
165
|
+
const dest = vaultPath();
|
|
166
|
+
if (!opts.overwrite && fs.existsSync(dest))
|
|
167
|
+
throw vaultAlreadyExistsError(dest);
|
|
168
|
+
const blob = fs.readFileSync(source, 'utf-8');
|
|
169
|
+
const key = { passphrase: password };
|
|
170
|
+
const data = decrypt(key, blob);
|
|
171
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
172
|
+
try {
|
|
173
|
+
fs.copyFileSync(source, dest, opts.overwrite ? 0 : fs.constants.COPYFILE_EXCL);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
if (err.code === 'EEXIST')
|
|
177
|
+
throw vaultAlreadyExistsError(dest);
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
fs.chmodSync(dest, 0o600);
|
|
181
|
+
cacheVaultData(dest, key, data);
|
|
182
|
+
cacheVaultKey(key);
|
|
183
|
+
return key;
|
|
184
|
+
}
|
|
185
|
+
export function unlock(password) {
|
|
186
|
+
if (!password)
|
|
187
|
+
throw new Error('Password is required.');
|
|
188
|
+
const file = vaultPath();
|
|
189
|
+
if (!fs.existsSync(file))
|
|
190
|
+
throw new Error(`Vault file not found: ${file}`);
|
|
191
|
+
const key = { passphrase: password };
|
|
192
|
+
const data = decrypt(key, fs.readFileSync(file, 'utf-8'));
|
|
193
|
+
cacheVaultData(file, key, data);
|
|
194
|
+
cacheVaultKey(key);
|
|
195
|
+
return key;
|
|
196
|
+
}
|
|
197
|
+
export function cacheVaultKey(key, ttlMs = VAULT_SESSION_TTL_MS) {
|
|
198
|
+
const session = {
|
|
199
|
+
v: 1,
|
|
200
|
+
passphrase: key.passphrase,
|
|
201
|
+
expiresAt: Date.now() + ttlMs,
|
|
202
|
+
};
|
|
203
|
+
setKeychainToken(VAULT_SESSION_ITEM, JSON.stringify(session), { noAcl: true });
|
|
204
|
+
}
|
|
205
|
+
export function clearVaultKey() {
|
|
206
|
+
clearVaultDataCache();
|
|
207
|
+
deleteKeychainToken(VAULT_SESSION_ITEM);
|
|
208
|
+
}
|
|
209
|
+
export function getVaultSession() {
|
|
210
|
+
let raw;
|
|
211
|
+
try {
|
|
212
|
+
raw = getKeychainToken(VAULT_SESSION_ITEM);
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return { loggedIn: false };
|
|
216
|
+
}
|
|
217
|
+
let parsed;
|
|
218
|
+
try {
|
|
219
|
+
parsed = JSON.parse(raw);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
clearVaultKey();
|
|
223
|
+
return { loggedIn: false };
|
|
224
|
+
}
|
|
225
|
+
if (parsed.v !== 1 || typeof parsed.passphrase !== 'string' || typeof parsed.expiresAt !== 'number') {
|
|
226
|
+
clearVaultKey();
|
|
227
|
+
return { loggedIn: false };
|
|
228
|
+
}
|
|
229
|
+
if (parsed.expiresAt <= Date.now()) {
|
|
230
|
+
clearVaultKey();
|
|
231
|
+
return { loggedIn: false, expiresAt: parsed.expiresAt };
|
|
232
|
+
}
|
|
233
|
+
return { loggedIn: true, key: { passphrase: parsed.passphrase }, expiresAt: parsed.expiresAt };
|
|
234
|
+
}
|
|
235
|
+
function requireVaultKey() {
|
|
236
|
+
const session = getVaultSession();
|
|
237
|
+
if (!session.loggedIn) {
|
|
238
|
+
throw new Error('Not logged in. Run: agents login');
|
|
239
|
+
}
|
|
240
|
+
return session.key;
|
|
241
|
+
}
|
|
242
|
+
function parseVaultItem(item) {
|
|
243
|
+
const metaPrefix = 'agents-cli.bundles.';
|
|
244
|
+
const keyPrefix = 'agents-cli.secrets.';
|
|
245
|
+
if (item.startsWith(metaPrefix)) {
|
|
246
|
+
return { kind: 'meta', bundle: item.slice(metaPrefix.length) };
|
|
247
|
+
}
|
|
248
|
+
if (item.startsWith(keyPrefix)) {
|
|
249
|
+
const rest = item.slice(keyPrefix.length);
|
|
250
|
+
const dot = rest.lastIndexOf('.');
|
|
251
|
+
if (dot <= 0 || dot >= rest.length - 1)
|
|
252
|
+
return null;
|
|
253
|
+
return { kind: 'key', bundle: rest.slice(0, dot), key: rest.slice(dot + 1) };
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
function vaultMissingError(file) {
|
|
258
|
+
return new Error(`Vault file is missing: ${file}. A vault was created but the file is gone` +
|
|
259
|
+
' (a sync conflict, an undownloaded placeholder, or an accidental move/delete).' +
|
|
260
|
+
' Restore it before writing, or run: agents login --create --force to start over.');
|
|
261
|
+
}
|
|
262
|
+
function readVaultData() {
|
|
263
|
+
const file = vaultPath();
|
|
264
|
+
if (!fs.existsSync(file)) {
|
|
265
|
+
// A live session means a vault was created/joined/unlocked, so the file
|
|
266
|
+
// must exist. If it is gone, refuse rather than silently starting from an
|
|
267
|
+
// empty vault (which a subsequent write would persist, destroying data).
|
|
268
|
+
if (getVaultSession().loggedIn)
|
|
269
|
+
throw vaultMissingError(file);
|
|
270
|
+
return emptyVault();
|
|
271
|
+
}
|
|
272
|
+
const key = requireVaultKey();
|
|
273
|
+
const stat = vaultFileStat(file);
|
|
274
|
+
if (vaultDataCache &&
|
|
275
|
+
vaultDataCache.file === file &&
|
|
276
|
+
vaultDataCache.passphrase === key.passphrase &&
|
|
277
|
+
sameVaultFileStat(vaultDataCache.stat, stat)) {
|
|
278
|
+
return vaultDataCache.data;
|
|
279
|
+
}
|
|
280
|
+
const data = decrypt(key, fs.readFileSync(file, 'utf-8'));
|
|
281
|
+
vaultDataCache = { file, passphrase: key.passphrase, stat, data };
|
|
282
|
+
return data;
|
|
283
|
+
}
|
|
284
|
+
function writeVaultData(data) {
|
|
285
|
+
const file = vaultPath();
|
|
286
|
+
const key = requireVaultKey();
|
|
287
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
288
|
+
atomicWriteFileSync(file, encrypt(key, data), { encoding: 'utf-8', mode: 0o600 });
|
|
289
|
+
cacheVaultData(file, key, data);
|
|
290
|
+
}
|
|
291
|
+
function withVaultMutation(mutate) {
|
|
292
|
+
const file = vaultPath();
|
|
293
|
+
requireVaultKey();
|
|
294
|
+
if (!fs.existsSync(file))
|
|
295
|
+
throw vaultMissingError(file);
|
|
296
|
+
return withFileLock(file, () => {
|
|
297
|
+
const data = readVaultData();
|
|
298
|
+
const { changed, result } = mutate(data);
|
|
299
|
+
if (changed)
|
|
300
|
+
writeVaultData(data);
|
|
301
|
+
return result;
|
|
302
|
+
}, { staleMs: VAULT_LOCK_STALE_MS, acquireTimeoutMs: VAULT_LOCK_ACQUIRE_TIMEOUT_MS });
|
|
303
|
+
}
|
|
304
|
+
export function vaultHasItem(item) {
|
|
305
|
+
const parsed = parseVaultItem(item);
|
|
306
|
+
if (!parsed)
|
|
307
|
+
return false;
|
|
308
|
+
const data = readVaultData();
|
|
309
|
+
const bundle = data.bundles[parsed.bundle];
|
|
310
|
+
if (!bundle)
|
|
311
|
+
return false;
|
|
312
|
+
return parsed.kind === 'meta' ? bundle.metadata !== undefined : parsed.key in bundle.keys;
|
|
313
|
+
}
|
|
314
|
+
export function vaultGetItem(item) {
|
|
315
|
+
const parsed = parseVaultItem(item);
|
|
316
|
+
if (!parsed)
|
|
317
|
+
throw new Error(`Vault item '${item}' is invalid.`);
|
|
318
|
+
const data = readVaultData();
|
|
319
|
+
const bundle = data.bundles[parsed.bundle];
|
|
320
|
+
const value = parsed.kind === 'meta' ? bundle?.metadata : bundle?.keys[parsed.key];
|
|
321
|
+
if (value === undefined)
|
|
322
|
+
throw new Error(`Vault item '${item}' not found.`);
|
|
323
|
+
return value;
|
|
324
|
+
}
|
|
325
|
+
export function vaultGetItems(items) {
|
|
326
|
+
const data = readVaultData();
|
|
327
|
+
const out = new Map();
|
|
328
|
+
for (const item of items) {
|
|
329
|
+
const parsed = parseVaultItem(item);
|
|
330
|
+
if (!parsed)
|
|
331
|
+
continue;
|
|
332
|
+
const bundle = data.bundles[parsed.bundle];
|
|
333
|
+
const value = parsed.kind === 'meta' ? bundle?.metadata : bundle?.keys[parsed.key];
|
|
334
|
+
if (value !== undefined)
|
|
335
|
+
out.set(item, value);
|
|
336
|
+
}
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
export function vaultSetItem(item, value) {
|
|
340
|
+
vaultSetItems(new Map([[item, value]]));
|
|
341
|
+
}
|
|
342
|
+
export function vaultSetItems(items) {
|
|
343
|
+
if (items.size === 0)
|
|
344
|
+
return;
|
|
345
|
+
withVaultMutation((data) => {
|
|
346
|
+
for (const [item, value] of items) {
|
|
347
|
+
const parsed = parseVaultItem(item);
|
|
348
|
+
if (!parsed)
|
|
349
|
+
throw new Error(`Vault item '${item}' is invalid.`);
|
|
350
|
+
const bundle = data.bundles[parsed.bundle] ?? { keys: {} };
|
|
351
|
+
if (parsed.kind === 'meta')
|
|
352
|
+
bundle.metadata = value;
|
|
353
|
+
else
|
|
354
|
+
bundle.keys[parsed.key] = value;
|
|
355
|
+
data.bundles[parsed.bundle] = bundle;
|
|
356
|
+
}
|
|
357
|
+
return { changed: true, result: undefined };
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
export function vaultDeleteItem(item) {
|
|
361
|
+
const parsed = parseVaultItem(item);
|
|
362
|
+
if (!parsed)
|
|
363
|
+
return false;
|
|
364
|
+
return withVaultMutation((data) => {
|
|
365
|
+
const bundle = data.bundles[parsed.bundle];
|
|
366
|
+
if (!bundle)
|
|
367
|
+
return { changed: false, result: false };
|
|
368
|
+
let deleted = false;
|
|
369
|
+
if (parsed.kind === 'meta') {
|
|
370
|
+
deleted = bundle.metadata !== undefined;
|
|
371
|
+
delete bundle.metadata;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
deleted = parsed.key in bundle.keys;
|
|
375
|
+
delete bundle.keys[parsed.key];
|
|
376
|
+
}
|
|
377
|
+
if (bundle.metadata === undefined && Object.keys(bundle.keys).length === 0) {
|
|
378
|
+
delete data.bundles[parsed.bundle];
|
|
379
|
+
}
|
|
380
|
+
return { changed: deleted, result: deleted };
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
export function vaultListItems(prefix) {
|
|
384
|
+
const data = readVaultData();
|
|
385
|
+
const out = [];
|
|
386
|
+
for (const [bundleName, bundle] of Object.entries(data.bundles)) {
|
|
387
|
+
const meta = `agents-cli.bundles.${bundleName}`;
|
|
388
|
+
if (bundle.metadata !== undefined && meta.startsWith(prefix))
|
|
389
|
+
out.push(meta);
|
|
390
|
+
for (const key of Object.keys(bundle.keys)) {
|
|
391
|
+
const item = `agents-cli.secrets.${bundleName}.${key}`;
|
|
392
|
+
if (item.startsWith(prefix))
|
|
393
|
+
out.push(item);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return out.sort();
|
|
397
|
+
}
|
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
// Caveat: an already-open shell won't pick up the new rc/PATH entry until it reloads
|
|
7
7
|
// — but new shells will, so the recurring prompt stops.
|
|
8
8
|
import { resultOf } from '../types.js';
|
|
9
|
-
import { isShimsInPath, addShimsToPath } from '../../shims.js';
|
|
9
|
+
import { isShimsInPath, addShimsToPath, listAgentsWithNonIsolatedInstalledVersions } from '../../shims.js';
|
|
10
10
|
export const pathCheck = {
|
|
11
11
|
id: 'path',
|
|
12
12
|
title: 'Shims directory on PATH',
|
|
13
13
|
cadence: 'startup',
|
|
14
14
|
async run(ctx) {
|
|
15
|
+
if (listAgentsWithNonIsolatedInstalledVersions().length === 0)
|
|
16
|
+
return resultOf([], []);
|
|
15
17
|
if (isShimsInPath())
|
|
16
18
|
return resultOf([], []);
|
|
17
19
|
if (ctx.dryRun)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// and PATH-order problem are POSIX; Windows resolves via the registry PATH).
|
|
6
6
|
import { resultOf } from '../types.js';
|
|
7
7
|
import { AGENTS } from '../../agents.js';
|
|
8
|
-
import { getPathShadowingExecutable, adoptShadowingLauncher,
|
|
8
|
+
import { getPathShadowingExecutable, adoptShadowingLauncher, listAgentsWithNonIsolatedInstalledVersions, } from '../../shims.js';
|
|
9
9
|
import { getGlobalDefault } from '../../versions.js';
|
|
10
10
|
export const shadowingCheck = {
|
|
11
11
|
id: 'shadowing',
|
|
@@ -15,7 +15,15 @@ export const shadowingCheck = {
|
|
|
15
15
|
async run(ctx) {
|
|
16
16
|
const fixed = [];
|
|
17
17
|
const needsAttention = [];
|
|
18
|
-
|
|
18
|
+
// Isolated-only agents are skipped outright. Adoption repoints the user's OWN
|
|
19
|
+
// launcher (e.g. the npm symlink ~/.npm-global/bin/codex) at our shim — the most
|
|
20
|
+
// invasive thing self-heal does, and the exact opposite of what `--isolated`
|
|
21
|
+
// promises. The `getGlobalDefault` guard below already blocked this in practice,
|
|
22
|
+
// since an isolated install never sets a default; but that made the boundary
|
|
23
|
+
// depend on an invariant enforced elsewhere. Any path that pins a default from
|
|
24
|
+
// an isolated copy would silently re-arm the adoption. Gate on the installs
|
|
25
|
+
// themselves so it cannot happen regardless of how a default got recorded.
|
|
26
|
+
for (const agent of listAgentsWithNonIsolatedInstalledVersions()) {
|
|
19
27
|
if (!getGlobalDefault(agent))
|
|
20
28
|
continue; // only default agents, like the interactive flow
|
|
21
29
|
const cmd = AGENTS[agent].cliCommand;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// background so the shim schema settles without user-facing churn.
|
|
5
5
|
import { resultOf } from '../types.js';
|
|
6
6
|
import { AGENTS } from '../../agents.js';
|
|
7
|
-
import { createShim, ensureShimCurrent, ensureVersionedAliasCurrent, isShimCurrent, isVersionedAliasCurrent, shimPointsAtLiveInstall, removeLegacyUserShim, listAgentsWithInstalledVersions, listShimFileNames, pruneOrphanedCommandShim, } from '../../shims.js';
|
|
7
|
+
import { createShim, ensureShimCurrent, ensureVersionedAliasCurrent, isShimCurrent, isVersionedAliasCurrent, removeShim, shimExists, shimPointsAtLiveInstall, removeLegacyUserShim, listAgentsWithInstalledVersions, listAgentsWithNonIsolatedInstalledVersions, listShimFileNames, pruneOrphanedCommandShim, } from '../../shims.js';
|
|
8
8
|
import { listInstalledVersions } from '../../versions.js';
|
|
9
9
|
export const shimsCheck = {
|
|
10
10
|
id: 'shims',
|
|
@@ -12,20 +12,28 @@ export const shimsCheck = {
|
|
|
12
12
|
cadence: 'frequent',
|
|
13
13
|
async run(ctx) {
|
|
14
14
|
const fixed = [];
|
|
15
|
+
const agentsWithBareShims = new Set(listAgentsWithNonIsolatedInstalledVersions());
|
|
15
16
|
for (const agent of listAgentsWithInstalledVersions()) {
|
|
16
17
|
const cmd = AGENTS[agent].cliCommand;
|
|
17
|
-
if (
|
|
18
|
-
if (!
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
if (agentsWithBareShims.has(agent)) {
|
|
19
|
+
if (!isShimCurrent(agent)) {
|
|
20
|
+
if (!ctx.dryRun)
|
|
21
|
+
ensureShimCurrent(agent);
|
|
22
|
+
fixed.push(`${cmd} shim`);
|
|
23
|
+
}
|
|
24
|
+
else if (!shimPointsAtLiveInstall(agent)) {
|
|
25
|
+
// Schema is current but the baked AGENTS_BIN points at a different, removed
|
|
26
|
+
// install (dev build, old npm-global, rotated version dir). ensureShimCurrent
|
|
27
|
+
// would no-op on a schema-current shim, so force a rewrite to the current install.
|
|
28
|
+
if (!ctx.dryRun)
|
|
29
|
+
createShim(agent);
|
|
30
|
+
fixed.push(`${cmd} shim (repointed to current install)`);
|
|
31
|
+
}
|
|
21
32
|
}
|
|
22
|
-
else if (
|
|
23
|
-
// Schema is current but the baked AGENTS_BIN points at a different, removed
|
|
24
|
-
// install (dev build, old npm-global, rotated version dir). ensureShimCurrent
|
|
25
|
-
// would no-op on a schema-current shim, so force a rewrite to the current install.
|
|
33
|
+
else if (shimExists(agent)) {
|
|
26
34
|
if (!ctx.dryRun)
|
|
27
|
-
|
|
28
|
-
fixed.push(
|
|
35
|
+
removeShim(agent);
|
|
36
|
+
fixed.push(`removed ${cmd} shim (isolated-only)`);
|
|
29
37
|
}
|
|
30
38
|
for (const version of listInstalledVersions(agent)) {
|
|
31
39
|
if (!isVersionedAliasCurrent(agent, version)) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rush Cloud session source.
|
|
3
3
|
*
|
|
4
|
-
* Fetches cloud-captured sessions from
|
|
4
|
+
* Fetches cloud-captured sessions from the cloud proxy (api.prix.dev) and caches
|
|
5
5
|
* them locally so the existing filesystem-based parse pipeline works unchanged.
|
|
6
6
|
*
|
|
7
|
-
* Endpoints consumed
|
|
7
|
+
* Endpoints consumed by the cloud proxy:
|
|
8
8
|
* GET /api/v1/cloud-runs → list executions
|
|
9
9
|
* GET /api/v1/cloud-runs/:id → get one (used for meta)
|
|
10
10
|
* GET /api/v1/cloud-runs/:id/session.jsonl → raw captured jsonl
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rush Cloud session source.
|
|
3
3
|
*
|
|
4
|
-
* Fetches cloud-captured sessions from
|
|
4
|
+
* Fetches cloud-captured sessions from the cloud proxy (api.prix.dev) and caches
|
|
5
5
|
* them locally so the existing filesystem-based parse pipeline works unchanged.
|
|
6
6
|
*
|
|
7
|
-
* Endpoints consumed
|
|
7
|
+
* Endpoints consumed by the cloud proxy:
|
|
8
8
|
* GET /api/v1/cloud-runs → list executions
|
|
9
9
|
* GET /api/v1/cloud-runs/:id → get one (used for meta)
|
|
10
10
|
* GET /api/v1/cloud-runs/:id/session.jsonl → raw captured jsonl
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ export interface SessionRow {
|
|
|
13
13
|
id: string;
|
|
14
14
|
short_id: string;
|
|
15
15
|
agent: string;
|
|
16
|
+
origin: string | null;
|
|
17
|
+
routine_name: string | null;
|
|
18
|
+
routine_run_id: string | null;
|
|
16
19
|
version: string | null;
|
|
17
20
|
account: string | null;
|
|
18
21
|
timestamp: string;
|
|
@@ -48,6 +51,7 @@ export interface ScanStamp {
|
|
|
48
51
|
export interface QueryOptions {
|
|
49
52
|
agent?: SessionAgentId;
|
|
50
53
|
agents?: SessionAgentId[];
|
|
54
|
+
origin?: 'cli' | 'routine';
|
|
51
55
|
version?: string;
|
|
52
56
|
cwd?: string;
|
|
53
57
|
/** Match any session whose cwd equals this or is a descendant of it. */
|
package/dist/lib/session/db.js
CHANGED
|
@@ -13,7 +13,7 @@ import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
|
13
13
|
const SESSIONS_DIR = getSessionsDir();
|
|
14
14
|
const DB_PATH = getSessionsDbPath();
|
|
15
15
|
/** Current schema version; bumped when migrations are added. */
|
|
16
|
-
const SCHEMA_VERSION =
|
|
16
|
+
const SCHEMA_VERSION = 13;
|
|
17
17
|
/**
|
|
18
18
|
* Canonicalize a file path for use as a scan_ledger key. The same physical
|
|
19
19
|
* session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
|
|
@@ -43,6 +43,9 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
43
43
|
id TEXT PRIMARY KEY,
|
|
44
44
|
short_id TEXT NOT NULL,
|
|
45
45
|
agent TEXT NOT NULL,
|
|
46
|
+
origin TEXT DEFAULT 'cli',
|
|
47
|
+
routine_name TEXT,
|
|
48
|
+
routine_run_id TEXT,
|
|
46
49
|
version TEXT,
|
|
47
50
|
account TEXT,
|
|
48
51
|
timestamp TEXT NOT NULL,
|
|
@@ -235,6 +238,20 @@ function migrateSchema(db, fromVersion) {
|
|
|
235
238
|
db.exec(`ALTER TABLE sessions ADD COLUMN output_tokens INTEGER`);
|
|
236
239
|
db.exec(`DELETE FROM scan_ledger;`);
|
|
237
240
|
}
|
|
241
|
+
if (fromVersion < 13) {
|
|
242
|
+
// v12 → v13: routine runs archive their sandboxed transcript into the run
|
|
243
|
+
// directory and get indexed as origin='routine', linked by routine_name and
|
|
244
|
+
// routine_run_id. Existing rows are normal CLI-origin sessions.
|
|
245
|
+
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
246
|
+
if (!cols.some(c => c.name === 'origin'))
|
|
247
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN origin TEXT DEFAULT 'cli'`);
|
|
248
|
+
if (!cols.some(c => c.name === 'routine_name'))
|
|
249
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN routine_name TEXT`);
|
|
250
|
+
if (!cols.some(c => c.name === 'routine_run_id'))
|
|
251
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN routine_run_id TEXT`);
|
|
252
|
+
db.exec(`UPDATE sessions SET origin = 'cli' WHERE origin IS NULL OR origin = ''`);
|
|
253
|
+
db.exec(`DELETE FROM scan_ledger;`);
|
|
254
|
+
}
|
|
238
255
|
}
|
|
239
256
|
/** Open (or return the cached) sessions database, applying migrations as needed. */
|
|
240
257
|
export function getDB() {
|
|
@@ -266,6 +283,8 @@ export function getDB() {
|
|
|
266
283
|
// run. It must NOT live in SCHEMA (executed before migration) or an existing
|
|
267
284
|
// DB would fail the index build on a column it doesn't have yet.
|
|
268
285
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_last_activity ON sessions(last_activity DESC)`);
|
|
286
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_origin ON sessions(origin)`);
|
|
287
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_routine_run_id ON sessions(routine_run_id)`);
|
|
269
288
|
// One-shot cleanup of the pre-SQLite JSONL indexes. Safe — nothing reads
|
|
270
289
|
// them anymore. Guarded by a meta flag so we only try once.
|
|
271
290
|
const cleaned = db.prepare(`SELECT value FROM meta WHERE key = 'legacy_indexes_removed'`).get();
|
|
@@ -447,13 +466,15 @@ export function recordScans(entries) {
|
|
|
447
466
|
}
|
|
448
467
|
const upsertSessionStmt = (db) => db.prepare(`
|
|
449
468
|
INSERT INTO sessions (
|
|
450
|
-
id, short_id, agent,
|
|
469
|
+
id, short_id, agent, origin, routine_name, routine_run_id,
|
|
470
|
+
version, account, timestamp, last_activity,
|
|
451
471
|
project, cwd, git_branch, topic, label, message_count, token_count,
|
|
452
472
|
output_tokens, cost_usd, duration_ms,
|
|
453
473
|
file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
|
|
454
474
|
pr_url, pr_number, worktree_slug, ticket_id, plan
|
|
455
475
|
) VALUES (
|
|
456
|
-
@id, @short_id, @agent, @
|
|
476
|
+
@id, @short_id, @agent, @origin, @routine_name, @routine_run_id,
|
|
477
|
+
@version, @account, @timestamp, @last_activity,
|
|
457
478
|
@project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
|
|
458
479
|
@output_tokens, @cost_usd, @duration_ms,
|
|
459
480
|
@file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
|
|
@@ -462,6 +483,9 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
462
483
|
ON CONFLICT(id) DO UPDATE SET
|
|
463
484
|
short_id = excluded.short_id,
|
|
464
485
|
agent = excluded.agent,
|
|
486
|
+
origin = excluded.origin,
|
|
487
|
+
routine_name = excluded.routine_name,
|
|
488
|
+
routine_run_id = excluded.routine_run_id,
|
|
465
489
|
version = excluded.version,
|
|
466
490
|
account = excluded.account,
|
|
467
491
|
timestamp = excluded.timestamp,
|
|
@@ -511,6 +535,9 @@ export function upsertSession(meta, content, scan) {
|
|
|
511
535
|
id: meta.id,
|
|
512
536
|
short_id: meta.shortId,
|
|
513
537
|
agent: meta.agent,
|
|
538
|
+
origin: meta.origin ?? 'cli',
|
|
539
|
+
routine_name: meta.routineName ?? null,
|
|
540
|
+
routine_run_id: meta.routineRunId ?? null,
|
|
514
541
|
version: meta.version ?? null,
|
|
515
542
|
account: meta.account ?? null,
|
|
516
543
|
timestamp: meta.timestamp,
|
|
@@ -601,6 +628,9 @@ export function upsertSessionsBatch(entries) {
|
|
|
601
628
|
id: meta.id,
|
|
602
629
|
short_id: meta.shortId,
|
|
603
630
|
agent: meta.agent,
|
|
631
|
+
origin: meta.origin ?? 'cli',
|
|
632
|
+
routine_name: meta.routineName ?? null,
|
|
633
|
+
routine_run_id: meta.routineRunId ?? null,
|
|
604
634
|
version: meta.version ?? null,
|
|
605
635
|
account: meta.account ?? null,
|
|
606
636
|
timestamp: meta.timestamp,
|
|
@@ -778,6 +808,9 @@ function rowToMeta(row) {
|
|
|
778
808
|
id: row.id,
|
|
779
809
|
shortId: row.short_id,
|
|
780
810
|
agent: row.agent,
|
|
811
|
+
origin: (row.origin === 'routine' ? 'routine' : 'cli'),
|
|
812
|
+
routineName: row.routine_name ?? undefined,
|
|
813
|
+
routineRunId: row.routine_run_id ?? undefined,
|
|
781
814
|
timestamp: row.timestamp,
|
|
782
815
|
lastActivity: row.last_activity ?? undefined,
|
|
783
816
|
project: row.project ?? undefined,
|
|
@@ -867,6 +900,10 @@ function buildSessionWhere(options) {
|
|
|
867
900
|
where.push('version = ?');
|
|
868
901
|
params.push(options.version);
|
|
869
902
|
}
|
|
903
|
+
if (options.origin) {
|
|
904
|
+
where.push("IFNULL(origin, 'cli') = ?");
|
|
905
|
+
params.push(options.origin);
|
|
906
|
+
}
|
|
870
907
|
if (options.cwd) {
|
|
871
908
|
where.push('cwd = ?');
|
|
872
909
|
params.push(options.cwd);
|
|
@@ -887,12 +924,12 @@ function buildSessionWhere(options) {
|
|
|
887
924
|
// for the same reason. short_id carries its own index (idx_sessions_short_id);
|
|
888
925
|
// id is the PRIMARY KEY.
|
|
889
926
|
if (options.idExact) {
|
|
890
|
-
where.push('(id = ? COLLATE NOCASE OR short_id = ? COLLATE NOCASE)');
|
|
891
|
-
params.push(options.idExact, options.idExact);
|
|
927
|
+
where.push('(id = ? COLLATE NOCASE OR short_id = ? COLLATE NOCASE OR routine_run_id = ? COLLATE NOCASE)');
|
|
928
|
+
params.push(options.idExact, options.idExact, options.idExact);
|
|
892
929
|
}
|
|
893
930
|
if (options.idPrefix) {
|
|
894
|
-
where.push('(id LIKE ? OR short_id LIKE ?)');
|
|
895
|
-
params.push(`${options.idPrefix}%`, `${options.idPrefix}%`);
|
|
931
|
+
where.push('(id LIKE ? OR short_id LIKE ? OR routine_run_id LIKE ?)');
|
|
932
|
+
params.push(`${options.idPrefix}%`, `${options.idPrefix}%`, `${options.idPrefix}%`);
|
|
896
933
|
}
|
|
897
934
|
if (typeof options.sinceMs === 'number') {
|
|
898
935
|
// Compare as strings; ISO 8601 timestamps sort lexicographically.
|
|
@@ -26,6 +26,8 @@ export interface DiscoverOptions {
|
|
|
26
26
|
excludeTeamOrigin?: boolean;
|
|
27
27
|
/** Keep only team-spawned sessions (used for hidden-count queries). */
|
|
28
28
|
onlyTeamOrigin?: boolean;
|
|
29
|
+
/** Keep only sessions from this source. */
|
|
30
|
+
origin?: 'cli' | 'routine';
|
|
29
31
|
/** Column to order results by (all descending): 'timestamp' (default), 'cost', or 'duration'. */
|
|
30
32
|
sortBy?: 'timestamp' | 'cost' | 'duration';
|
|
31
33
|
/** Called as each agent makes parsing progress. Totals count only files that need re-parsing (cache misses). */
|