@phnx-labs/agents-cli 1.20.93 → 1.21.1
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 +290 -0
- package/README.md +4 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +3 -5
- package/dist/commands/doctor.js +23 -0
- package/dist/commands/exec.js +13 -0
- package/dist/commands/feed.js +37 -23
- package/dist/commands/mailboxes.js +39 -1
- package/dist/commands/message.js +12 -1
- package/dist/commands/monitors.js +8 -6
- package/dist/commands/packages.js +14 -5
- package/dist/commands/projects.d.ts +28 -0
- package/dist/commands/projects.js +173 -47
- package/dist/commands/routines.js +27 -1
- package/dist/commands/rules.js +6 -0
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +16 -8
- package/dist/commands/setup-browser.js +5 -1
- package/dist/commands/setup-fleet.js +5 -0
- package/dist/commands/setup-preferences.d.ts +53 -0
- package/dist/commands/setup-preferences.js +142 -0
- package/dist/commands/setup.js +8 -9
- package/dist/commands/ssh.js +220 -10
- package/dist/commands/sync.d.ts +2 -2
- package/dist/commands/sync.js +4 -9
- package/dist/commands/watchdog.js +26 -3
- package/dist/index.js +22 -6
- package/dist/lib/activity.d.ts +13 -0
- package/dist/lib/activity.js +22 -2
- package/dist/lib/auto-dispatch.d.ts +6 -1
- package/dist/lib/auto-dispatch.js +7 -2
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/channels/providers/openclaw-telegram.js +1 -1
- package/dist/lib/channels/resolve.d.ts +18 -1
- package/dist/lib/channels/resolve.js +15 -5
- package/dist/lib/daemon.d.ts +13 -0
- package/dist/lib/daemon.js +80 -60
- package/dist/lib/device-config.d.ts +82 -0
- package/dist/lib/device-config.js +296 -0
- package/dist/lib/event-provenance.d.ts +19 -0
- package/dist/lib/event-provenance.js +48 -0
- package/dist/lib/events.d.ts +2 -2
- package/dist/lib/events.js +4 -53
- package/dist/lib/feed-broadcast.d.ts +41 -5
- package/dist/lib/feed-broadcast.js +110 -12
- package/dist/lib/feed-post.d.ts +16 -2
- package/dist/lib/feed-post.js +35 -7
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +3 -1
- package/dist/lib/git.d.ts +13 -2
- package/dist/lib/git.js +38 -6
- package/dist/lib/hooks.d.ts +16 -1
- package/dist/lib/hooks.js +61 -1
- package/dist/lib/linear-project-counts.d.ts +72 -5
- package/dist/lib/linear-project-counts.js +90 -6
- package/dist/lib/linear-projects.d.ts +12 -0
- package/dist/lib/linear-projects.js +30 -0
- package/dist/lib/mailbox-gc.js +30 -7
- package/dist/lib/mailbox.d.ts +14 -1
- package/dist/lib/mailbox.js +35 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +5 -21
- package/dist/lib/menubar/install-menubar.js +35 -44
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/models.js +21 -11
- package/dist/lib/monitors/config.d.ts +1 -1
- package/dist/lib/monitors/dispatch.d.ts +6 -2
- package/dist/lib/monitors/dispatch.js +10 -15
- package/dist/lib/notify.d.ts +42 -6
- package/dist/lib/notify.js +41 -32
- package/dist/lib/overdue.d.ts +1 -1
- package/dist/lib/overdue.js +54 -19
- package/dist/lib/project-import.d.ts +96 -0
- package/dist/lib/project-import.js +171 -0
- package/dist/lib/remote-agents-json.d.ts +14 -1
- package/dist/lib/remote-agents-json.js +21 -3
- package/dist/lib/routines.d.ts +53 -0
- package/dist/lib/routines.js +105 -4
- package/dist/lib/rules/run-sync.d.ts +18 -0
- package/dist/lib/rules/run-sync.js +92 -0
- package/dist/lib/scheduler.js +12 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/index.d.ts +4 -5
- package/dist/lib/secrets/index.js +4 -5
- package/dist/lib/session/active.d.ts +16 -2
- package/dist/lib/session/active.js +8 -4
- package/dist/lib/session/bundle.js +5 -1
- package/dist/lib/session/remote-list.d.ts +1 -15
- package/dist/lib/session/remote-list.js +22 -111
- package/dist/lib/session/sync/config.d.ts +13 -5
- package/dist/lib/session/sync/config.js +21 -13
- package/dist/lib/startup/command-registry.d.ts +0 -2
- package/dist/lib/startup/command-registry.js +1 -5
- package/dist/lib/state.d.ts +7 -2
- package/dist/lib/state.js +85 -10
- package/dist/lib/sync-umbrella.d.ts +11 -23
- package/dist/lib/sync-umbrella.js +14 -45
- package/dist/lib/teams/agents.d.ts +35 -3
- package/dist/lib/teams/agents.js +52 -8
- package/dist/lib/teams/scheduler.d.ts +29 -2
- package/dist/lib/teams/scheduler.js +60 -16
- package/dist/lib/types.d.ts +23 -1
- package/package.json +1 -2
- package/dist/commands/drive.d.ts +0 -10
- package/dist/commands/drive.js +0 -183
- package/dist/commands/hq.d.ts +0 -2
- package/dist/commands/hq.js +0 -58
- package/dist/commands/sessions-sync.d.ts +0 -17
- package/dist/commands/sessions-sync.js +0 -135
- package/dist/commands/sync-provision.d.ts +0 -23
- package/dist/commands/sync-provision.js +0 -107
- package/dist/lib/drive-sync.d.ts +0 -45
- package/dist/lib/drive-sync.js +0 -238
- package/dist/lib/hq/floor.d.ts +0 -87
- package/dist/lib/hq/floor.js +0 -243
- package/dist/lib/session/sync/crdt.d.ts +0 -44
- package/dist/lib/session/sync/crdt.js +0 -119
- package/dist/lib/session/sync/manifest.d.ts +0 -62
- package/dist/lib/session/sync/manifest.js +0 -100
- package/dist/lib/session/sync/provision.d.ts +0 -49
- package/dist/lib/session/sync/provision.js +0 -91
- package/dist/lib/session/sync/r2.d.ts +0 -32
- package/dist/lib/session/sync/r2.js +0 -121
- package/dist/lib/session/sync/sync.d.ts +0 -106
- package/dist/lib/session/sync/sync.js +0 -374
package/dist/commands/message.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import { die } from '../lib/format.js';
|
|
4
|
+
import { parseDuration } from '../lib/hooks/cache.js';
|
|
4
5
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
5
6
|
import { getTaskById, updateTaskStatus } from '../lib/cloud/store.js';
|
|
6
7
|
import { resolveProvider } from '../lib/cloud/registry.js';
|
|
@@ -53,6 +54,7 @@ async function deliverViaMailbox(mailboxId, text, block, opts) {
|
|
|
53
54
|
text,
|
|
54
55
|
from: opts.from,
|
|
55
56
|
blockId: block?.blockId,
|
|
57
|
+
ttlSeconds: opts.ttlSeconds,
|
|
56
58
|
});
|
|
57
59
|
if (block) {
|
|
58
60
|
recordMessageReceipt(block.blockId, {
|
|
@@ -116,10 +118,19 @@ export function registerMessageCommand(program) {
|
|
|
116
118
|
.option('--from <who>', 'Label recorded as the sender of this message')
|
|
117
119
|
.option('--as <operator>', 'Verified operator id answering a high-consequence block')
|
|
118
120
|
.option('--surface <surface>', 'Surface that is sending this answer (feed, terminal, etc.)', 'cli')
|
|
121
|
+
.option('--ttl <dur>', 'Delivery TTL if the message is not consumed (e.g. 30m, 1h, 24h); 0 disables expiry')
|
|
119
122
|
.action(async (target, text, opts) => {
|
|
120
123
|
if (!target.trim()) {
|
|
121
124
|
die('Target must be a session/agent id or cloud task id. Run `agents sessions --active` to list running agents.');
|
|
122
125
|
}
|
|
126
|
+
let ttlSeconds;
|
|
127
|
+
if (opts.ttl !== undefined) {
|
|
128
|
+
const parsed = parseDuration(opts.ttl);
|
|
129
|
+
if (parsed == null) {
|
|
130
|
+
die(`Invalid --ttl ${JSON.stringify(opts.ttl)}: expected a duration like 30m, 1h, 24h, or 0.`);
|
|
131
|
+
}
|
|
132
|
+
ttlSeconds = parsed;
|
|
133
|
+
}
|
|
123
134
|
const sessions = await getActiveSessions();
|
|
124
135
|
const res = resolveMessageTarget(target, sessions, (id) => getTaskById(id) != null);
|
|
125
136
|
switch (res.kind) {
|
|
@@ -152,7 +163,7 @@ export function registerMessageCommand(program) {
|
|
|
152
163
|
// First-answer-wins for any path that closes an open block.
|
|
153
164
|
claimBlockAnswer(block, opts);
|
|
154
165
|
if (route.kind === 'mailbox') {
|
|
155
|
-
await deliverViaMailbox(res.id, text, block, opts);
|
|
166
|
+
await deliverViaMailbox(res.id, text, block, { from: opts.from, ttlSeconds });
|
|
156
167
|
return;
|
|
157
168
|
}
|
|
158
169
|
if (route.kind === 'tmux' || route.kind === 'iterm' || route.kind === 'pty') {
|
|
@@ -57,7 +57,7 @@ function actionLabel(action) {
|
|
|
57
57
|
case 'routine':
|
|
58
58
|
return `routine ${action.routine ?? ''}`;
|
|
59
59
|
case 'notify':
|
|
60
|
-
return `notify ${action.notifyChannel ?? '
|
|
60
|
+
return `notify ${action.notifyChannel ?? 'owner'}`;
|
|
61
61
|
case 'webhook-out':
|
|
62
62
|
return `webhook-out ${action.url ?? ''}`;
|
|
63
63
|
default:
|
|
@@ -195,9 +195,11 @@ function buildAction(options) {
|
|
|
195
195
|
if (options.routine)
|
|
196
196
|
chosen.push({ type: 'routine', routine: options.routine });
|
|
197
197
|
if (options.notify !== undefined) {
|
|
198
|
-
// --notify may be a bare flag (
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
// --notify may be a bare flag (notify the owner) or carry a channel that
|
|
199
|
+
// overrides notify.owner.channel. Left unset, the send resolves the owner
|
|
200
|
+
// channel + target from notify.owner in agents.yaml (one source of truth).
|
|
201
|
+
const channel = typeof options.notify === 'string' ? options.notify : undefined;
|
|
202
|
+
chosen.push({ type: 'notify', ...(channel ? { notifyChannel: channel } : {}) });
|
|
201
203
|
}
|
|
202
204
|
if (options.webhookOut)
|
|
203
205
|
chosen.push({ type: 'webhook-out', url: options.webhookOut });
|
|
@@ -310,7 +312,7 @@ export function registerMonitorsCommands(program) {
|
|
|
310
312
|
.option('--effort <effort>', 'Reasoning effort for --run: low | medium | high | xhigh | max | auto')
|
|
311
313
|
.option('--action-timeout <t>', 'Kill the --run action if it runs longer than this (e.g. 10m)')
|
|
312
314
|
.option('--routine <name>', 'Fire an existing routine on change')
|
|
313
|
-
.option('--notify [channel]', '
|
|
315
|
+
.option('--notify [channel]', 'Notify the owner (notify.owner); [channel] overrides the owner channel')
|
|
314
316
|
.option('--webhook-out <url>', 'POST the event to this URL')
|
|
315
317
|
// PLACEMENT / hygiene
|
|
316
318
|
.option('--device <name>', 'OWNER device — the single machine that evaluates + fires (exactly-once)')
|
|
@@ -573,7 +575,7 @@ export function registerMonitorsCommands(program) {
|
|
|
573
575
|
name,
|
|
574
576
|
source: { type: 'poll', command: 'echo hello', interval: '1m' },
|
|
575
577
|
condition: { mode: 'on-change' },
|
|
576
|
-
action: { type: 'notify'
|
|
578
|
+
action: { type: 'notify' },
|
|
577
579
|
});
|
|
578
580
|
fs.writeFileSync(monitorPath, template, 'utf-8');
|
|
579
581
|
console.log(chalk.gray(`Created new monitor file: ${monitorPath}`));
|
|
@@ -14,7 +14,7 @@ import { AGENTS, getAllCliStates, agentLabel, } from '../lib/agents.js';
|
|
|
14
14
|
import { capableAgents } from '../lib/capabilities.js';
|
|
15
15
|
import { DEFAULT_REGISTRIES } from '../lib/types.js';
|
|
16
16
|
import { getRegistries, setRegistry, removeRegistry, search as searchRegistries, resolvePackage, validatedNpmSpec, validatedPyPISpec, buildSkillIndex, verifySkillIntegrity, parseOwnerRepoFromRemote, } from '../lib/registry.js';
|
|
17
|
-
import { cloneRepo, commitAndPush, getRemoteUrl, isGitRepo } from '../lib/git.js';
|
|
17
|
+
import { cloneRepo, commitAndPush, getCurrentBranch, getRemoteUrl, isGitRepo } from '../lib/git.js';
|
|
18
18
|
import { discoverCommands, resolveCommandSource, installCommand, installCommandCentrally, } from '../lib/commands.js';
|
|
19
19
|
import { discoverSkillsFromRepo, installSkill, installSkillCentrally, } from '../lib/skills.js';
|
|
20
20
|
import { discoverHooksFromRepo, installHooks, installHooksCentrally, } from '../lib/hooks.js';
|
|
@@ -335,7 +335,7 @@ When to use:
|
|
|
335
335
|
.description('Generate a skills-index.json for a git repo and push it, making its skills discoverable via agents search/install')
|
|
336
336
|
.option('--repo <alias>', 'Publish an extra repo added via `agents repo add` (default: your ~/.agents repo)')
|
|
337
337
|
.option('--name <name>', 'Registry name to suggest in the output (default: the repo name)')
|
|
338
|
-
.option('--branch <branch>', 'Branch the raw URL
|
|
338
|
+
.option('--branch <branch>', 'Branch to push the index to and reference in the raw URL (default: the repo\'s current branch)')
|
|
339
339
|
.option('--dry-run', 'Write the index and print the URL without committing or pushing')
|
|
340
340
|
.addHelpText('after', `
|
|
341
341
|
Publish walks a repo's skills/ directory, records a sha256 of every SKILL.md,
|
|
@@ -398,20 +398,29 @@ the sha256 in the index and abort on mismatch.
|
|
|
398
398
|
for (const s of index.skills) {
|
|
399
399
|
console.log(` ${chalk.cyan(s.name)} ${chalk.gray(`sha256:${s.sha256?.slice(0, 12)}…`)}`);
|
|
400
400
|
}
|
|
401
|
+
// The branch the URL references must be the branch the index actually
|
|
402
|
+
// lands on. In a real push that's whatever commitAndPush reports back
|
|
403
|
+
// (the requested --branch, else the checked-out branch); in a dry run we
|
|
404
|
+
// resolve it the same way without pushing.
|
|
405
|
+
let urlBranch;
|
|
401
406
|
if (options.dryRun) {
|
|
407
|
+
urlBranch = options.branch || (await getCurrentBranch(repoDir));
|
|
402
408
|
console.log(chalk.gray(`\nDry run — wrote ${indexPath} but did not commit or push.`));
|
|
403
409
|
}
|
|
404
410
|
else {
|
|
405
411
|
const pushSpinner = ora('Committing and pushing skills-index.json...').start();
|
|
406
|
-
const result = await commitAndPush(repoDir, 'chore: update skills-index.json (agents publish)');
|
|
412
|
+
const result = await commitAndPush(repoDir, 'chore: update skills-index.json (agents publish)', options.branch);
|
|
407
413
|
if (!result.success) {
|
|
408
414
|
pushSpinner.fail(`Push failed: ${result.error}`);
|
|
409
415
|
console.log(chalk.gray('The index was written locally — commit and push it manually to publish.'));
|
|
410
416
|
process.exit(1);
|
|
411
417
|
}
|
|
412
|
-
|
|
418
|
+
// commitAndPush always reports the pushed branch on success; the `??`
|
|
419
|
+
// only satisfies the optional return type, it is not a behavior path.
|
|
420
|
+
urlBranch = result.branch ?? (await getCurrentBranch(repoDir));
|
|
421
|
+
pushSpinner.succeed(`Pushed skills-index.json to ${urlBranch}`);
|
|
413
422
|
}
|
|
414
|
-
const rawUrl = `https://raw.githubusercontent.com/${repoSlug}/${
|
|
423
|
+
const rawUrl = `https://raw.githubusercontent.com/${repoSlug}/${urlBranch}/skills-index.json`;
|
|
415
424
|
const registryName = options.name || repoSlug.split('/')[1] || 'my-skills';
|
|
416
425
|
console.log(chalk.bold('\nPublished. Share these with anyone who wants your skills:\n'));
|
|
417
426
|
console.log(chalk.gray(' Index URL:'));
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* lifecycle state, plan completion, open PRs, and tickets in flight.
|
|
10
10
|
*/
|
|
11
11
|
import type { Command } from 'commander';
|
|
12
|
+
import { type LinearMilestone } from '../lib/linear-project-counts.js';
|
|
12
13
|
/** Recursion guard: a peer answering a probe fan-out never re-fans-out itself. */
|
|
13
14
|
export declare const PROJECTS_NO_FANOUT_ENV = "AGENTS_PROJECTS_LOCAL";
|
|
14
15
|
/**
|
|
@@ -19,4 +20,31 @@ export declare const PROJECTS_NO_FANOUT_ENV = "AGENTS_PROJECTS_LOCAL";
|
|
|
19
20
|
* string when everything answered.
|
|
20
21
|
*/
|
|
21
22
|
export declare function formatFleetSkippedNote(skipped: string[]): string;
|
|
23
|
+
/** One `projects list` row, pre-render. */
|
|
24
|
+
export interface ProjectListRow {
|
|
25
|
+
name: string;
|
|
26
|
+
path: string;
|
|
27
|
+
repo: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Column widths for `projects list`, sized to the rows actually being printed.
|
|
31
|
+
* Fixed padding was the bug: home-relative roots run past 50 characters, so a
|
|
32
|
+
* hardcoded 32 pushed the repo column off its gridline on every long path.
|
|
33
|
+
* Paths longer than {@link LIST_PATH_MAX} truncate rather than widen the table.
|
|
34
|
+
*/
|
|
35
|
+
export declare function computeProjectListWidths(rows: ProjectListRow[]): {
|
|
36
|
+
name: number;
|
|
37
|
+
path: number;
|
|
38
|
+
repo: number;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* A milestone's target date as a person would say it — "due tomorrow",
|
|
42
|
+
* "overdue by 3 days", "due Aug 21" — never a raw `2026-08-21` or a duration in
|
|
43
|
+
* hours. Linear stores a calendar date with no timezone, so both sides are
|
|
44
|
+
* compared at LOCAL midnight; parsing `YYYY-MM-DD` with `new Date(str)` would
|
|
45
|
+
* read it as UTC and shift the answer by a day for anyone west of Greenwich.
|
|
46
|
+
*/
|
|
47
|
+
export declare function formatMilestoneDue(targetDate: string, nowMs: number): string | undefined;
|
|
48
|
+
/** The `next` card line: what this project is due to hit, and how far along it is. */
|
|
49
|
+
export declare function formatNextMilestone(ms: LinearMilestone, nowMs: number): string;
|
|
22
50
|
export declare function registerProjectsCommands(program: Command): void;
|
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
13
14
|
import { execFileSync, spawnSync } from 'child_process';
|
|
14
15
|
import { betaEnableHint, isBetaEnabled } from '../lib/beta.js';
|
|
15
16
|
import { setHelpSections } from '../lib/help.js';
|
|
16
17
|
import { getMainRepoRoot } from '../lib/git.js';
|
|
17
18
|
import { parseOwnerRepoFromRemote } from '../lib/registry.js';
|
|
18
|
-
import {
|
|
19
|
+
import { truncate } from '../lib/format.js';
|
|
20
|
+
import { expandLocalHome, getProjectRoot, toHomeRelative } from '../lib/project-root.js';
|
|
19
21
|
import { machineId } from '../lib/machine-id.js';
|
|
20
22
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
21
23
|
import { gatherRemoteActive } from '../lib/session/remote-active.js';
|
|
@@ -26,6 +28,7 @@ import { listProjectDefs, loadProjectDef, writeProjectDef, removeProjectDef, pro
|
|
|
26
28
|
import { rollupSessionsByProject, planPct, enrichProjectSignals, formatProjectMembers, } from '../lib/project-status.js';
|
|
27
29
|
import { fetchLinearProjectCounts } from '../lib/linear-project-counts.js';
|
|
28
30
|
import { listLinearProjects, pickLinearProject } from '../lib/linear-projects.js';
|
|
31
|
+
import { buildFactoryImportCandidates, buildLinearImportCandidates, validateImportOpts, } from '../lib/project-import.js';
|
|
29
32
|
/** Recursion guard: a peer answering a probe fan-out never re-fans-out itself. */
|
|
30
33
|
export const PROJECTS_NO_FANOUT_ENV = 'AGENTS_PROJECTS_LOCAL';
|
|
31
34
|
/** Max peers named in the skipped note before the rest collapse to `+N`. */
|
|
@@ -63,6 +66,135 @@ function originSlug(cwd) {
|
|
|
63
66
|
return undefined;
|
|
64
67
|
}
|
|
65
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Read the Factory registry off disk and plan its import. Every read failure is
|
|
71
|
+
* fatal and named — an unreadable or malformed registry must not read as "0
|
|
72
|
+
* projects to import".
|
|
73
|
+
*/
|
|
74
|
+
function runFactoryImport(existing, opts) {
|
|
75
|
+
const src = factoryProjectsPath();
|
|
76
|
+
let rawText;
|
|
77
|
+
try {
|
|
78
|
+
rawText = fs.readFileSync(src, 'utf8');
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
console.error(chalk.red(`No Factory registry at ${src}`));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(rawText);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
console.error(chalk.red(`Factory registry at ${src} is not valid JSON`));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const rows = Array.isArray(parsed)
|
|
93
|
+
? parsed
|
|
94
|
+
: Array.isArray(parsed?.projects)
|
|
95
|
+
? parsed.projects
|
|
96
|
+
: [];
|
|
97
|
+
return buildFactoryImportCandidates(rows, existing, opts);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* List the workspace's Linear projects and plan their import, binding each to a
|
|
101
|
+
* local checkout under the configured projects root when the names match
|
|
102
|
+
* exactly. A missing / logged-out `linear` CLI is loud (this is an explicit user
|
|
103
|
+
* command), and nothing is written before the whole plan is built.
|
|
104
|
+
*/
|
|
105
|
+
function runLinearImport(existing, opts) {
|
|
106
|
+
let list;
|
|
107
|
+
try {
|
|
108
|
+
list = listLinearProjects();
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
console.error(chalk.red(e instanceof Error ? e.message : String(e)));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
// No projects root configured (or unreadable) → no local matching; every def
|
|
115
|
+
// still imports, carrying its Linear link and nothing it can't prove.
|
|
116
|
+
const rootAbs = getProjectRoot() ? expandLocalHome(getProjectRoot()) : undefined;
|
|
117
|
+
let localDirs = [];
|
|
118
|
+
if (rootAbs) {
|
|
119
|
+
try {
|
|
120
|
+
localDirs = fs
|
|
121
|
+
.readdirSync(rootAbs, { withFileTypes: true })
|
|
122
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith('.'))
|
|
123
|
+
.map((d) => d.name);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
localDirs = [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return buildLinearImportCandidates(list, existing, {
|
|
130
|
+
localDirs,
|
|
131
|
+
resolveRoot: (dir) => (rootAbs ? toHomeRelative(path.join(rootAbs, dir)) : undefined),
|
|
132
|
+
resolveOrigin: (dir) => (rootAbs ? originSlug(path.join(rootAbs, dir)) : undefined),
|
|
133
|
+
}, opts);
|
|
134
|
+
}
|
|
135
|
+
/** Longest path a `list` row shows before it truncates. */
|
|
136
|
+
const LIST_PATH_MAX = 48;
|
|
137
|
+
/**
|
|
138
|
+
* Column widths for `projects list`, sized to the rows actually being printed.
|
|
139
|
+
* Fixed padding was the bug: home-relative roots run past 50 characters, so a
|
|
140
|
+
* hardcoded 32 pushed the repo column off its gridline on every long path.
|
|
141
|
+
* Paths longer than {@link LIST_PATH_MAX} truncate rather than widen the table.
|
|
142
|
+
*/
|
|
143
|
+
export function computeProjectListWidths(rows) {
|
|
144
|
+
const widest = (pick, cap) => Math.min(cap, rows.reduce((w, r) => Math.max(w, pick(r).length), 0));
|
|
145
|
+
return {
|
|
146
|
+
name: widest((r) => r.name, 64),
|
|
147
|
+
path: widest((r) => r.path, LIST_PATH_MAX),
|
|
148
|
+
repo: widest((r) => r.repo, 64),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A milestone's target date as a person would say it — "due tomorrow",
|
|
153
|
+
* "overdue by 3 days", "due Aug 21" — never a raw `2026-08-21` or a duration in
|
|
154
|
+
* hours. Linear stores a calendar date with no timezone, so both sides are
|
|
155
|
+
* compared at LOCAL midnight; parsing `YYYY-MM-DD` with `new Date(str)` would
|
|
156
|
+
* read it as UTC and shift the answer by a day for anyone west of Greenwich.
|
|
157
|
+
*/
|
|
158
|
+
export function formatMilestoneDue(targetDate, nowMs) {
|
|
159
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(targetDate.trim());
|
|
160
|
+
if (!m)
|
|
161
|
+
return undefined;
|
|
162
|
+
const due = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
163
|
+
if (Number.isNaN(due.getTime()))
|
|
164
|
+
return undefined;
|
|
165
|
+
const now = new Date(nowMs);
|
|
166
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
167
|
+
const days = Math.round((due.getTime() - today.getTime()) / 86_400_000);
|
|
168
|
+
if (days === 0)
|
|
169
|
+
return 'due today';
|
|
170
|
+
if (days === 1)
|
|
171
|
+
return 'due tomorrow';
|
|
172
|
+
if (days === -1)
|
|
173
|
+
return 'overdue by a day';
|
|
174
|
+
if (days < 0)
|
|
175
|
+
return `overdue by ${-days} days`;
|
|
176
|
+
if (days <= 14)
|
|
177
|
+
return `due in ${days} days`;
|
|
178
|
+
const sameYear = due.getFullYear() === today.getFullYear();
|
|
179
|
+
const label = due.toLocaleDateString('en-US', {
|
|
180
|
+
month: 'short',
|
|
181
|
+
day: 'numeric',
|
|
182
|
+
...(sameYear ? {} : { year: 'numeric' }),
|
|
183
|
+
});
|
|
184
|
+
return `due ${label}`;
|
|
185
|
+
}
|
|
186
|
+
/** The `next` card line: what this project is due to hit, and how far along it is. */
|
|
187
|
+
export function formatNextMilestone(ms, nowMs) {
|
|
188
|
+
const parts = [chalk.bold(ms.name)];
|
|
189
|
+
// A milestone with nothing filed under it yet has no progress to report —
|
|
190
|
+
// `0/0` is noise, not information.
|
|
191
|
+
if (ms.total > 0)
|
|
192
|
+
parts.push(`${ms.done}/${ms.total}`);
|
|
193
|
+
const due = ms.targetDate ? formatMilestoneDue(ms.targetDate, nowMs) : undefined;
|
|
194
|
+
if (due)
|
|
195
|
+
parts.push(due.startsWith('overdue') ? chalk.yellow(due) : chalk.dim(due));
|
|
196
|
+
return parts.join(chalk.dim(' · '));
|
|
197
|
+
}
|
|
66
198
|
function statusBar(r) {
|
|
67
199
|
const parts = [];
|
|
68
200
|
const push = (n, label, color) => {
|
|
@@ -80,7 +212,7 @@ function statusBar(r) {
|
|
|
80
212
|
parts.push(chalk.gray(`+${r.agents - shown} other`));
|
|
81
213
|
return parts.join(' · ') || chalk.gray('no live agents');
|
|
82
214
|
}
|
|
83
|
-
function renderCard(def, r, remote, fleet, linear) {
|
|
215
|
+
function renderCard(def, r, remote, fleet, linear, nowMs = Date.now()) {
|
|
84
216
|
const agents = r?.agents ?? 0;
|
|
85
217
|
const pct = r ? planPct(r.plan) : undefined;
|
|
86
218
|
const planStr = pct === undefined ? '' : ` · ${chalk.cyan(`${pct}% plan`)}`;
|
|
@@ -104,6 +236,9 @@ function renderCard(def, r, remote, fleet, linear) {
|
|
|
104
236
|
if (linear) {
|
|
105
237
|
console.log(` ${chalk.dim('linear')} ${linear.done}/${linear.total}${linear.truncated ? '+' : ''} done · ${linear.inProgress} in progress`);
|
|
106
238
|
}
|
|
239
|
+
if (linear?.nextMilestone) {
|
|
240
|
+
console.log(` ${chalk.dim('next')} ${formatNextMilestone(linear.nextMilestone, nowMs)}`);
|
|
241
|
+
}
|
|
107
242
|
if (r && r.tickets.length) {
|
|
108
243
|
console.log(` ${chalk.dim('tickets')} ${r.tickets.slice(0, 8).join(' · ')}${r.tickets.length > 8 ? ' …' : ''}`);
|
|
109
244
|
}
|
|
@@ -138,6 +273,7 @@ export function registerProjectsCommands(program) {
|
|
|
138
273
|
.description('Named multi-repo projects with a progress rollup.');
|
|
139
274
|
setHelpSections(projects, {
|
|
140
275
|
examples: `
|
|
276
|
+
agents projects import --from-linear # the projects you actually track
|
|
141
277
|
agents projects add rush --repo phnx-labs/rush --path apps/web
|
|
142
278
|
agents projects list
|
|
143
279
|
agents projects status # progress card for every project
|
|
@@ -149,6 +285,11 @@ export function registerProjectsCommands(program) {
|
|
|
149
285
|
notes: `
|
|
150
286
|
Definitions are hand-editable YAML in ~/.agents/projects/ and sync across
|
|
151
287
|
machines with 'agents push/pull'. Enable with: agents beta enable projects.
|
|
288
|
+
|
|
289
|
+
'import --from-factory' reads Factory's auto-detected registry, which
|
|
290
|
+
guesses from checkouts on disk — it imports only 'high' confidence rows by
|
|
291
|
+
default. Widen with --min-confidence medium or --all, and drop a bad guess
|
|
292
|
+
with 'agents projects rm <name>'.
|
|
152
293
|
`,
|
|
153
294
|
});
|
|
154
295
|
projects.hook('preAction', (_thisCommand, actionCommand) => {
|
|
@@ -180,10 +321,16 @@ export function registerProjectsCommands(program) {
|
|
|
180
321
|
return;
|
|
181
322
|
}
|
|
182
323
|
const roll = rollupSessionsByProject(defs, await getActiveSessions());
|
|
183
|
-
|
|
324
|
+
const rows = defs.map((d) => ({
|
|
325
|
+
name: d.name,
|
|
326
|
+
path: truncate(d.root ?? d.defaultPath ?? '', LIST_PATH_MAX),
|
|
327
|
+
repo: d.repo ?? d.repos?.[0]?.slug ?? '',
|
|
328
|
+
}));
|
|
329
|
+
const w = computeProjectListWidths(rows);
|
|
330
|
+
for (const [i, d] of defs.entries()) {
|
|
184
331
|
const agents = roll.get(d.name)?.agents ?? 0;
|
|
185
|
-
const
|
|
186
|
-
console.log(` ${chalk.bold(
|
|
332
|
+
const row = rows[i];
|
|
333
|
+
console.log(` ${chalk.bold(row.name.padEnd(w.name))} ${chalk.dim(row.path.padEnd(w.path))} ${chalk.cyan(row.repo.padEnd(w.repo))} ${agents} agents`);
|
|
187
334
|
}
|
|
188
335
|
});
|
|
189
336
|
// ---- add ----
|
|
@@ -384,7 +531,7 @@ export function registerProjectsCommands(program) {
|
|
|
384
531
|
return;
|
|
385
532
|
}
|
|
386
533
|
for (const d of defs) {
|
|
387
|
-
renderCard(d, roll.get(d.name), remote.get(d.name), opts.fleet ? fleetFor(d) : undefined, linear.get(d.name));
|
|
534
|
+
renderCard(d, roll.get(d.name), remote.get(d.name), opts.fleet ? fleetFor(d) : undefined, linear.get(d.name), nowMs);
|
|
388
535
|
}
|
|
389
536
|
if (fleetSkipped.length > 0)
|
|
390
537
|
process.stdout.write(formatFleetSkippedNote(fleetSkipped));
|
|
@@ -402,54 +549,33 @@ export function registerProjectsCommands(program) {
|
|
|
402
549
|
// ---- import ----
|
|
403
550
|
projects
|
|
404
551
|
.command('import')
|
|
405
|
-
.description('
|
|
406
|
-
.
|
|
552
|
+
.description('Import project definitions from Linear (preferred) or the Factory auto-detection registry.')
|
|
553
|
+
.option('--from-linear', 'Import the workspace\'s Linear projects (via the `linear` CLI)')
|
|
554
|
+
.option('--from-factory', 'Import the Factory registry (~/.agents/factory/projects.json)')
|
|
555
|
+
.option('--min-confidence <level>', '--from-factory only: lowest detection confidence to import — low|medium|high (default: high)')
|
|
556
|
+
.option('--all', '--from-factory only: import every row regardless of confidence')
|
|
407
557
|
.option('--force', 'Overwrite existing definitions')
|
|
408
|
-
.action((
|
|
409
|
-
|
|
410
|
-
let rawText;
|
|
411
|
-
try {
|
|
412
|
-
rawText = fs.readFileSync(src, 'utf8');
|
|
413
|
-
}
|
|
414
|
-
catch {
|
|
415
|
-
console.error(chalk.red(`No Factory registry at ${src}`));
|
|
416
|
-
process.exit(1);
|
|
417
|
-
}
|
|
418
|
-
let rows;
|
|
558
|
+
.action((raw) => {
|
|
559
|
+
let opts;
|
|
419
560
|
try {
|
|
420
|
-
|
|
561
|
+
opts = validateImportOpts(raw);
|
|
421
562
|
}
|
|
422
|
-
catch {
|
|
423
|
-
console.error(chalk.red(
|
|
563
|
+
catch (e) {
|
|
564
|
+
console.error(chalk.red(e instanceof Error ? e.message : String(e)));
|
|
424
565
|
process.exit(1);
|
|
425
566
|
}
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
for (const raw of list) {
|
|
430
|
-
if (!raw || typeof raw !== 'object')
|
|
431
|
-
continue;
|
|
432
|
-
const o = raw;
|
|
433
|
-
const name = typeof o.name === 'string' ? o.name : undefined;
|
|
434
|
-
if (!name || !isSafeProjectName(name)) {
|
|
435
|
-
skipped++;
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
if (loadProjectDef(name) && !opts.force) {
|
|
439
|
-
skipped++;
|
|
440
|
-
continue;
|
|
441
|
-
}
|
|
442
|
-
const def = { name };
|
|
443
|
-
if (typeof o.path === 'string')
|
|
444
|
-
def.root = toHomeRelative(o.path);
|
|
445
|
-
if (typeof o.repoSlug === 'string')
|
|
446
|
-
def.repo = o.repoSlug;
|
|
447
|
-
if (typeof o.linearProjectId === 'string')
|
|
448
|
-
def.linear = { projectId: o.linearProjectId };
|
|
567
|
+
const existing = new Map(listProjectDefs().map((d) => [d.name, d]));
|
|
568
|
+
const result = opts.source === 'linear' ? runLinearImport(existing, opts) : runFactoryImport(existing, opts);
|
|
569
|
+
for (const def of result.defs)
|
|
449
570
|
writeProjectDef(def);
|
|
450
|
-
|
|
571
|
+
const n = result.defs.length;
|
|
572
|
+
const s = result.skipped.length;
|
|
573
|
+
console.log(chalk.green(`Imported ${n} project${n === 1 ? '' : 's'}${s ? chalk.gray(` (${s} skipped)`) : ''}`));
|
|
574
|
+
for (const skip of result.skipped)
|
|
575
|
+
console.log(chalk.gray(` skip ${skip.name}: ${skip.reason}`));
|
|
576
|
+
if (opts.source === 'factory' && s > 0 && opts.minConfidence === 'high') {
|
|
577
|
+
console.log(chalk.gray(' (widen with --min-confidence medium or --all)'));
|
|
451
578
|
}
|
|
452
|
-
console.log(chalk.green(`Imported ${created} project${created === 1 ? '' : 's'}${skipped ? chalk.gray(` (${skipped} skipped)`) : ''}`));
|
|
453
579
|
});
|
|
454
580
|
// ---- link ----
|
|
455
581
|
projects
|
|
@@ -11,6 +11,7 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
13
|
import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemonLog, getDaemonStatus, } from '../lib/daemon.js';
|
|
14
|
+
import { assertSchedulerEnabled } from '../lib/device-config.js';
|
|
14
15
|
import { resolveAgentName, isAgentHardDeprecated, hardDeprecationError } from '../lib/agents.js';
|
|
15
16
|
import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
|
|
16
17
|
import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, getLatestRun, getRunDir, getJobPath, parseAtTime, hasCompletedOneShotRun, isOneShotLikeSchedule, isOneShotRoutine, isPastOneShotRoutine, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, parseHostStrategy, resolveHostStrategy, placementRequiresFiringPin, HOST_STRATEGIES, } from '../lib/routines.js';
|
|
@@ -295,9 +296,21 @@ function parseRoutineTrigger(options) {
|
|
|
295
296
|
/**
|
|
296
297
|
* Start or reload the background scheduler so newly-added jobs fire on time.
|
|
297
298
|
* `quiet` suppresses human status lines for JSON callers.
|
|
299
|
+
*
|
|
300
|
+
* When this device has `scheduler.enabled=false` the auto-start is skipped with
|
|
301
|
+
* the stated reason (the add itself already succeeded — the job is config and
|
|
302
|
+
* stays valid fleet-wide); the refusal message names the setting and the fix.
|
|
298
303
|
*/
|
|
299
304
|
function ensureSchedulerRunning(opts = {}) {
|
|
300
305
|
const log = opts.stderr ? console.error : console.log;
|
|
306
|
+
try {
|
|
307
|
+
assertSchedulerEnabled();
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
// Loud stated skip, on stderr so --json stdout stays clean.
|
|
311
|
+
console.error(chalk.yellow(err.message));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
301
314
|
if (isDaemonRunning()) {
|
|
302
315
|
signalDaemonReload();
|
|
303
316
|
if (!opts.quiet)
|
|
@@ -1480,9 +1493,22 @@ export function registerRoutinesCommands(program) {
|
|
|
1480
1493
|
.command('start')
|
|
1481
1494
|
.description('Start the background scheduler. Usually unnecessary — it auto-starts when you add your first routine.')
|
|
1482
1495
|
.action(() => {
|
|
1496
|
+
try {
|
|
1497
|
+
// A manual start on a scheduler-disabled device refuses with the same
|
|
1498
|
+
// message the auto-start surfaces give.
|
|
1499
|
+
assertSchedulerEnabled();
|
|
1500
|
+
}
|
|
1501
|
+
catch (err) {
|
|
1502
|
+
console.error(chalk.red(err.message));
|
|
1503
|
+
process.exit(1);
|
|
1504
|
+
}
|
|
1483
1505
|
const result = startDaemon();
|
|
1484
1506
|
if (result.method === 'already-running') {
|
|
1485
|
-
|
|
1507
|
+
// Signal a reload even here: if the daemon booted while this device had
|
|
1508
|
+
// scheduler.enabled=false, the reload re-evaluates the gate and boots
|
|
1509
|
+
// the scheduler — a manual start heals a scheduler-less daemon.
|
|
1510
|
+
signalDaemonReload();
|
|
1511
|
+
console.log(chalk.yellow(`Scheduler already running (PID: ${result.pid}) — reloaded`));
|
|
1486
1512
|
}
|
|
1487
1513
|
else if (result.pid) {
|
|
1488
1514
|
console.log(chalk.green(`Scheduler started (PID: ${result.pid})`));
|
package/dist/commands/rules.js
CHANGED
|
@@ -512,6 +512,12 @@ Examples:
|
|
|
512
512
|
.addHelpText('after', `
|
|
513
513
|
Targets are <agent>@<version>. Use 'default' for the alias of the global default version.
|
|
514
514
|
|
|
515
|
+
This re-syncs the version home immediately, AND the choice sticks: every later
|
|
516
|
+
'agents run' for this agent+version re-applies the active preset before launch
|
|
517
|
+
(skip-fast when nothing changed), so a preset set here never goes stale even if
|
|
518
|
+
something changes it again without running 'switch' — you don't need to re-run
|
|
519
|
+
'switch' or 'sync' by hand before the next launch.
|
|
520
|
+
|
|
515
521
|
Examples:
|
|
516
522
|
# Persist the cautious preset for claude 2.1.111
|
|
517
523
|
agents rules switch claude@2.1.111 --preset cautious
|
|
@@ -315,6 +315,13 @@ export declare function isBareBrowserListing(options: SessionsOptions, query: st
|
|
|
315
315
|
* `runSessionBrowser` picker cannot represent.
|
|
316
316
|
*/
|
|
317
317
|
export declare function hasNoBrowserDisqualifyingFlags(options: SessionsOptions, query: string | undefined): boolean;
|
|
318
|
+
/** Resolve a session by id/query globally and print its compact preview (no pager).
|
|
319
|
+
* Backs `--preview` — the fast path for the "peek before resume" hot loop. */
|
|
320
|
+
export declare function renderSessionPreview(query: string, scope: {
|
|
321
|
+
agent?: string;
|
|
322
|
+
project?: string;
|
|
323
|
+
local?: boolean;
|
|
324
|
+
}): Promise<void>;
|
|
318
325
|
/**
|
|
319
326
|
* The one-line live status banner shown above a session preview: the glyph, the
|
|
320
327
|
* status word, and — when the session needs a human or has LOST one — a plain
|
|
@@ -351,6 +358,16 @@ export declare function teamBadge(session: SessionMeta): {
|
|
|
351
358
|
* when `showTicket` — otherwise a listing with no refs would waste a column of
|
|
352
359
|
* dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
|
|
353
360
|
export declare function flatSessionRow(session: SessionMeta, live?: ActiveSession, showTicket?: boolean, cols?: PickerColumns, favorite?: boolean): string;
|
|
361
|
+
/**
|
|
362
|
+
* Live-session index for enriching the default listing, or undefined when
|
|
363
|
+
* enrichment is off (`--no-live`) or irrelevant (`--json`, which serializes
|
|
364
|
+
* SessionMeta). Full detection (incl. the headless `ps` scan) is deliberate:
|
|
365
|
+
* bare-CLI and tmux agents are the common case here, and skipping them would
|
|
366
|
+
* leave the glyph almost never showing. The listing is a one-shot user action,
|
|
367
|
+
* not a hot loop, so the `ps`/`lsof` cost is acceptable; `--no-live` is the
|
|
368
|
+
* escape hatch. Never throws — a probe failure just yields a plain listing.
|
|
369
|
+
*/
|
|
370
|
+
export declare function maybeLiveIndex(options: SessionsOptions): Promise<Map<string, ActiveSession> | undefined>;
|
|
354
371
|
/**
|
|
355
372
|
* Group key for the overview: resolve the cwd through the same canonical
|
|
356
373
|
* resolver the `agents activity` timeline groups by — a defined project's name
|