@phnx-labs/agents-cli 1.22.13 → 1.22.15
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 +30 -0
- package/README.md +4 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.js +4 -6
- package/dist/commands/packages.js +90 -22
- package/dist/commands/projects.d.ts +2 -5
- package/dist/commands/projects.js +92 -65
- package/dist/commands/routines.js +53 -71
- package/dist/commands/secrets.js +10 -6
- package/dist/commands/sessions-optimize.d.ts +14 -0
- package/dist/commands/sessions-optimize.js +41 -0
- package/dist/commands/sessions.js +2 -0
- package/dist/commands/setup-watchdog.d.ts +3 -0
- package/dist/commands/setup-watchdog.js +54 -0
- package/dist/commands/setup.js +8 -1
- package/dist/commands/ssh.d.ts +11 -0
- package/dist/commands/ssh.js +18 -4
- package/dist/commands/watchdog.d.ts +3 -4
- package/dist/commands/watchdog.js +40 -28
- package/dist/lib/auto-dispatch.d.ts +8 -10
- package/dist/lib/auto-dispatch.js +20 -42
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +4 -3
- package/dist/lib/project-import.d.ts +7 -46
- package/dist/lib/project-import.js +9 -108
- package/dist/lib/projects.d.ts +17 -0
- package/dist/lib/projects.js +17 -1
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +11 -0
- package/dist/lib/routine-activation.d.ts +14 -0
- package/dist/lib/routine-activation.js +78 -0
- package/dist/lib/routines-project.d.ts +1 -1
- package/dist/lib/routines-project.js +3 -9
- package/dist/lib/routines.d.ts +3 -1
- package/dist/lib/routines.js +32 -19
- 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/session/db.d.ts +19 -0
- package/dist/lib/session/db.js +24 -0
- package/dist/lib/state.js +17 -4
- package/dist/lib/types.d.ts +14 -1
- package/dist/lib/watchdog/rotate.d.ts +1 -1
- package/dist/lib/watchdog/rotate.js +1 -1
- package/package.json +1 -1
- package/dist/lib/watchdog/routine.d.ts +0 -44
- package/dist/lib/watchdog/routine.js +0 -69
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function normalizeRoutineNames(names: Iterable<string>): string[];
|
|
2
|
+
/** null means this device has not materialized activation state yet. */
|
|
3
|
+
export declare function enabledRoutineNames(): string[] | null;
|
|
4
|
+
export declare function routineEnabledOnThisDevice(name: string): boolean | null;
|
|
5
|
+
export declare function replaceEnabledRoutines(names: Iterable<string>): string[];
|
|
6
|
+
/**
|
|
7
|
+
* Add or remove one routine on this machine. `legacyEnabledNames` seeds the
|
|
8
|
+
* manifest the first time an upgraded host changes activation, preserving every
|
|
9
|
+
* other routine that was effectively enabled under the old definition fields.
|
|
10
|
+
*/
|
|
11
|
+
export declare function setRoutineEnabledOnThisDevice(name: string, enabled: boolean, legacyEnabledNames?: Iterable<string>): string[];
|
|
12
|
+
/** Read-only fleet view from synced device documents. Never writes a peer file. */
|
|
13
|
+
export declare function devicesWithRoutineEnabled(name: string): string[];
|
|
14
|
+
export declare function currentRoutineDevice(): string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/** Device-scoped routine activation.
|
|
2
|
+
*
|
|
3
|
+
* Definitions say what a routine does. This module owns the independent answer
|
|
4
|
+
* to whether THIS device runs it: membership in the top-level `routines:` list
|
|
5
|
+
* of `~/.agents/devices/<machine>/agents.yaml`.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as yaml from 'yaml';
|
|
10
|
+
import { machineId } from './machine-id.js';
|
|
11
|
+
import { getUserAgentsDir, readMeta, updateMeta } from './state.js';
|
|
12
|
+
export function normalizeRoutineNames(names) {
|
|
13
|
+
return [...new Set([...names].map((name) => name.trim()).filter(Boolean))].sort();
|
|
14
|
+
}
|
|
15
|
+
/** null means this device has not materialized activation state yet. */
|
|
16
|
+
export function enabledRoutineNames() {
|
|
17
|
+
const names = readMeta().deviceRoutines;
|
|
18
|
+
return Array.isArray(names) ? normalizeRoutineNames(names) : null;
|
|
19
|
+
}
|
|
20
|
+
export function routineEnabledOnThisDevice(name) {
|
|
21
|
+
const names = enabledRoutineNames();
|
|
22
|
+
return names === null ? null : names.includes(name);
|
|
23
|
+
}
|
|
24
|
+
export function replaceEnabledRoutines(names) {
|
|
25
|
+
const normalized = normalizeRoutineNames(names);
|
|
26
|
+
updateMeta((meta) => ({ ...meta, deviceRoutines: normalized }));
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Add or remove one routine on this machine. `legacyEnabledNames` seeds the
|
|
31
|
+
* manifest the first time an upgraded host changes activation, preserving every
|
|
32
|
+
* other routine that was effectively enabled under the old definition fields.
|
|
33
|
+
*/
|
|
34
|
+
export function setRoutineEnabledOnThisDevice(name, enabled, legacyEnabledNames = []) {
|
|
35
|
+
const current = enabledRoutineNames() ?? normalizeRoutineNames(legacyEnabledNames);
|
|
36
|
+
const next = new Set(current);
|
|
37
|
+
if (enabled)
|
|
38
|
+
next.add(name);
|
|
39
|
+
else
|
|
40
|
+
next.delete(name);
|
|
41
|
+
return replaceEnabledRoutines(next);
|
|
42
|
+
}
|
|
43
|
+
/** Read-only fleet view from synced device documents. Never writes a peer file. */
|
|
44
|
+
export function devicesWithRoutineEnabled(name) {
|
|
45
|
+
const devicesDir = path.join(getUserAgentsDir(), 'devices');
|
|
46
|
+
if (!fs.existsSync(devicesDir))
|
|
47
|
+
return [];
|
|
48
|
+
const devices = [];
|
|
49
|
+
for (const entry of fs.readdirSync(devicesDir, { withFileTypes: true })) {
|
|
50
|
+
if (!entry.isDirectory())
|
|
51
|
+
continue;
|
|
52
|
+
const file = path.join(devicesDir, entry.name, 'agents.yaml');
|
|
53
|
+
if (!fs.existsSync(file))
|
|
54
|
+
continue;
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = yaml.parse(fs.readFileSync(file, 'utf-8'));
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw new Error(`Device config corrupted at ${file}: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
63
|
+
throw new Error(`Device config corrupted at ${file}: expected a YAML map.`);
|
|
64
|
+
}
|
|
65
|
+
const routines = parsed.routines;
|
|
66
|
+
if (routines === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
if (!Array.isArray(routines) || routines.some((routine) => typeof routine !== 'string')) {
|
|
69
|
+
throw new Error(`Device config corrupted at ${file}: routines must be a string list.`);
|
|
70
|
+
}
|
|
71
|
+
if (routines.includes(name))
|
|
72
|
+
devices.push(entry.name);
|
|
73
|
+
}
|
|
74
|
+
return devices.sort();
|
|
75
|
+
}
|
|
76
|
+
export function currentRoutineDevice() {
|
|
77
|
+
return machineId();
|
|
78
|
+
}
|
|
@@ -67,7 +67,7 @@ export interface SyncProjectResult {
|
|
|
67
67
|
* - Overwrites user copies that already carry matching `source.projectPath`
|
|
68
68
|
* - Never clobbers a hand-authored user routine (no source / different source)
|
|
69
69
|
* - Removes user copies from this project whose YAML disappeared
|
|
70
|
-
* -
|
|
70
|
+
* - Activates materialized routines on this device without mutating definitions
|
|
71
71
|
*/
|
|
72
72
|
export declare function syncProjectRoutines(projectRoot: string): SyncProjectResult;
|
|
73
73
|
export interface SyncAllResult {
|
|
@@ -14,9 +14,8 @@ import * as path from 'path';
|
|
|
14
14
|
import * as yaml from 'yaml';
|
|
15
15
|
import { execFileSync } from 'child_process';
|
|
16
16
|
import { getProjectAgentsDir, getProjectRoutinesDir, readMeta, updateMeta, ensureAgentsDir, } from './state.js';
|
|
17
|
-
import { readJob, writeJob, deleteJob, listJobs, validateJob,
|
|
17
|
+
import { readJob, writeJob, setJobEnabled, deleteJob, listJobs, validateJob, } from './routines.js';
|
|
18
18
|
import { parseOwnerRepoFromRemote } from './registry.js';
|
|
19
|
-
import { machineId } from './machine-id.js';
|
|
20
19
|
import { isSafeSegmentName } from './paths.js';
|
|
21
20
|
/** Expand `~/…` and resolve to an absolute path. */
|
|
22
21
|
export function expandProjectPath(p) {
|
|
@@ -221,7 +220,7 @@ function readProjectJobFile(filePath) {
|
|
|
221
220
|
* - Overwrites user copies that already carry matching `source.projectPath`
|
|
222
221
|
* - Never clobbers a hand-authored user routine (no source / different source)
|
|
223
222
|
* - Removes user copies from this project whose YAML disappeared
|
|
224
|
-
* -
|
|
223
|
+
* - Activates materialized routines on this device without mutating definitions
|
|
225
224
|
*/
|
|
226
225
|
export function syncProjectRoutines(projectRoot) {
|
|
227
226
|
ensureAgentsDir();
|
|
@@ -276,12 +275,6 @@ export function syncProjectRoutines(projectRoot) {
|
|
|
276
275
|
if (existing.createdAt)
|
|
277
276
|
job.createdAt = existing.createdAt;
|
|
278
277
|
}
|
|
279
|
-
// Placement that leaves the firing machine must pin devices to avoid
|
|
280
|
-
// every fleet daemon dispatching once.
|
|
281
|
-
const strategy = resolveHostStrategy(job);
|
|
282
|
-
if (placementRequiresFiringPin(strategy) && (!job.devices || job.devices.length === 0)) {
|
|
283
|
-
job.devices = [machineId()];
|
|
284
|
-
}
|
|
285
278
|
// Surface repo for list/display when project has a GitHub origin.
|
|
286
279
|
if (git.repo && !job.repo)
|
|
287
280
|
job.repo = git.repo;
|
|
@@ -294,6 +287,7 @@ export function syncProjectRoutines(projectRoot) {
|
|
|
294
287
|
}
|
|
295
288
|
try {
|
|
296
289
|
writeJob(job);
|
|
290
|
+
setJobEnabled(job.name, job.enabled);
|
|
297
291
|
result.synced.push(file.name);
|
|
298
292
|
}
|
|
299
293
|
catch (err) {
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -294,7 +294,7 @@ export declare function finalizeRunMeta(meta: RunMeta, status: RunMeta['status']
|
|
|
294
294
|
* `yosemite-s0` all agree. Every fire path (cron scheduler, webhook,
|
|
295
295
|
* catchup/overdue, manual run) gates on this.
|
|
296
296
|
*/
|
|
297
|
-
export declare function jobRunsOnThisDevice(config: Pick<JobConfig, 'devices'>): boolean;
|
|
297
|
+
export declare function jobRunsOnThisDevice(config: Pick<JobConfig, 'name' | 'devices'>): boolean;
|
|
298
298
|
/**
|
|
299
299
|
* The ONE device that owns a routine — the single daemon allowed to fire it.
|
|
300
300
|
*
|
|
@@ -423,6 +423,8 @@ export declare function serializeJob(output: Record<string, unknown>, existingTe
|
|
|
423
423
|
export declare function deleteJob(name: string): boolean;
|
|
424
424
|
/** Enable or disable a job by name. */
|
|
425
425
|
export declare function setJobEnabled(name: string, enabled: boolean): void;
|
|
426
|
+
/** Materialize legacy definition state into this device's activation manifest. */
|
|
427
|
+
export declare function migrateLegacyRoutineActivation(): boolean;
|
|
426
428
|
/** Validate a partial job config, returning a list of human-readable errors. */
|
|
427
429
|
export declare function validateJob(config: Partial<JobConfig>): string[];
|
|
428
430
|
/** Validate a job trigger block, returning a list of human-readable errors. */
|
package/dist/lib/routines.js
CHANGED
|
@@ -17,6 +17,7 @@ import { ALL_AGENT_IDS } from './agents.js';
|
|
|
17
17
|
import { machineId, normalizeHost } from './machine-id.js';
|
|
18
18
|
import { resolveActor } from './actor.js';
|
|
19
19
|
import { percentile } from './percentile.js';
|
|
20
|
+
import { enabledRoutineNames, replaceEnabledRoutines, routineEnabledOnThisDevice, setRoutineEnabledOnThisDevice, } from './routine-activation.js';
|
|
20
21
|
export const HOST_STRATEGIES = ['local', 'host', 'fleet', 'cloud'];
|
|
21
22
|
/** Canonical set of accepted GitHub trigger events — single source for validation. */
|
|
22
23
|
export const GITHUB_TRIGGER_EVENTS = [
|
|
@@ -82,6 +83,9 @@ export function finalizeRunMeta(meta, status, exitCode, opts) {
|
|
|
82
83
|
* catchup/overdue, manual run) gates on this.
|
|
83
84
|
*/
|
|
84
85
|
export function jobRunsOnThisDevice(config) {
|
|
86
|
+
const activated = routineEnabledOnThisDevice(config.name);
|
|
87
|
+
if (activated !== null)
|
|
88
|
+
return activated;
|
|
85
89
|
const owner = routineOwnerDevice(config);
|
|
86
90
|
// Unrestricted: no pin means fleet-wide by design (`watchdog`, `check-updates`).
|
|
87
91
|
if (owner === null)
|
|
@@ -260,7 +264,7 @@ export function listJobs(cwd) {
|
|
|
260
264
|
jobs.push(job);
|
|
261
265
|
}
|
|
262
266
|
}
|
|
263
|
-
return jobs;
|
|
267
|
+
return jobs.map(applyDeviceActivation);
|
|
264
268
|
}
|
|
265
269
|
/**
|
|
266
270
|
* Read a single job config by name, checking project > user > system.
|
|
@@ -286,12 +290,16 @@ export function readJob(name, cwd) {
|
|
|
286
290
|
const job = readJobFromDir(dir, name);
|
|
287
291
|
if (job) {
|
|
288
292
|
if (scope === 'project')
|
|
289
|
-
return overlayUserRoutineDevices(job, readJobFromDir(userDir, name));
|
|
290
|
-
return job;
|
|
293
|
+
return applyDeviceActivation(overlayUserRoutineDevices(job, readJobFromDir(userDir, name)));
|
|
294
|
+
return applyDeviceActivation(job);
|
|
291
295
|
}
|
|
292
296
|
}
|
|
293
297
|
return null;
|
|
294
298
|
}
|
|
299
|
+
function applyDeviceActivation(job) {
|
|
300
|
+
const activated = routineEnabledOnThisDevice(job.name);
|
|
301
|
+
return activated === null ? job : { ...job, enabled: activated };
|
|
302
|
+
}
|
|
295
303
|
function readJobFromDir(dir, name) {
|
|
296
304
|
for (const ext of ['.yml', '.yaml']) {
|
|
297
305
|
const filePath = safeJoin(dir, name + ext);
|
|
@@ -327,6 +335,10 @@ function readJobFile(filePath) {
|
|
|
327
335
|
...JOB_DEFAULTS,
|
|
328
336
|
...parsed,
|
|
329
337
|
name: parsed.name || path.basename(filePath).replace(/\.ya?ml$/, ''),
|
|
338
|
+
// Before a device manifest exists, preserve only explicit legacy state.
|
|
339
|
+
// A new built-in definition with no `enabled:` field stays opt-in until
|
|
340
|
+
// setup materializes this host's routines list.
|
|
341
|
+
enabled: Object.prototype.hasOwnProperty.call(parsed, 'enabled') ? parsed.enabled !== false : false,
|
|
330
342
|
};
|
|
331
343
|
}
|
|
332
344
|
catch {
|
|
@@ -368,15 +380,12 @@ export function writeJob(config) {
|
|
|
368
380
|
delete output.effort;
|
|
369
381
|
if (output.timeout === '10m')
|
|
370
382
|
delete output.timeout;
|
|
371
|
-
|
|
372
|
-
delete output.enabled;
|
|
383
|
+
delete output.enabled;
|
|
373
384
|
if (output.runOnce === false || output.runOnce === undefined)
|
|
374
385
|
delete output.runOnce;
|
|
375
386
|
if (output.catchup === true || output.catchup === undefined)
|
|
376
387
|
delete output.catchup;
|
|
377
|
-
|
|
378
|
-
if (!devArr || devArr.length === 0)
|
|
379
|
-
delete output.devices;
|
|
388
|
+
delete output.devices;
|
|
380
389
|
let existingText = null;
|
|
381
390
|
if (ymlExists || yamlExists) {
|
|
382
391
|
try {
|
|
@@ -441,8 +450,20 @@ export function setJobEnabled(name, enabled) {
|
|
|
441
450
|
const job = readJob(name);
|
|
442
451
|
if (!job)
|
|
443
452
|
throw new Error(`Job '${name}' not found`);
|
|
444
|
-
|
|
445
|
-
|
|
453
|
+
const legacyEnabled = enabledRoutineNames() === null
|
|
454
|
+
? listJobs().filter((candidate) => candidate.enabled && jobRunsOnThisDevice(candidate)).map((candidate) => candidate.name)
|
|
455
|
+
: [];
|
|
456
|
+
setRoutineEnabledOnThisDevice(name, enabled, legacyEnabled);
|
|
457
|
+
}
|
|
458
|
+
/** Materialize legacy definition state into this device's activation manifest. */
|
|
459
|
+
export function migrateLegacyRoutineActivation() {
|
|
460
|
+
if (enabledRoutineNames() !== null)
|
|
461
|
+
return false;
|
|
462
|
+
const jobs = listJobs();
|
|
463
|
+
if (!jobs.some((job) => job.enabled || Array.isArray(job.devices)))
|
|
464
|
+
return false;
|
|
465
|
+
replaceEnabledRoutines(jobs.filter((job) => job.enabled && jobRunsOnThisDevice(job)).map((job) => job.name));
|
|
466
|
+
return true;
|
|
446
467
|
}
|
|
447
468
|
/** Validate a partial job config, returning a list of human-readable errors. */
|
|
448
469
|
export function validateJob(config) {
|
|
@@ -606,15 +627,6 @@ export function validateJob(config) {
|
|
|
606
627
|
if (config.catchup !== undefined && typeof config.catchup !== 'boolean') {
|
|
607
628
|
errors.push('catchup must be a boolean (false to skip running a missed fire late)');
|
|
608
629
|
}
|
|
609
|
-
// Off-box placement without a devices pin fires on every fleet daemon and
|
|
610
|
-
// each dispatches once (RUSH-1980). Enforce the pin at validation so hand
|
|
611
|
-
// edits and devices --clear cannot re-open the hole.
|
|
612
|
-
if (placementRequiresFiringPin(strategy)) {
|
|
613
|
-
if (!config.devices || config.devices.length === 0) {
|
|
614
|
-
errors.push(`hostStrategy: ${strategy} requires devices: [<name>] to pin which daemon may fire ` +
|
|
615
|
-
'(otherwise every fleet daemon dispatches once). Pin to one machine, e.g. devices: [this-host]');
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
630
|
return errors;
|
|
619
631
|
}
|
|
620
632
|
/** Validate a job trigger block, returning a list of human-readable errors. */
|
|
@@ -1155,6 +1167,7 @@ export function installJobFromSource(sourcePath, name) {
|
|
|
1155
1167
|
return { success: false, error: errors.join(', ') };
|
|
1156
1168
|
}
|
|
1157
1169
|
writeJob(config);
|
|
1170
|
+
setJobEnabled(config.name, config.enabled);
|
|
1158
1171
|
return { success: true };
|
|
1159
1172
|
}
|
|
1160
1173
|
catch (err) {
|
|
Binary file
|
|
Binary file
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -120,6 +120,25 @@ export interface QueryOptions {
|
|
|
120
120
|
export declare function getDB(): Database.Database;
|
|
121
121
|
/** Close the cached database connection. */
|
|
122
122
|
export declare function closeDB(): void;
|
|
123
|
+
export interface FtsOptimizeResult {
|
|
124
|
+
table: string;
|
|
125
|
+
segmentsBefore: number;
|
|
126
|
+
segmentsAfter: number;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Compact the session + tool-call full-text-search indexes.
|
|
130
|
+
*
|
|
131
|
+
* FTS5 appends a new segment on every insert and leaves a tombstone on every
|
|
132
|
+
* delete. The scanner delete+inserts a session's docs on each rescan (and on
|
|
133
|
+
* every extractor-version bump), and FTS5 never merges those segments on its
|
|
134
|
+
* own — so `tool_call_text_data` / `session_text_data` accumulate hundreds of
|
|
135
|
+
* thousands of unmerged segments, gigabytes of index for tens of MB of content,
|
|
136
|
+
* and `agents sessions` queries slow to a crawl. The FTS5 `'optimize'` command
|
|
137
|
+
* merges every segment into one and purges tombstones. Non-destructive: no
|
|
138
|
+
* searchable content is lost. Reclaimed space becomes reusable free pages inside
|
|
139
|
+
* the DB file; run VACUUM (with the daemon stopped) to return it to the OS.
|
|
140
|
+
*/
|
|
141
|
+
export declare function optimizeSessionSearchIndex(): FtsOptimizeResult[];
|
|
123
142
|
/**
|
|
124
143
|
* Try to claim the right to run the incremental scan. Returns true if this
|
|
125
144
|
* process should proceed with scanning, false if another live process is
|
package/dist/lib/session/db.js
CHANGED
|
@@ -820,6 +820,30 @@ export function closeDB() {
|
|
|
820
820
|
cachedStmts = {};
|
|
821
821
|
}
|
|
822
822
|
}
|
|
823
|
+
/**
|
|
824
|
+
* Compact the session + tool-call full-text-search indexes.
|
|
825
|
+
*
|
|
826
|
+
* FTS5 appends a new segment on every insert and leaves a tombstone on every
|
|
827
|
+
* delete. The scanner delete+inserts a session's docs on each rescan (and on
|
|
828
|
+
* every extractor-version bump), and FTS5 never merges those segments on its
|
|
829
|
+
* own — so `tool_call_text_data` / `session_text_data` accumulate hundreds of
|
|
830
|
+
* thousands of unmerged segments, gigabytes of index for tens of MB of content,
|
|
831
|
+
* and `agents sessions` queries slow to a crawl. The FTS5 `'optimize'` command
|
|
832
|
+
* merges every segment into one and purges tombstones. Non-destructive: no
|
|
833
|
+
* searchable content is lost. Reclaimed space becomes reusable free pages inside
|
|
834
|
+
* the DB file; run VACUUM (with the daemon stopped) to return it to the OS.
|
|
835
|
+
*/
|
|
836
|
+
export function optimizeSessionSearchIndex() {
|
|
837
|
+
const db = getDB();
|
|
838
|
+
// Hardcoded literals — never interpolate caller input into an identifier.
|
|
839
|
+
const tables = ['tool_call_text', 'session_text'];
|
|
840
|
+
const segments = (table) => db.prepare(`SELECT count(*) AS n FROM ${table}_data`).get().n;
|
|
841
|
+
return tables.map((table) => {
|
|
842
|
+
const segmentsBefore = segments(table);
|
|
843
|
+
db.prepare(`INSERT INTO ${table}(${table}) VALUES('optimize')`).run();
|
|
844
|
+
return { table, segmentsBefore, segmentsAfter: segments(table) };
|
|
845
|
+
});
|
|
846
|
+
}
|
|
823
847
|
// ---------------------------------------------------------------------------
|
|
824
848
|
// Scan coordinator — prevents concurrent full scans across processes
|
|
825
849
|
// ---------------------------------------------------------------------------
|
package/dist/lib/state.js
CHANGED
|
@@ -812,6 +812,7 @@ const META_KEY_SCOPE = {
|
|
|
812
812
|
versions: 'device',
|
|
813
813
|
defaultBrowserProfile: 'device',
|
|
814
814
|
deviceConfig: 'device',
|
|
815
|
+
deviceRoutines: 'device',
|
|
815
816
|
// Central — synced via agents.yaml.
|
|
816
817
|
run: 'central',
|
|
817
818
|
model: 'central',
|
|
@@ -909,7 +910,7 @@ function serializeCentral(central) {
|
|
|
909
910
|
return isEmpty ? META_HEADER : doc.toString({ collectionStyle: 'block' });
|
|
910
911
|
}
|
|
911
912
|
function writeMetaUnlocked(meta) {
|
|
912
|
-
const { agents, isolatedAgents, versions, defaultBrowserProfile, deviceConfig, ...central } = meta;
|
|
913
|
+
const { agents, isolatedAgents, versions, defaultBrowserProfile, deviceConfig, deviceRoutines, ...central } = meta;
|
|
913
914
|
// Write the machine-local files FIRST, then strip central — so a crash mid-write
|
|
914
915
|
// never removes pins/versions from central before they're persisted elsewhere.
|
|
915
916
|
const devicePath = getDeviceMetaPath();
|
|
@@ -923,7 +924,8 @@ function writeMetaUnlocked(meta) {
|
|
|
923
924
|
// Device-scope config (`maxAgents`, `schedulerEnabled`, …) is per-machine, so it
|
|
924
925
|
// rides the device doc under `config:` — never the central doc that syncs.
|
|
925
926
|
const hasDeviceConfig = !!deviceConfig && Object.keys(deviceConfig).length > 0;
|
|
926
|
-
|
|
927
|
+
const hasDeviceRoutines = Array.isArray(deviceRoutines);
|
|
928
|
+
if (hasAgents || hasIsolatedAgents || hasDefaultBrowser || hasDeviceConfig || hasDeviceRoutines) {
|
|
927
929
|
// Device-local doc carries `agents:` pins, `defaultBrowserProfile:`, and the
|
|
928
930
|
// device-scope `config:` — all per-machine and must never land in central
|
|
929
931
|
// agents.yaml (which syncs).
|
|
@@ -936,6 +938,8 @@ function writeMetaUnlocked(meta) {
|
|
|
936
938
|
deviceDoc.defaultBrowserProfile = defaultBrowserProfile;
|
|
937
939
|
if (hasDeviceConfig)
|
|
938
940
|
deviceDoc.config = deviceConfig;
|
|
941
|
+
if (hasDeviceRoutines)
|
|
942
|
+
deviceDoc.routines = deviceRoutines;
|
|
939
943
|
fs.mkdirSync(path.dirname(devicePath), { recursive: true });
|
|
940
944
|
writeIfChanged(devicePath, META_HEADER + yaml.stringify(deviceDoc));
|
|
941
945
|
}
|
|
@@ -971,8 +975,12 @@ function writeMetaUnlocked(meta) {
|
|
|
971
975
|
function overlayMachineLocal(meta) {
|
|
972
976
|
const devicePath = getDeviceMetaPath();
|
|
973
977
|
if (fs.existsSync(devicePath)) {
|
|
978
|
+
let dm = null;
|
|
974
979
|
try {
|
|
975
|
-
|
|
980
|
+
dm = yaml.parse(fs.readFileSync(devicePath, 'utf-8'));
|
|
981
|
+
}
|
|
982
|
+
catch { /* preserve the existing tolerance for malformed legacy device YAML */ }
|
|
983
|
+
if (dm) {
|
|
976
984
|
if (dm?.agents)
|
|
977
985
|
meta.agents = { ...meta.agents, ...dm.agents };
|
|
978
986
|
if (dm?.isolatedAgents)
|
|
@@ -981,8 +989,13 @@ function overlayMachineLocal(meta) {
|
|
|
981
989
|
meta.defaultBrowserProfile = dm.defaultBrowserProfile;
|
|
982
990
|
if (dm?.config)
|
|
983
991
|
meta.deviceConfig = dm.config;
|
|
992
|
+
if (dm && Object.prototype.hasOwnProperty.call(dm, 'routines')) {
|
|
993
|
+
if (!Array.isArray(dm.routines) || dm.routines.some((name) => typeof name !== 'string')) {
|
|
994
|
+
throw new Error(`Device config corrupted at ${devicePath}: routines must be a string list.`);
|
|
995
|
+
}
|
|
996
|
+
meta.deviceRoutines = dm.routines;
|
|
997
|
+
}
|
|
984
998
|
}
|
|
985
|
-
catch { /* ignore malformed device file */ }
|
|
986
999
|
}
|
|
987
1000
|
const vrPath = getVersionResourcesPath();
|
|
988
1001
|
if (fs.existsSync(vrPath)) {
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -511,10 +511,16 @@ export interface RegistrySearchResult {
|
|
|
511
511
|
}
|
|
512
512
|
/** A package that has been resolved from a registry and is ready to install. */
|
|
513
513
|
export interface ResolvedPackage {
|
|
514
|
-
|
|
514
|
+
/** `plugin` is Phase 5 packaging: `agents install plugin:<spec>` → plugins install. */
|
|
515
|
+
type: 'mcp' | 'skill' | 'git' | 'plugin';
|
|
515
516
|
source: string;
|
|
516
517
|
mcpEntry?: McpServerEntry;
|
|
517
518
|
skillEntry?: SkillEntry;
|
|
519
|
+
/**
|
|
520
|
+
* Plugin install spec (`name@url`, path, or bare source) when `type === 'plugin'`.
|
|
521
|
+
* Same grammar as `agents plugins install <spec>`.
|
|
522
|
+
*/
|
|
523
|
+
pluginSpec?: string;
|
|
518
524
|
}
|
|
519
525
|
/** Categories of resources that can be synced into an agent version home. */
|
|
520
526
|
export type ResourceType = 'commands' | 'skills' | 'hooks' | 'memory' | 'mcp' | 'permissions' | 'subagents' | 'plugins' | 'workflows';
|
|
@@ -937,6 +943,13 @@ export interface Meta {
|
|
|
937
943
|
* device-config registry (`maxAgents`, `schedulerEnabled`, `notes`). Per-machine by design — unset = today's behavior.
|
|
938
944
|
*/
|
|
939
945
|
deviceConfig?: Record<string, unknown>;
|
|
946
|
+
/**
|
|
947
|
+
* Routine names enabled on this machine. In memory this stays distinct from
|
|
948
|
+
* portable user config; state.ts writes it as top-level `routines:` in
|
|
949
|
+
* `~/.agents/devices/<machine>/agents.yaml`. Presence in the list is the whole
|
|
950
|
+
* activation state: absent means disabled on this device.
|
|
951
|
+
*/
|
|
952
|
+
deviceRoutines?: string[];
|
|
940
953
|
/**
|
|
941
954
|
* Agent-host registry keyed by host name (`agents hosts`). Portable user
|
|
942
955
|
* config synced with `agents repo push/pull`. For `ssh-config` hosts this is
|
|
@@ -166,7 +166,7 @@ export declare function isWatchdogRotateEnabled(): boolean;
|
|
|
166
166
|
* Persist `watchdog.rotate: on|off`. Called by the `agents watchdog rotate
|
|
167
167
|
* on|off` subcommand (commands/watchdog.ts) — the rotate-only switch the
|
|
168
168
|
* Factory migration uses so a user who opted out of autoRotate keeps nudging
|
|
169
|
-
* (rather than `agents watchdog
|
|
169
|
+
* (rather than `agents watchdog off`, which disables the whole watchdog here).
|
|
170
170
|
*/
|
|
171
171
|
export declare function setWatchdogRotateEnabled(on: boolean): void;
|
|
172
172
|
export interface RotateGateResult {
|
|
@@ -291,7 +291,7 @@ export function isWatchdogRotateEnabled() {
|
|
|
291
291
|
* Persist `watchdog.rotate: on|off`. Called by the `agents watchdog rotate
|
|
292
292
|
* on|off` subcommand (commands/watchdog.ts) — the rotate-only switch the
|
|
293
293
|
* Factory migration uses so a user who opted out of autoRotate keeps nudging
|
|
294
|
-
* (rather than `agents watchdog
|
|
294
|
+
* (rather than `agents watchdog off`, which disables the whole watchdog here).
|
|
295
295
|
*/
|
|
296
296
|
export function setWatchdogRotateEnabled(on) {
|
|
297
297
|
const meta = readMeta();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.15",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The watchdog, expressed as a routine.
|
|
3
|
-
*
|
|
4
|
-
* The always-on watchdog is not a bespoke subsystem — it is a plain `command`
|
|
5
|
-
* routine the daemon's existing scheduler fires: `agents watchdog --nudge`, once
|
|
6
|
-
* every couple of minutes. Enabling / disabling the watchdog is enabling /
|
|
7
|
-
* disabling that routine, so there is ONE durable, inspectable concept
|
|
8
|
-
* (`agents routines list` shows it; the scheduler catches it up if the daemon was
|
|
9
|
-
* down) instead of a private sentinel file and a hand-rolled `--watch` loop.
|
|
10
|
-
*
|
|
11
|
-
* A `command` routine — not `agent`/`workflow` — because the tick is deterministic
|
|
12
|
-
* housekeeping: no LLM, no auth, no sandbox. Each fire runs ONE bounded tick (the
|
|
13
|
-
* cron is the loop), so the daemon's per-fire model fits without a long-lived
|
|
14
|
-
* process.
|
|
15
|
-
*/
|
|
16
|
-
import { type JobConfig } from '../routines.js';
|
|
17
|
-
/** The routine name. `agents routines list` / `readJob` key on this. */
|
|
18
|
-
export declare const WATCHDOG_ROUTINE_NAME = "watchdog";
|
|
19
|
-
/**
|
|
20
|
-
* Fire every 2 minutes. Mirrors the extension's 120s tick, and is well inside the
|
|
21
|
-
* 5m stall threshold and 20m nudge cooldown, so a stall is noticed promptly
|
|
22
|
-
* without the daemon spawning a tick more often than the cooldown can matter.
|
|
23
|
-
*/
|
|
24
|
-
export declare const WATCHDOG_ROUTINE_SCHEDULE = "*/2 * * * *";
|
|
25
|
-
/** The plain shell each fire runs: one bounded, deterministic nudge tick. */
|
|
26
|
-
export declare const WATCHDOG_ROUTINE_COMMAND = "agents watchdog --nudge";
|
|
27
|
-
/**
|
|
28
|
-
* Build the watchdog routine config. Pure — touches no disk. `mode`/`effort`/
|
|
29
|
-
* `timeout` are required by JobConfig but ignored for `command` routines;
|
|
30
|
-
* writeJob() drops the default ('auto'/'10m') values so the on-disk file stays
|
|
31
|
-
* to the point (name + schedule + command + enabled).
|
|
32
|
-
*/
|
|
33
|
-
export declare function buildWatchdogRoutine(enabled: boolean): JobConfig;
|
|
34
|
-
/** Is the watchdog routine present on disk (user or system layer)? */
|
|
35
|
-
export declare function watchdogRoutineExists(): boolean;
|
|
36
|
-
/** Is the watchdog routine present AND enabled? */
|
|
37
|
-
export declare function isWatchdogRoutineEnabled(): boolean;
|
|
38
|
-
/**
|
|
39
|
-
* Ensure the watchdog routine exists with the requested enabled state, preserving
|
|
40
|
-
* any user edits. Absent → create it; present → only flip `enabled` (never clobber
|
|
41
|
-
* a schedule or command the user tuned). Idempotent: calling it twice with the
|
|
42
|
-
* same state is a no-op after the first.
|
|
43
|
-
*/
|
|
44
|
-
export declare function ensureWatchdogRoutine(enabled: boolean): void;
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The watchdog, expressed as a routine.
|
|
3
|
-
*
|
|
4
|
-
* The always-on watchdog is not a bespoke subsystem — it is a plain `command`
|
|
5
|
-
* routine the daemon's existing scheduler fires: `agents watchdog --nudge`, once
|
|
6
|
-
* every couple of minutes. Enabling / disabling the watchdog is enabling /
|
|
7
|
-
* disabling that routine, so there is ONE durable, inspectable concept
|
|
8
|
-
* (`agents routines list` shows it; the scheduler catches it up if the daemon was
|
|
9
|
-
* down) instead of a private sentinel file and a hand-rolled `--watch` loop.
|
|
10
|
-
*
|
|
11
|
-
* A `command` routine — not `agent`/`workflow` — because the tick is deterministic
|
|
12
|
-
* housekeeping: no LLM, no auth, no sandbox. Each fire runs ONE bounded tick (the
|
|
13
|
-
* cron is the loop), so the daemon's per-fire model fits without a long-lived
|
|
14
|
-
* process.
|
|
15
|
-
*/
|
|
16
|
-
import { readJob, writeJob, setJobEnabled } from '../routines.js';
|
|
17
|
-
/** The routine name. `agents routines list` / `readJob` key on this. */
|
|
18
|
-
export const WATCHDOG_ROUTINE_NAME = 'watchdog';
|
|
19
|
-
/**
|
|
20
|
-
* Fire every 2 minutes. Mirrors the extension's 120s tick, and is well inside the
|
|
21
|
-
* 5m stall threshold and 20m nudge cooldown, so a stall is noticed promptly
|
|
22
|
-
* without the daemon spawning a tick more often than the cooldown can matter.
|
|
23
|
-
*/
|
|
24
|
-
export const WATCHDOG_ROUTINE_SCHEDULE = '*/2 * * * *';
|
|
25
|
-
/** The plain shell each fire runs: one bounded, deterministic nudge tick. */
|
|
26
|
-
export const WATCHDOG_ROUTINE_COMMAND = 'agents watchdog --nudge';
|
|
27
|
-
/**
|
|
28
|
-
* Build the watchdog routine config. Pure — touches no disk. `mode`/`effort`/
|
|
29
|
-
* `timeout` are required by JobConfig but ignored for `command` routines;
|
|
30
|
-
* writeJob() drops the default ('auto'/'10m') values so the on-disk file stays
|
|
31
|
-
* to the point (name + schedule + command + enabled).
|
|
32
|
-
*/
|
|
33
|
-
export function buildWatchdogRoutine(enabled) {
|
|
34
|
-
return {
|
|
35
|
-
name: WATCHDOG_ROUTINE_NAME,
|
|
36
|
-
schedule: WATCHDOG_ROUTINE_SCHEDULE,
|
|
37
|
-
command: WATCHDOG_ROUTINE_COMMAND,
|
|
38
|
-
enabled,
|
|
39
|
-
mode: 'auto',
|
|
40
|
-
effort: 'auto',
|
|
41
|
-
timeout: '10m',
|
|
42
|
-
prompt: '',
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
/** Is the watchdog routine present on disk (user or system layer)? */
|
|
46
|
-
export function watchdogRoutineExists() {
|
|
47
|
-
return readJob(WATCHDOG_ROUTINE_NAME) !== null;
|
|
48
|
-
}
|
|
49
|
-
/** Is the watchdog routine present AND enabled? */
|
|
50
|
-
export function isWatchdogRoutineEnabled() {
|
|
51
|
-
const job = readJob(WATCHDOG_ROUTINE_NAME);
|
|
52
|
-
return job !== null && job.enabled === true;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Ensure the watchdog routine exists with the requested enabled state, preserving
|
|
56
|
-
* any user edits. Absent → create it; present → only flip `enabled` (never clobber
|
|
57
|
-
* a schedule or command the user tuned). Idempotent: calling it twice with the
|
|
58
|
-
* same state is a no-op after the first.
|
|
59
|
-
*/
|
|
60
|
-
export function ensureWatchdogRoutine(enabled) {
|
|
61
|
-
const existing = readJob(WATCHDOG_ROUTINE_NAME);
|
|
62
|
-
if (existing === null) {
|
|
63
|
-
writeJob(buildWatchdogRoutine(enabled));
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (existing.enabled !== enabled) {
|
|
67
|
-
setJobEnabled(WATCHDOG_ROUTINE_NAME, enabled);
|
|
68
|
-
}
|
|
69
|
-
}
|