@phnx-labs/agents-cli 1.22.12 → 1.22.13
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 +6 -0
- package/README.md +5 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/sessions.d.ts +15 -0
- package/dist/commands/sessions.js +79 -10
- package/dist/commands/workflows.js +1 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/resources/workflows.js +41 -21
- 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/workflows.d.ts +22 -4
- package/dist/lib/workflows.js +79 -29
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.13
|
|
4
|
+
|
|
5
|
+
- **`agents sessions` accepts direct live-state flags and remains fleet-wide by default.** `--working`, `--idle`, `--waiting`, `--orphan`/`--orphaned`, `--crashed`, `--closed`, `--abandoned`, `--queued`, and `--unknown` each imply the live scan; multiple flags form a union. `--working` is narrower than `--active`: it excludes idle, waiting, and lifecycle-failure rows. Cross-device collection was already the default and stays that way; `--local` opts out, while `--all` continues to widen historical directory and time scope. Source: `apps/cli/src/commands/sessions.ts`, `apps/cli/src/commands/sessions.test.ts`.
|
|
6
|
+
|
|
7
|
+
- **Workflows: `name@source` disambiguation (Phase 5 packaging).** When two plugins (or a plugin and an extra repo) ship the same workflow name, pin the source: `agents run deploy@ship-tools` or `agents run workflow:deploy@social`. Bare names keep layered precedence (project > user > plugin > extra > system); a missing source returns no match instead of silently falling back. Source: `apps/cli/src/lib/workflows.ts`, `apps/cli/src/lib/resources/workflows.ts`.
|
|
8
|
+
|
|
3
9
|
## 1.22.12
|
|
4
10
|
|
|
5
11
|
- Store operational events in daily history directories, retain 7 days and at most 50 MiB automatically, and make `agents logs audit` use the `agents events --audit` reader.
|
package/README.md
CHANGED
|
@@ -292,6 +292,10 @@ Search is the past tense. `--active` is the present -- it infers what each runni
|
|
|
292
292
|
|
|
293
293
|
```bash
|
|
294
294
|
agents sessions --active # every live run across the fleet, with state
|
|
295
|
+
agents sessions --working # actively producing work (fleet-wide)
|
|
296
|
+
agents sessions --idle # stopped between turns (fleet-wide)
|
|
297
|
+
agents sessions --orphan # agent outlived its terminal client
|
|
298
|
+
agents sessions --crashed # terminal and agent disappeared uncleanly
|
|
295
299
|
agents sessions focus a1b2c3d4 # jump back into one — attach in place, or resume
|
|
296
300
|
```
|
|
297
301
|
|
|
@@ -322,7 +326,7 @@ Filters **stack** (they AND together), the active set shows in the header, and t
|
|
|
322
326
|
| --- | --- |
|
|
323
327
|
|  |  |
|
|
324
328
|
|
|
325
|
-
Each live session resolves to `working`, `waiting_input` (with why -- a question, a plan review, or a permission prompt), or `
|
|
329
|
+
Each live session resolves to `working`, `waiting_input` (with why -- a question, a plan review, or a permission prompt), `idle`, or a lifecycle state such as `orphaned`, `crashed`, `closed`, `abandoned`, `queued`, or `unknown`. Pass the matching flag (`--working`, `--idle`, `--waiting`, `--orphan`, `--crashed`, `--closed`, `--abandoned`, `--queued`, `--unknown`) directly; each implies `--active`, and several flags form a union. The fleet fan-out is already the default; `--local` opts out. `--all` instead widens historical directory and time scope. Rows also carry badges for the PR, worktree, and ticket. `agents sessions focus [id]` attaches the live pane in place -- the tmux split locally or over SSH, or its Ghostty tab -- and falls back to a fresh tab + resume when the terminal is gone.
|
|
326
330
|
|
|
327
331
|
Landing on a session cold? `agents sessions <id>` prints a catch-up digest: an inferred title, files changed grouped by directory (created / modified / deleted), a histogram of which tools did the work (including parsed Bash commands -- `git`, `npm`, `ffmpeg`, `ssh`, and so on), and the last test verdict -- the signals to reload a task in seconds.
|
|
328
332
|
|
package/dist/bin/agents
CHANGED
|
Binary file
|
|
@@ -46,6 +46,16 @@ interface SessionsOptions extends SessionFilterOptions {
|
|
|
46
46
|
flat?: boolean;
|
|
47
47
|
/** With --active: show only sessions waiting on user input; exit 1 if any. */
|
|
48
48
|
waiting?: boolean;
|
|
49
|
+
/** Live-state shorthand filters. Any one implies --active; several compose as OR. */
|
|
50
|
+
working?: boolean;
|
|
51
|
+
idle?: boolean;
|
|
52
|
+
orphan?: boolean;
|
|
53
|
+
orphaned?: boolean;
|
|
54
|
+
crashed?: boolean;
|
|
55
|
+
closed?: boolean;
|
|
56
|
+
abandoned?: boolean;
|
|
57
|
+
queued?: boolean;
|
|
58
|
+
unknown?: boolean;
|
|
49
59
|
/** Show only favorited (starred) sessions — the `f` key's flag twin. */
|
|
50
60
|
favorites?: boolean;
|
|
51
61
|
/** Enrich the listing with live glyphs/preview for running rows. Default on;
|
|
@@ -311,6 +321,11 @@ export declare function gatherActiveSessions(opts?: {
|
|
|
311
321
|
sessions: ActiveSession[];
|
|
312
322
|
remoteDeviceCount: number;
|
|
313
323
|
}>;
|
|
324
|
+
export type LiveStatusFilter = 'working' | 'idle' | 'waiting' | 'orphaned' | 'crashed' | 'closed' | 'abandoned' | 'queued' | 'unknown';
|
|
325
|
+
/** Match the status words users see, preserving activity's richer working signal. */
|
|
326
|
+
export declare function matchesLiveStatus(session: ActiveSession, status: LiveStatusFilter): boolean;
|
|
327
|
+
/** Resolve convenience flags once. Multiple flags intentionally form a union. */
|
|
328
|
+
export declare function requestedLiveStatuses(options: SessionsOptions): LiveStatusFilter[];
|
|
314
329
|
/**
|
|
315
330
|
* A bare interactive fleet listing — no query, no render/filter flag — that the
|
|
316
331
|
* `runSessionBrowser` picker can represent. The single predicate shared by the
|
|
@@ -1091,9 +1091,12 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
1091
1091
|
const merged = opts.favoritesOnly
|
|
1092
1092
|
? gathered.sessions.filter((s) => !!s.sessionId && listFavorites().has(s.sessionId))
|
|
1093
1093
|
: gathered.sessions;
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
const
|
|
1094
|
+
// Status flags form a union. --waiting additionally retains its scriptable
|
|
1095
|
+
// gate: exit non-zero when the union contains a session awaiting the user.
|
|
1096
|
+
const statusFiltered = opts.statuses?.length
|
|
1097
|
+
? merged.filter((session) => opts.statuses.some((status) => matchesLiveStatus(session, status)))
|
|
1098
|
+
: merged;
|
|
1099
|
+
const sessions = statusFiltered;
|
|
1097
1100
|
if (asJson) {
|
|
1098
1101
|
// Resolve who is watching each local tmux pane before serializing: `viewingIn`
|
|
1099
1102
|
// is how a consumer distinguishes a session someone is looking at from one
|
|
@@ -1101,7 +1104,7 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
1101
1104
|
// scriptable path stays cheap — see enrichTmuxLocators.
|
|
1102
1105
|
await enrichTmuxLocators(sessions.filter(s => !s.machine || s.machine === self));
|
|
1103
1106
|
process.stdout.write(JSON.stringify(serializeActiveSessionsForJson(sessions), null, 2) + '\n');
|
|
1104
|
-
if (waitingOnly && sessions.
|
|
1107
|
+
if (waitingOnly && sessions.some(isAwaitingUser))
|
|
1105
1108
|
process.exitCode = 1;
|
|
1106
1109
|
return;
|
|
1107
1110
|
}
|
|
@@ -1136,9 +1139,40 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
1136
1139
|
if (!opts.local && !opts.hosts?.length && remoteDeviceCount === 0)
|
|
1137
1140
|
printCrossMachineTip();
|
|
1138
1141
|
// Scriptable gate: a non-zero exit when anything is waiting on the user.
|
|
1139
|
-
if (waitingOnly && sessions.
|
|
1142
|
+
if (waitingOnly && sessions.some(isAwaitingUser))
|
|
1140
1143
|
process.exitCode = 1;
|
|
1141
1144
|
}
|
|
1145
|
+
/** Match the status words users see, preserving activity's richer working signal. */
|
|
1146
|
+
export function matchesLiveStatus(session, status) {
|
|
1147
|
+
if (status === 'working')
|
|
1148
|
+
return session.activity === 'working' || (!session.activity && session.status === 'running');
|
|
1149
|
+
if (status === 'waiting')
|
|
1150
|
+
return isAwaitingUser(session);
|
|
1151
|
+
return session.status === status;
|
|
1152
|
+
}
|
|
1153
|
+
/** Resolve convenience flags once. Multiple flags intentionally form a union. */
|
|
1154
|
+
export function requestedLiveStatuses(options) {
|
|
1155
|
+
const statuses = [];
|
|
1156
|
+
if (options.working)
|
|
1157
|
+
statuses.push('working');
|
|
1158
|
+
if (options.idle)
|
|
1159
|
+
statuses.push('idle');
|
|
1160
|
+
if (options.waiting)
|
|
1161
|
+
statuses.push('waiting');
|
|
1162
|
+
if (options.orphan || options.orphaned)
|
|
1163
|
+
statuses.push('orphaned');
|
|
1164
|
+
if (options.crashed)
|
|
1165
|
+
statuses.push('crashed');
|
|
1166
|
+
if (options.closed)
|
|
1167
|
+
statuses.push('closed');
|
|
1168
|
+
if (options.abandoned)
|
|
1169
|
+
statuses.push('abandoned');
|
|
1170
|
+
if (options.queued)
|
|
1171
|
+
statuses.push('queued');
|
|
1172
|
+
if (options.unknown)
|
|
1173
|
+
statuses.push('unknown');
|
|
1174
|
+
return [...new Set(statuses)];
|
|
1175
|
+
}
|
|
1142
1176
|
/** Nudge shown when `--active` has no other machines to fold in. */
|
|
1143
1177
|
function printCrossMachineTip() {
|
|
1144
1178
|
console.log(chalk.gray("\nTip: include sessions from your other machines — register them with 'ag devices sync', then rerun. Use --local to skip."));
|
|
@@ -1204,6 +1238,22 @@ function canonicalSessionsCommand(query, options) {
|
|
|
1204
1238
|
const a = ['sessions'];
|
|
1205
1239
|
if (options.active)
|
|
1206
1240
|
a.push('--active');
|
|
1241
|
+
if (options.working)
|
|
1242
|
+
a.push('--working');
|
|
1243
|
+
if (options.idle)
|
|
1244
|
+
a.push('--idle');
|
|
1245
|
+
if (options.orphan || options.orphaned)
|
|
1246
|
+
a.push('--orphan');
|
|
1247
|
+
if (options.crashed)
|
|
1248
|
+
a.push('--crashed');
|
|
1249
|
+
if (options.closed)
|
|
1250
|
+
a.push('--closed');
|
|
1251
|
+
if (options.abandoned)
|
|
1252
|
+
a.push('--abandoned');
|
|
1253
|
+
if (options.queued)
|
|
1254
|
+
a.push('--queued');
|
|
1255
|
+
if (options.unknown)
|
|
1256
|
+
a.push('--unknown');
|
|
1207
1257
|
if (options.teams)
|
|
1208
1258
|
a.push('--teams');
|
|
1209
1259
|
if (options.inTeam)
|
|
@@ -1421,6 +1471,8 @@ async function sessionsAction(query, options,
|
|
|
1421
1471
|
*/
|
|
1422
1472
|
limitSource) {
|
|
1423
1473
|
const queryClauses = options.query ?? [];
|
|
1474
|
+
const liveStatuses = requestedLiveStatuses(options);
|
|
1475
|
+
const liveOnly = options.active === true || liveStatuses.length > 0;
|
|
1424
1476
|
const toolOnly = options.include?.split(',').map((role) => role.trim()).filter(Boolean).join(',') === 'tools';
|
|
1425
1477
|
const toolEvidenceMode = toolOnly;
|
|
1426
1478
|
if (options.count && !toolOnly) {
|
|
@@ -1551,7 +1603,7 @@ limitSource) {
|
|
|
1551
1603
|
// keeps the legacy per-host stream (each remote's raw stdout under a
|
|
1552
1604
|
// `── host ──` banner). With --active, the hosts are folded into the merged
|
|
1553
1605
|
// machine-grouped view instead (handled below).
|
|
1554
|
-
if (options.host && options.host.length > 0 && !
|
|
1606
|
+
if (options.host && options.host.length > 0 && !liveOnly && !toolEvidenceMode) {
|
|
1555
1607
|
// --local means "skip the SSH fan-out"; --host means "look only over there".
|
|
1556
1608
|
// Together they ask for a peer's sessions without dialing the peer, which can
|
|
1557
1609
|
// only ever be empty — so say that instead of rendering a blank list.
|
|
@@ -1591,7 +1643,7 @@ limitSource) {
|
|
|
1591
1643
|
await renderSessionPreview(query, { agent: options.agent, project: options.project, local: options.local });
|
|
1592
1644
|
return;
|
|
1593
1645
|
}
|
|
1594
|
-
if (
|
|
1646
|
+
if (liveOnly) {
|
|
1595
1647
|
// The running view is built from the live scan, which carries no team lineage
|
|
1596
1648
|
// (that comes off the transcript index and the teams meta dir), so --in-team
|
|
1597
1649
|
// has nothing to match on here. Say so rather than ignoring the flag.
|
|
@@ -1606,7 +1658,7 @@ limitSource) {
|
|
|
1606
1658
|
// --since seeds the window; --until / --project (no browser field) or a
|
|
1607
1659
|
// multi-host scope fall through to the static dump that already honors them.
|
|
1608
1660
|
if (useInteractiveBrowser(options) &&
|
|
1609
|
-
|
|
1661
|
+
liveStatuses.length === 0 &&
|
|
1610
1662
|
!options.until &&
|
|
1611
1663
|
!options.project &&
|
|
1612
1664
|
!options.sort &&
|
|
@@ -1630,6 +1682,7 @@ limitSource) {
|
|
|
1630
1682
|
local: forceLocal,
|
|
1631
1683
|
hosts: options.host,
|
|
1632
1684
|
favoritesOnly: options.favorites === true,
|
|
1685
|
+
statuses: liveStatuses,
|
|
1633
1686
|
});
|
|
1634
1687
|
return;
|
|
1635
1688
|
}
|
|
@@ -3617,7 +3670,16 @@ export function registerSessionsCommands(program) {
|
|
|
3617
3670
|
.option('--active', 'Show only sessions running right now across terminals, teams, cloud, and headless agents')
|
|
3618
3671
|
.option('--roots', 'With --json: emit the on-disk directories scanned for session transcripts, per agent (for external watchers)')
|
|
3619
3672
|
.option('--local', 'Only this machine — skip the cross-machine SSH fan-out (default listing and --active)')
|
|
3620
|
-
.option('--
|
|
3673
|
+
.option('--working', 'Show live sessions currently doing work (implies --active)')
|
|
3674
|
+
.option('--idle', 'Show live sessions that have stopped between turns (implies --active)')
|
|
3675
|
+
.option('--waiting', 'Show live sessions waiting on your input; exits non-zero if any (implies --active)')
|
|
3676
|
+
.option('--orphan', 'Show live sessions whose process outlived its terminal client (implies --active)')
|
|
3677
|
+
.option('--orphaned', 'Alias for --orphan')
|
|
3678
|
+
.option('--crashed', 'Show sessions whose terminal disappeared with the process (implies --active)')
|
|
3679
|
+
.option('--closed', 'Show recently observed sessions whose process exited normally (implies --active)')
|
|
3680
|
+
.option('--abandoned', 'Show sessions with no transcript progress for the abandonment window (implies --active)')
|
|
3681
|
+
.option('--queued', 'Show queued sessions that have not started running (implies --active)')
|
|
3682
|
+
.option('--unknown', 'Show sessions whose live state cannot be determined (implies --active)')
|
|
3621
3683
|
.option('--favorites', 'Show only favorited (starred) sessions — star them with `*` in the browser or `agents sessions favorite <id>`')
|
|
3622
3684
|
.option('--tree', 'Group the listing by directory; drops the id/version columns for readability')
|
|
3623
3685
|
.option('--flat', 'Plain flat table (one row per session) instead of the grouped project overview')
|
|
@@ -3645,6 +3707,12 @@ export function registerSessionsCommands(program) {
|
|
|
3645
3707
|
# Show only what's running right now (terminals, teams, cloud, headless)
|
|
3646
3708
|
agents sessions --active
|
|
3647
3709
|
|
|
3710
|
+
# Filter the live fleet by the status word shown in the roster
|
|
3711
|
+
agents sessions --working
|
|
3712
|
+
agents sessions --idle
|
|
3713
|
+
agents sessions --orphan
|
|
3714
|
+
agents sessions --crashed
|
|
3715
|
+
|
|
3648
3716
|
# --- Session lifecycle (one verb per intent) ---
|
|
3649
3717
|
# Jump to a live session (attach its terminal, or open a tab + resume)
|
|
3650
3718
|
agents sessions focus a1b2c3d4
|
|
@@ -3703,7 +3771,8 @@ export function registerSessionsCommands(program) {
|
|
|
3703
3771
|
detach <id> interactive → headless continuation
|
|
3704
3772
|
attach <id> headless → interactive in this terminal
|
|
3705
3773
|
resume [query] multi-select history → open tabs (or run --resume <id>)
|
|
3706
|
-
- The interactive listing
|
|
3774
|
+
- The interactive listing and every live-status flag fold in your other online machines automatically (live over SSH, no sync) — each row is labelled by host, this machine first. Use --local to skip the fan-out; single-id lookups stay local.
|
|
3775
|
+
- --all is not a device flag: it widens historical directory and time filters. Fleet collection is already the default. A status flag (--working/--idle/--waiting/--orphan/--crashed/--closed/--abandoned/--queued/--unknown) implies --active; combine status flags for a union.
|
|
3707
3776
|
- --host runs the query on the remote's own index over SSH (host alias or user@host); repeat or pass several to fan out. SSH access is the only auth.
|
|
3708
3777
|
- --in-team matches both ends of the lineage: the session that ran 'agents teams create/add', and (with --teams) that team's teammates. In the interactive list, 't' cycles the same filter over the teams in view.
|
|
3709
3778
|
- --include and --exclude are mutually exclusive.
|
|
@@ -30,6 +30,7 @@ Structure:
|
|
|
30
30
|
plugins/ optional: plugin bundles scoped to this workflow
|
|
31
31
|
|
|
32
32
|
Resolution: project > user > plugin (plugins/*/workflows/) > extra > system.
|
|
33
|
+
Pin a source with name@plugin or name@extra-alias (optional workflow: prefix).
|
|
33
34
|
|
|
34
35
|
Note: agents run defaults to --mode plan (read-only). For workflows that
|
|
35
36
|
write files, post comments, or otherwise mutate state, pass --mode edit or
|
|
Binary file
|
|
Binary file
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { getProjectAgentsDir, getUserWorkflowsDir, getSystemWorkflowsDir, getEnabledExtraRepos, } from '../state.js';
|
|
11
|
-
import { parseWorkflowFrontmatter, countWorkflowSubagents, listPluginWorkflowDirs,
|
|
11
|
+
import { parseWorkflowFrontmatter, countWorkflowSubagents, listPluginWorkflowDirs, resolveWorkflowRef, parseWorkflowRef, } from '../workflows.js';
|
|
12
12
|
function getLayerDirs(cwd) {
|
|
13
13
|
const projectDir = getProjectAgentsDir(cwd);
|
|
14
14
|
const extraRepos = getEnabledExtraRepos();
|
|
@@ -47,6 +47,27 @@ function orderedWorkflowSearchDirs(cwd) {
|
|
|
47
47
|
out.push({ dir: dirs.system, layer: 'system' });
|
|
48
48
|
return out;
|
|
49
49
|
}
|
|
50
|
+
/** Map a resolved absolute workflow dir to its origin layer (best-effort). */
|
|
51
|
+
function layerForWorkflowPath(workflowPath, cwd) {
|
|
52
|
+
const abs = path.resolve(workflowPath);
|
|
53
|
+
const under = (root) => {
|
|
54
|
+
const r = path.resolve(root);
|
|
55
|
+
return abs === r || abs.startsWith(r + path.sep);
|
|
56
|
+
};
|
|
57
|
+
const projectDir = getProjectAgentsDir(cwd);
|
|
58
|
+
if (projectDir && under(path.join(projectDir, 'workflows')))
|
|
59
|
+
return 'project';
|
|
60
|
+
if (under(getUserWorkflowsDir()))
|
|
61
|
+
return 'user';
|
|
62
|
+
if (under(getSystemWorkflowsDir()))
|
|
63
|
+
return 'system';
|
|
64
|
+
for (const extra of getEnabledExtraRepos()) {
|
|
65
|
+
if (under(path.join(extra.dir, 'workflows')))
|
|
66
|
+
return 'system';
|
|
67
|
+
}
|
|
68
|
+
// Plugin marketplaces (and anything else plugin-shaped).
|
|
69
|
+
return 'plugin';
|
|
70
|
+
}
|
|
50
71
|
class WorkflowsHandlerImpl {
|
|
51
72
|
kind = 'workflow';
|
|
52
73
|
listAll(_agent, cwd) {
|
|
@@ -76,27 +97,26 @@ class WorkflowsHandlerImpl {
|
|
|
76
97
|
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
77
98
|
}
|
|
78
99
|
resolve(_agent, name, cwd) {
|
|
79
|
-
//
|
|
80
|
-
|
|
100
|
+
// Delegate to resolveWorkflowRef so bare, workflow:, and name@source share one path.
|
|
101
|
+
const workflowPath = resolveWorkflowRef(name, cwd ?? process.cwd());
|
|
102
|
+
if (!workflowPath)
|
|
81
103
|
return null;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
return null;
|
|
104
|
+
const fm = parseWorkflowFrontmatter(workflowPath);
|
|
105
|
+
if (!fm)
|
|
106
|
+
return null;
|
|
107
|
+
const parsed = parseWorkflowRef(name);
|
|
108
|
+
const displayName = parsed?.name ?? path.basename(workflowPath);
|
|
109
|
+
return {
|
|
110
|
+
name: displayName,
|
|
111
|
+
item: {
|
|
112
|
+
name: fm.name || displayName,
|
|
113
|
+
description: fm.description,
|
|
114
|
+
model: fm.model,
|
|
115
|
+
subagentCount: countWorkflowSubagents(workflowPath),
|
|
116
|
+
},
|
|
117
|
+
layer: layerForWorkflowPath(workflowPath, cwd),
|
|
118
|
+
path: workflowPath,
|
|
119
|
+
};
|
|
100
120
|
}
|
|
101
121
|
sync(_agent, _versionHome, _cwd) {
|
|
102
122
|
// Version-home copies are written by syncResourcesToVersion in versions.ts.
|
|
Binary file
|
|
Binary file
|
package/dist/lib/workflows.d.ts
CHANGED
|
@@ -318,19 +318,37 @@ export declare function grokWorkflowMarker(filePath: string): string | null;
|
|
|
318
318
|
* runnable via `agents run <name>` without a separate install into
|
|
319
319
|
* ~/.agents/workflows/ (Phase 5 packaging). Within the plugin band, project
|
|
320
320
|
* plugins beat user/system plugins (same first-hit-wins as other layers).
|
|
321
|
+
*
|
|
322
|
+
* Pass `pluginName` to restrict to one plugin (for `name@plugin` resolution).
|
|
321
323
|
*/
|
|
322
|
-
export declare function listPluginWorkflowDirs(cwd?: string): string[];
|
|
324
|
+
export declare function listPluginWorkflowDirs(cwd?: string, pluginName?: string): string[];
|
|
323
325
|
/**
|
|
324
|
-
* True when `ref` is a single bare workflow
|
|
325
|
-
* Name lookup must not path-join multi-segment
|
|
326
|
+
* True when `ref` is a single bare workflow / source identifier (no path
|
|
327
|
+
* separators, no `..`, no `@`). Name lookup must not path-join multi-segment
|
|
328
|
+
* or traversal refs into search roots. `name@source` is parsed separately.
|
|
326
329
|
*/
|
|
327
330
|
export declare function isBareWorkflowName(ref: string): boolean;
|
|
331
|
+
/** Parsed `agents run` workflow reference (docs/07-entrypoints). */
|
|
332
|
+
export interface ParsedWorkflowRef {
|
|
333
|
+
/** Workflow directory name (WORKFLOW.md parent). */
|
|
334
|
+
name: string;
|
|
335
|
+
/**
|
|
336
|
+
* When set (`name@source`), pin resolution to that source only:
|
|
337
|
+
* a plugin name, or an enabled extra-repo alias.
|
|
338
|
+
*/
|
|
339
|
+
source?: string;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Parse a workflow run target: optional `workflow:` type prefix and optional
|
|
343
|
+
* `@source` pin. Returns null when the form is not a valid name lookup.
|
|
344
|
+
*/
|
|
345
|
+
export declare function parseWorkflowRef(ref: string): ParsedWorkflowRef | null;
|
|
328
346
|
/**
|
|
329
347
|
* Resolve an `agents run <workflow>` reference.
|
|
330
348
|
*
|
|
331
349
|
* Directories are accepted anywhere on disk when they contain WORKFLOW.md.
|
|
332
350
|
* Name lookup precedence (docs/07-entrypoints): project > user > plugin > extra > system.
|
|
333
|
-
*
|
|
351
|
+
* Pin a source with `name@plugin` or `name@extra-alias` (optional `workflow:` prefix).
|
|
334
352
|
*/
|
|
335
353
|
export declare function resolveWorkflowRef(ref: string, cwd?: string): string | null;
|
|
336
354
|
/**
|
package/dist/lib/workflows.js
CHANGED
|
@@ -522,15 +522,8 @@ function resolveWorkflowPath(ref, cwd) {
|
|
|
522
522
|
const candidate = path.isAbsolute(expanded) ? expanded : path.resolve(cwd, expanded);
|
|
523
523
|
return isWorkflowDir(candidate) ? candidate : null;
|
|
524
524
|
}
|
|
525
|
-
/**
|
|
526
|
-
|
|
527
|
-
* extra). Used by name resolution and listing so a plugin-packaged workflow is
|
|
528
|
-
* runnable via `agents run <name>` without a separate install into
|
|
529
|
-
* ~/.agents/workflows/ (Phase 5 packaging). Within the plugin band, project
|
|
530
|
-
* plugins beat user/system plugins (same first-hit-wins as other layers).
|
|
531
|
-
*/
|
|
532
|
-
export function listPluginWorkflowDirs(cwd = process.cwd()) {
|
|
533
|
-
const pluginRoots = [];
|
|
525
|
+
/** Plugin-root marketplace dirs in project → user → system → extra order. */
|
|
526
|
+
function pluginMarketplaceDirs(cwd) {
|
|
534
527
|
const pluginsDirs = [];
|
|
535
528
|
const projectPlugins = getProjectPluginsDir(cwd);
|
|
536
529
|
if (projectPlugins)
|
|
@@ -539,7 +532,34 @@ export function listPluginWorkflowDirs(cwd = process.cwd()) {
|
|
|
539
532
|
for (const extra of getEnabledExtraRepos()) {
|
|
540
533
|
pluginsDirs.push(path.join(extra.dir, 'plugins'));
|
|
541
534
|
}
|
|
542
|
-
|
|
535
|
+
return pluginsDirs;
|
|
536
|
+
}
|
|
537
|
+
function isPluginDirectory(pluginRoot, entry) {
|
|
538
|
+
if (entry.name.startsWith('.'))
|
|
539
|
+
return false;
|
|
540
|
+
let isDir = entry.isDirectory();
|
|
541
|
+
if (!isDir && entry.isSymbolicLink()) {
|
|
542
|
+
try {
|
|
543
|
+
isDir = fs.statSync(pluginRoot).isDirectory();
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
isDir = false;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return isDir;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Plugin `workflows/` directories in discovery order (project → user → system →
|
|
553
|
+
* extra). Used by name resolution and listing so a plugin-packaged workflow is
|
|
554
|
+
* runnable via `agents run <name>` without a separate install into
|
|
555
|
+
* ~/.agents/workflows/ (Phase 5 packaging). Within the plugin band, project
|
|
556
|
+
* plugins beat user/system plugins (same first-hit-wins as other layers).
|
|
557
|
+
*
|
|
558
|
+
* Pass `pluginName` to restrict to one plugin (for `name@plugin` resolution).
|
|
559
|
+
*/
|
|
560
|
+
export function listPluginWorkflowDirs(cwd = process.cwd(), pluginName) {
|
|
561
|
+
const pluginRoots = [];
|
|
562
|
+
for (const pluginsDir of pluginMarketplaceDirs(cwd)) {
|
|
543
563
|
if (!fs.existsSync(pluginsDir))
|
|
544
564
|
continue;
|
|
545
565
|
let entries;
|
|
@@ -550,20 +570,10 @@ export function listPluginWorkflowDirs(cwd = process.cwd()) {
|
|
|
550
570
|
continue;
|
|
551
571
|
}
|
|
552
572
|
for (const entry of entries) {
|
|
553
|
-
if (entry.name
|
|
573
|
+
if (pluginName !== undefined && entry.name !== pluginName)
|
|
554
574
|
continue;
|
|
555
|
-
// Directories and symlinks-to-directories (plugin marketplaces often symlink).
|
|
556
575
|
const pluginRoot = path.join(pluginsDir, entry.name);
|
|
557
|
-
|
|
558
|
-
if (!isDir && entry.isSymbolicLink()) {
|
|
559
|
-
try {
|
|
560
|
-
isDir = fs.statSync(pluginRoot).isDirectory();
|
|
561
|
-
}
|
|
562
|
-
catch {
|
|
563
|
-
isDir = false;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
if (!isDir)
|
|
576
|
+
if (!isPluginDirectory(pluginRoot, entry))
|
|
567
577
|
continue;
|
|
568
578
|
const workflowsDir = path.join(pluginRoot, 'workflows');
|
|
569
579
|
if (fs.existsSync(workflowsDir))
|
|
@@ -573,8 +583,9 @@ export function listPluginWorkflowDirs(cwd = process.cwd()) {
|
|
|
573
583
|
return pluginRoots;
|
|
574
584
|
}
|
|
575
585
|
/**
|
|
576
|
-
* True when `ref` is a single bare workflow
|
|
577
|
-
* Name lookup must not path-join multi-segment
|
|
586
|
+
* True when `ref` is a single bare workflow / source identifier (no path
|
|
587
|
+
* separators, no `..`, no `@`). Name lookup must not path-join multi-segment
|
|
588
|
+
* or traversal refs into search roots. `name@source` is parsed separately.
|
|
578
589
|
*/
|
|
579
590
|
export function isBareWorkflowName(ref) {
|
|
580
591
|
if (!ref || ref === '.' || ref === '..')
|
|
@@ -583,26 +594,65 @@ export function isBareWorkflowName(ref) {
|
|
|
583
594
|
return false;
|
|
584
595
|
if (ref.includes('..'))
|
|
585
596
|
return false;
|
|
597
|
+
if (ref.includes('@'))
|
|
598
|
+
return false;
|
|
586
599
|
// Reject absolute paths (posix or Windows).
|
|
587
600
|
if (path.isAbsolute(ref))
|
|
588
601
|
return false;
|
|
589
602
|
return path.basename(ref) === ref;
|
|
590
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Parse a workflow run target: optional `workflow:` type prefix and optional
|
|
606
|
+
* `@source` pin. Returns null when the form is not a valid name lookup.
|
|
607
|
+
*/
|
|
608
|
+
export function parseWorkflowRef(ref) {
|
|
609
|
+
let r = ref.trim();
|
|
610
|
+
if (r.startsWith('workflow:'))
|
|
611
|
+
r = r.slice('workflow:'.length);
|
|
612
|
+
if (!r)
|
|
613
|
+
return null;
|
|
614
|
+
const at = r.lastIndexOf('@');
|
|
615
|
+
if (at > 0) {
|
|
616
|
+
const name = r.slice(0, at);
|
|
617
|
+
const source = r.slice(at + 1);
|
|
618
|
+
if (!isBareWorkflowName(name) || !isBareWorkflowName(source))
|
|
619
|
+
return null;
|
|
620
|
+
return { name, source };
|
|
621
|
+
}
|
|
622
|
+
if (!isBareWorkflowName(r))
|
|
623
|
+
return null;
|
|
624
|
+
return { name: r };
|
|
625
|
+
}
|
|
591
626
|
/**
|
|
592
627
|
* Resolve an `agents run <workflow>` reference.
|
|
593
628
|
*
|
|
594
629
|
* Directories are accepted anywhere on disk when they contain WORKFLOW.md.
|
|
595
630
|
* Name lookup precedence (docs/07-entrypoints): project > user > plugin > extra > system.
|
|
596
|
-
*
|
|
631
|
+
* Pin a source with `name@plugin` or `name@extra-alias` (optional `workflow:` prefix).
|
|
597
632
|
*/
|
|
598
633
|
export function resolveWorkflowRef(ref, cwd = process.cwd()) {
|
|
599
634
|
const direct = resolveWorkflowPath(ref, cwd);
|
|
600
635
|
if (direct)
|
|
601
636
|
return direct;
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
if (!isBareWorkflowName(ref))
|
|
637
|
+
const parsed = parseWorkflowRef(ref);
|
|
638
|
+
if (!parsed)
|
|
605
639
|
return null;
|
|
640
|
+
// Source-qualified: only that plugin or extra-repo workflows/ (no layered fallback).
|
|
641
|
+
if (parsed.source) {
|
|
642
|
+
for (const dir of listPluginWorkflowDirs(cwd, parsed.source)) {
|
|
643
|
+
const workflowPath = path.join(dir, parsed.name);
|
|
644
|
+
if (isWorkflowDir(workflowPath))
|
|
645
|
+
return workflowPath;
|
|
646
|
+
}
|
|
647
|
+
for (const extra of getEnabledExtraRepos()) {
|
|
648
|
+
if (extra.alias !== parsed.source)
|
|
649
|
+
continue;
|
|
650
|
+
const workflowPath = path.join(extra.dir, 'workflows', parsed.name);
|
|
651
|
+
if (isWorkflowDir(workflowPath))
|
|
652
|
+
return workflowPath;
|
|
653
|
+
}
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
606
656
|
const projectAgentsDir = getProjectAgentsDir(cwd);
|
|
607
657
|
const searchDirs = [
|
|
608
658
|
...(projectAgentsDir ? [path.join(projectAgentsDir, 'workflows')] : []),
|
|
@@ -612,7 +662,7 @@ export function resolveWorkflowRef(ref, cwd = process.cwd()) {
|
|
|
612
662
|
getSystemWorkflowsDir(),
|
|
613
663
|
];
|
|
614
664
|
for (const dir of searchDirs) {
|
|
615
|
-
const workflowPath = path.join(dir,
|
|
665
|
+
const workflowPath = path.join(dir, parsed.name);
|
|
616
666
|
if (isWorkflowDir(workflowPath))
|
|
617
667
|
return workflowPath;
|
|
618
668
|
}
|
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.13",
|
|
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",
|