@phnx-labs/agents-cli 1.20.82 → 1.20.83
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 +108 -0
- package/README.md +1 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.d.ts +2 -1
- package/dist/commands/doctor.js +25 -6
- package/dist/commands/menubar.js +12 -0
- package/dist/commands/routines.d.ts +8 -1
- package/dist/commands/routines.js +229 -75
- package/dist/commands/sessions.d.ts +84 -0
- package/dist/commands/sessions.js +48 -19
- package/dist/index.js +47 -0
- package/dist/lib/exec.d.ts +7 -0
- package/dist/lib/exec.js +34 -3
- package/dist/lib/hooks.d.ts +29 -0
- package/dist/lib/hooks.js +109 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +24 -0
- package/dist/lib/menubar/install-menubar.js +52 -2
- package/dist/lib/routines.d.ts +13 -0
- package/dist/lib/routines.js +116 -0
- package/dist/lib/scheduler.js +16 -4
- 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/active.d.ts +18 -0
- package/dist/lib/session/active.js +32 -4
- package/dist/lib/state.js +57 -1
- package/package.json +1 -1
package/dist/lib/routines.d.ts
CHANGED
|
@@ -347,6 +347,19 @@ export declare function validateJob(config: Partial<JobConfig>): string[];
|
|
|
347
347
|
export declare function validateTrigger(trigger: unknown): string[];
|
|
348
348
|
/** True when a job's endAt has already elapsed. False when endAt is unset or in the future. */
|
|
349
349
|
export declare function isPastEndAt(config: Pick<JobConfig, 'endAt'>, now?: Date): boolean;
|
|
350
|
+
export interface OneShotScheduleParts {
|
|
351
|
+
minute: number;
|
|
352
|
+
hour: number;
|
|
353
|
+
day: number;
|
|
354
|
+
month: number;
|
|
355
|
+
}
|
|
356
|
+
export declare function parseOneShotLikeSchedule(schedule: string | undefined | null): OneShotScheduleParts | null;
|
|
357
|
+
export declare function isOneShotLikeSchedule(schedule: string | undefined | null): boolean;
|
|
358
|
+
export declare function isOneShotRoutine(config: Pick<JobConfig, 'schedule' | 'runOnce'>): boolean;
|
|
359
|
+
export declare function oneShotScheduleFireDate(schedule: string | undefined | null, now?: Date, timezone?: string): Date | null;
|
|
360
|
+
export declare function isPastOneShotRoutine(config: Pick<JobConfig, 'schedule' | 'runOnce' | 'timezone'>, now?: Date): boolean;
|
|
361
|
+
export declare function hasCompletedOneShotRun(config: Pick<JobConfig, 'name' | 'schedule' | 'runOnce' | 'timezone'>, now?: Date): boolean;
|
|
362
|
+
export declare function shouldPurgeCompletedOneShotRoutine(config: Pick<JobConfig, 'name' | 'schedule' | 'runOnce' | 'timezone'>, now?: Date): boolean;
|
|
350
363
|
/** Expand built-in and user-defined template variables in a job's prompt string. */
|
|
351
364
|
export declare function resolveJobPrompt(config: JobConfig): string;
|
|
352
365
|
/** Parse a human-readable timeout string (e.g. "10m", "2h", "1h30m", "3d", "1w") into milliseconds.
|
package/dist/lib/routines.js
CHANGED
|
@@ -586,6 +586,122 @@ export function isPastEndAt(config, now = new Date()) {
|
|
|
586
586
|
return false;
|
|
587
587
|
return now.getTime() >= end;
|
|
588
588
|
}
|
|
589
|
+
export function parseOneShotLikeSchedule(schedule) {
|
|
590
|
+
if (!schedule)
|
|
591
|
+
return null;
|
|
592
|
+
const parts = schedule.trim().split(/\s+/);
|
|
593
|
+
if (parts.length !== 5)
|
|
594
|
+
return null;
|
|
595
|
+
const [minuteRaw, hourRaw, dayRaw, monthRaw, weekdayRaw] = parts;
|
|
596
|
+
if (weekdayRaw !== '*')
|
|
597
|
+
return null;
|
|
598
|
+
if (![minuteRaw, hourRaw, dayRaw, monthRaw].every((p) => /^\d+$/.test(p)))
|
|
599
|
+
return null;
|
|
600
|
+
const minute = parseInt(minuteRaw, 10);
|
|
601
|
+
const hour = parseInt(hourRaw, 10);
|
|
602
|
+
const day = parseInt(dayRaw, 10);
|
|
603
|
+
const month = parseInt(monthRaw, 10);
|
|
604
|
+
if (minute < 0 || minute > 59)
|
|
605
|
+
return null;
|
|
606
|
+
if (hour < 0 || hour > 23)
|
|
607
|
+
return null;
|
|
608
|
+
if (month < 1 || month > 12)
|
|
609
|
+
return null;
|
|
610
|
+
if (day < 1 || day > 31)
|
|
611
|
+
return null;
|
|
612
|
+
return { minute, hour, day, month };
|
|
613
|
+
}
|
|
614
|
+
export function isOneShotLikeSchedule(schedule) {
|
|
615
|
+
return parseOneShotLikeSchedule(schedule) !== null;
|
|
616
|
+
}
|
|
617
|
+
export function isOneShotRoutine(config) {
|
|
618
|
+
return Boolean(config.runOnce || isOneShotLikeSchedule(config.schedule));
|
|
619
|
+
}
|
|
620
|
+
function zonedParts(date, timezone) {
|
|
621
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
622
|
+
timeZone: timezone,
|
|
623
|
+
year: 'numeric',
|
|
624
|
+
month: 'numeric',
|
|
625
|
+
day: 'numeric',
|
|
626
|
+
hour: 'numeric',
|
|
627
|
+
minute: 'numeric',
|
|
628
|
+
hour12: false,
|
|
629
|
+
hourCycle: 'h23',
|
|
630
|
+
}).formatToParts(date);
|
|
631
|
+
const get = (type) => parseInt(parts.find((p) => p.type === type)?.value ?? '0', 10);
|
|
632
|
+
return {
|
|
633
|
+
year: get('year'),
|
|
634
|
+
month: get('month'),
|
|
635
|
+
day: get('day'),
|
|
636
|
+
hour: get('hour'),
|
|
637
|
+
minute: get('minute'),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function zonedDateToUtc(year, parts, timezone) {
|
|
641
|
+
const targetUtc = Date.UTC(year, parts.month - 1, parts.day, parts.hour, parts.minute, 0, 0);
|
|
642
|
+
let candidate = new Date(targetUtc);
|
|
643
|
+
for (let i = 0; i < 4; i++) {
|
|
644
|
+
const actual = zonedParts(candidate, timezone);
|
|
645
|
+
const actualUtc = Date.UTC(actual.year, actual.month - 1, actual.day, actual.hour, actual.minute, 0, 0);
|
|
646
|
+
const delta = actualUtc - targetUtc;
|
|
647
|
+
if (delta === 0)
|
|
648
|
+
break;
|
|
649
|
+
candidate = new Date(candidate.getTime() - delta);
|
|
650
|
+
}
|
|
651
|
+
const verify = zonedParts(candidate, timezone);
|
|
652
|
+
if (verify.year !== year ||
|
|
653
|
+
verify.month !== parts.month ||
|
|
654
|
+
verify.day !== parts.day ||
|
|
655
|
+
verify.hour !== parts.hour ||
|
|
656
|
+
verify.minute !== parts.minute) {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
return candidate;
|
|
660
|
+
}
|
|
661
|
+
export function oneShotScheduleFireDate(schedule, now = new Date(), timezone) {
|
|
662
|
+
const parts = parseOneShotLikeSchedule(schedule);
|
|
663
|
+
if (!parts)
|
|
664
|
+
return null;
|
|
665
|
+
if (timezone) {
|
|
666
|
+
try {
|
|
667
|
+
const year = zonedParts(now, timezone).year;
|
|
668
|
+
return zonedDateToUtc(year, parts, timezone);
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const fireAt = new Date(now.getFullYear(), parts.month - 1, parts.day, parts.hour, parts.minute, 0, 0);
|
|
675
|
+
if (fireAt.getFullYear() !== now.getFullYear() ||
|
|
676
|
+
fireAt.getMonth() !== parts.month - 1 ||
|
|
677
|
+
fireAt.getDate() !== parts.day ||
|
|
678
|
+
fireAt.getHours() !== parts.hour ||
|
|
679
|
+
fireAt.getMinutes() !== parts.minute) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
return fireAt;
|
|
683
|
+
}
|
|
684
|
+
export function isPastOneShotRoutine(config, now = new Date()) {
|
|
685
|
+
if (!isOneShotRoutine(config))
|
|
686
|
+
return false;
|
|
687
|
+
const fireAt = oneShotScheduleFireDate(config.schedule, now, config.timezone);
|
|
688
|
+
return Boolean(fireAt && now.getTime() >= fireAt.getTime());
|
|
689
|
+
}
|
|
690
|
+
export function hasCompletedOneShotRun(config, now = new Date()) {
|
|
691
|
+
if (!isPastOneShotRoutine(config, now))
|
|
692
|
+
return false;
|
|
693
|
+
const fireAt = oneShotScheduleFireDate(config.schedule, now, config.timezone);
|
|
694
|
+
if (!fireAt)
|
|
695
|
+
return false;
|
|
696
|
+
const latest = getLatestRun(config.name);
|
|
697
|
+
if (!latest || latest.status === 'running')
|
|
698
|
+
return false;
|
|
699
|
+
const startedAt = Date.parse(latest.startedAt);
|
|
700
|
+
return Number.isFinite(startedAt) && startedAt >= fireAt.getTime() - 60_000;
|
|
701
|
+
}
|
|
702
|
+
export function shouldPurgeCompletedOneShotRoutine(config, now = new Date()) {
|
|
703
|
+
return hasCompletedOneShotRun(config, now);
|
|
704
|
+
}
|
|
589
705
|
/** Expand built-in and user-defined template variables in a job's prompt string. */
|
|
590
706
|
export function resolveJobPrompt(config) {
|
|
591
707
|
const now = new Date();
|
package/dist/lib/scheduler.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* on startup and reloads them on SIGHUP.
|
|
7
7
|
*/
|
|
8
8
|
import { Cron } from 'croner';
|
|
9
|
-
import { listJobs, deleteJob, isPastEndAt, setJobEnabled, jobRunsOnThisDevice } from './routines.js';
|
|
9
|
+
import { listJobs, deleteJob, isPastEndAt, isPastOneShotRoutine, isOneShotRoutine, setJobEnabled, shouldPurgeCompletedOneShotRoutine, jobRunsOnThisDevice, } from './routines.js';
|
|
10
10
|
/** In-memory cron scheduler that triggers a callback when jobs fire. */
|
|
11
11
|
export class JobScheduler {
|
|
12
12
|
jobs = new Map();
|
|
@@ -20,9 +20,15 @@ export class JobScheduler {
|
|
|
20
20
|
// Trigger-only jobs (no cron schedule) fire via the webhook receiver,
|
|
21
21
|
// not the cron loop — skip them here. Jobs pinned to another device
|
|
22
22
|
// (routines are fleet-synced) never enter this machine's cron loop.
|
|
23
|
-
if (config.enabled
|
|
24
|
-
|
|
23
|
+
if (!config.enabled || !config.schedule || !jobRunsOnThisDevice(config))
|
|
24
|
+
continue;
|
|
25
|
+
if (shouldPurgeCompletedOneShotRoutine(config)) {
|
|
26
|
+
deleteJob(config.name);
|
|
27
|
+
continue;
|
|
25
28
|
}
|
|
29
|
+
if (isPastOneShotRoutine(config))
|
|
30
|
+
continue;
|
|
31
|
+
this.schedule(config);
|
|
26
32
|
}
|
|
27
33
|
}
|
|
28
34
|
schedule(config) {
|
|
@@ -30,6 +36,12 @@ export class JobScheduler {
|
|
|
30
36
|
if (!config.schedule)
|
|
31
37
|
return;
|
|
32
38
|
this.unschedule(config.name);
|
|
39
|
+
if (shouldPurgeCompletedOneShotRoutine(config)) {
|
|
40
|
+
deleteJob(config.name);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (isPastOneShotRoutine(config))
|
|
44
|
+
return;
|
|
33
45
|
// catch: true — a throw from one job's callback should not kill the
|
|
34
46
|
// whole cron loop. Each invocation of onTrigger is already wrapped in
|
|
35
47
|
// try/catch, but a synchronous throw before the await would otherwise
|
|
@@ -59,7 +71,7 @@ export class JobScheduler {
|
|
|
59
71
|
console.error(`Job '${config.name}' failed:`, err.message);
|
|
60
72
|
}
|
|
61
73
|
// One-shot jobs: remove after first execution
|
|
62
|
-
if (config
|
|
74
|
+
if (isOneShotRoutine(config)) {
|
|
63
75
|
this.unschedule(config.name);
|
|
64
76
|
deleteJob(config.name);
|
|
65
77
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -147,6 +147,17 @@ export interface ActiveSession {
|
|
|
147
147
|
*/
|
|
148
148
|
machine?: string;
|
|
149
149
|
teamName?: string;
|
|
150
|
+
/**
|
|
151
|
+
* For a teams teammate: the session id of the ORCHESTRATOR that spawned the
|
|
152
|
+
* team (the agent that ran `agents teams add`, captured from AGENTS_SESSION_ID
|
|
153
|
+
* at spawn). Lets the listing answer "which session spun up this team" and
|
|
154
|
+
* group teammates under their orchestrator. Distinct from `sessionId`, which is
|
|
155
|
+
* the teammate's OWN transcript.
|
|
156
|
+
*/
|
|
157
|
+
orchestratorSessionId?: string;
|
|
158
|
+
/** Display label for the orchestrator (its topic/label), resolved when the
|
|
159
|
+
* orchestrator is itself present in the active set. Display-only. */
|
|
160
|
+
orchestratorLabel?: string;
|
|
150
161
|
agentId?: string;
|
|
151
162
|
cloudProvider?: string;
|
|
152
163
|
cloudTaskId?: string;
|
|
@@ -445,6 +456,13 @@ export declare function listTmuxAgentSessions(): Promise<ActiveSession[]>;
|
|
|
445
456
|
* terminal/headless row for the same session id.
|
|
446
457
|
*/
|
|
447
458
|
export declare function getActiveSessions(opts?: ActiveQueryOptions): Promise<ActiveSession[]>;
|
|
459
|
+
/**
|
|
460
|
+
* Resolve each teams row's `orchestratorLabel` from the orchestrator's own row,
|
|
461
|
+
* when that orchestrator session is itself in the active set (it usually is — the
|
|
462
|
+
* agent that ran `agents teams add` is running). Falls back to nothing, so the
|
|
463
|
+
* renderer shows the short id. Pure over the array; exported for tests.
|
|
464
|
+
*/
|
|
465
|
+
export declare function annotateOrchestratorLabels(sessions: ActiveSession[]): void;
|
|
448
466
|
/**
|
|
449
467
|
* Match an SSH client IP to a registered device (pure — testable with a plain
|
|
450
468
|
* registry object). Returns the device name + ssh login user when the IP is a
|
|
@@ -581,16 +581,24 @@ export async function listTeamsActive() {
|
|
|
581
581
|
const mgr = new AgentManager();
|
|
582
582
|
const running = await mgr.listRunning();
|
|
583
583
|
return running.map((a) => {
|
|
584
|
-
|
|
585
|
-
|
|
584
|
+
// The teammate's OWN transcript is `remoteSessionId` (captured from its first
|
|
585
|
+
// stream event). `parentSessionId` is the ORCHESTRATOR that spawned the team
|
|
586
|
+
// (AGENTS_SESSION_ID at spawn) — a link, not this teammate's id. Keying the
|
|
587
|
+
// row off the orchestrator conflated the two (a teammate showed the
|
|
588
|
+
// orchestrator's id/topic and lineage was invisible); resolve the teammate's
|
|
589
|
+
// own session for the row and expose the orchestrator separately.
|
|
590
|
+
const ownSessionId = a.remoteSessionId ?? undefined;
|
|
591
|
+
const sessionFile = findSessionFileForKind(a.agentType, a.cwd ?? undefined, ownSessionId);
|
|
586
592
|
const topic = sessionFile ? quickExtractTopic(sessionFile) : undefined;
|
|
587
593
|
const pidAlive = a.pid ? isPidAlive(a.pid) : true;
|
|
588
594
|
const { state, tokPerSec } = computeLiveSignals(a.agentType, sessionFile, a.cwd ?? undefined, pidAlive);
|
|
595
|
+
const resolvedId = ownSessionId ?? sessionIdFromFile(sessionFile);
|
|
589
596
|
return applyState({
|
|
590
597
|
context: 'teams',
|
|
591
598
|
kind: a.agentType,
|
|
592
599
|
pid: a.pid ?? undefined,
|
|
593
|
-
sessionId:
|
|
600
|
+
sessionId: resolvedId,
|
|
601
|
+
orchestratorSessionId: a.parentSessionId ?? undefined,
|
|
594
602
|
cwd: a.cwd ?? undefined,
|
|
595
603
|
label: a.name ?? undefined,
|
|
596
604
|
topic,
|
|
@@ -603,7 +611,7 @@ export async function listTeamsActive() {
|
|
|
603
611
|
// The frozen actor stamped on the teammate record (RUSH-2028) — who ran
|
|
604
612
|
// this teammate, surfaced as the owner in --active (RUSH-2018); sidecar
|
|
605
613
|
// fallback for a teammate record predating the actor field.
|
|
606
|
-
owner: resolveOwner(a.actor,
|
|
614
|
+
owner: resolveOwner(a.actor, resolvedId),
|
|
607
615
|
}, state, sessionFile, pidAlive);
|
|
608
616
|
});
|
|
609
617
|
}
|
|
@@ -1287,8 +1295,28 @@ export async function getActiveSessions(opts = {}) {
|
|
|
1287
1295
|
await enrichProvenance(merged);
|
|
1288
1296
|
await resolveOrigins(merged);
|
|
1289
1297
|
foldPresence(merged);
|
|
1298
|
+
annotateOrchestratorLabels(merged);
|
|
1290
1299
|
return merged;
|
|
1291
1300
|
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Resolve each teams row's `orchestratorLabel` from the orchestrator's own row,
|
|
1303
|
+
* when that orchestrator session is itself in the active set (it usually is — the
|
|
1304
|
+
* agent that ran `agents teams add` is running). Falls back to nothing, so the
|
|
1305
|
+
* renderer shows the short id. Pure over the array; exported for tests.
|
|
1306
|
+
*/
|
|
1307
|
+
export function annotateOrchestratorLabels(sessions) {
|
|
1308
|
+
const byId = new Map();
|
|
1309
|
+
for (const s of sessions)
|
|
1310
|
+
if (s.sessionId)
|
|
1311
|
+
byId.set(s.sessionId, s);
|
|
1312
|
+
for (const s of sessions) {
|
|
1313
|
+
if (!s.orchestratorSessionId)
|
|
1314
|
+
continue;
|
|
1315
|
+
const orch = byId.get(s.orchestratorSessionId);
|
|
1316
|
+
if (orch)
|
|
1317
|
+
s.orchestratorLabel = orch.label || orch.topic || undefined;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1292
1320
|
/**
|
|
1293
1321
|
* Fold detach/attach presence onto each row from the detach store. A stored
|
|
1294
1322
|
* record wins (`background`/`parked`); otherwise a live terminal session is
|
package/dist/lib/state.js
CHANGED
|
@@ -619,6 +619,62 @@ function writeIfChanged(filePath, content) {
|
|
|
619
619
|
* All callers funnel through writeMeta → here, so nothing else changes. Empty
|
|
620
620
|
* `agents:` / `versions:` are not written (no empty committed files).
|
|
621
621
|
*/
|
|
622
|
+
/**
|
|
623
|
+
* Serialize the central (synced) meta to `agents.yaml` WITHOUT destroying the
|
|
624
|
+
* hand-written comments in the committed file.
|
|
625
|
+
*
|
|
626
|
+
* `yaml.stringify(central)` drops every comment, so the freshly-written bytes
|
|
627
|
+
* never equal the comment-annotated file on disk — `writeIfChanged`'s byte
|
|
628
|
+
* compare then rewrites on EVERY meta write, leaving `agents.yaml` perpetually
|
|
629
|
+
* dirty and wedging `agents sync` ("Blocked by local changes"). Instead we parse
|
|
630
|
+
* the existing file into a `yaml.Document` (which preserves comments + ordering)
|
|
631
|
+
* and edit only the keys that actually changed — untouched keys, and all their
|
|
632
|
+
* comments, are left byte-stable. If nothing central changed we return the exact
|
|
633
|
+
* existing bytes, so a device-field-only write no longer touches `agents.yaml` at
|
|
634
|
+
* all. Falls back to plain stringify only when the file doesn't exist yet.
|
|
635
|
+
*/
|
|
636
|
+
function serializeCentral(central) {
|
|
637
|
+
const isEmpty = Object.keys(central).length === 0;
|
|
638
|
+
let existing = null;
|
|
639
|
+
try {
|
|
640
|
+
existing = fs.readFileSync(META_FILE, 'utf-8');
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
/* first write — no file yet */
|
|
644
|
+
}
|
|
645
|
+
if (existing == null) {
|
|
646
|
+
// Empty central → header only. `yaml.stringify({})` emits `{}` (a FLOW empty
|
|
647
|
+
// map); once that lands on disk, a later parseDocument sees a flow root and
|
|
648
|
+
// doc.set() below would inherit flow, flow-ifying the whole file. Writing just
|
|
649
|
+
// the header avoids seeding that poison.
|
|
650
|
+
return isEmpty ? META_HEADER : META_HEADER + yaml.stringify(central);
|
|
651
|
+
}
|
|
652
|
+
const doc = yaml.parseDocument(existing);
|
|
653
|
+
const current = doc.toJSON() ?? {};
|
|
654
|
+
let changed = false;
|
|
655
|
+
for (const [k, v] of Object.entries(central)) {
|
|
656
|
+
if (JSON.stringify(current[k]) !== JSON.stringify(v)) {
|
|
657
|
+
doc.set(k, v);
|
|
658
|
+
changed = true;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
for (const k of Object.keys(current)) {
|
|
662
|
+
if (!(k in central)) {
|
|
663
|
+
doc.delete(k);
|
|
664
|
+
changed = true;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// No central field changed → keep the file byte-identical (comments intact), so
|
|
668
|
+
// writeIfChanged skips it and the churn loop never starts.
|
|
669
|
+
if (!changed)
|
|
670
|
+
return existing;
|
|
671
|
+
// Everything cleared → header only (never leave a flow `{}` behind).
|
|
672
|
+
// Otherwise force BLOCK style: an existing flow root (e.g. a legacy `{}`) would
|
|
673
|
+
// otherwise make the edited nodes render flow (`disabledCommands: [ teams ]`
|
|
674
|
+
// instead of a `- teams` block list). collectionStyle pins the whole doc block
|
|
675
|
+
// while parseDocument still preserves comments + key ordering.
|
|
676
|
+
return isEmpty ? META_HEADER : doc.toString({ collectionStyle: 'block' });
|
|
677
|
+
}
|
|
622
678
|
function writeMetaUnlocked(meta) {
|
|
623
679
|
const { agents, isolatedAgents, versions, defaultBrowserProfile, ...central } = meta;
|
|
624
680
|
// Write the machine-local files FIRST, then strip central — so a crash mid-write
|
|
@@ -656,7 +712,7 @@ function writeMetaUnlocked(meta) {
|
|
|
656
712
|
fs.mkdirSync(path.dirname(vrPath), { recursive: true });
|
|
657
713
|
writeIfChanged(vrPath, JSON.stringify(versions, null, 2) + '\n');
|
|
658
714
|
}
|
|
659
|
-
writeIfChanged(META_FILE,
|
|
715
|
+
writeIfChanged(META_FILE, serializeCentral(central));
|
|
660
716
|
metaCache = null;
|
|
661
717
|
}
|
|
662
718
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.83",
|
|
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",
|