@phnx-labs/agents-cli 1.22.9 → 1.22.11
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 +37 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +21 -9
- package/dist/commands/browser.js +2 -2
- package/dist/commands/events.d.ts +1 -1
- package/dist/commands/events.js +1 -1
- package/dist/commands/projects.d.ts +1 -2
- package/dist/commands/projects.js +39 -47
- package/dist/commands/secrets.js +38 -39
- package/dist/commands/ssh.js +4 -5
- package/dist/commands/workflows.js +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/event-stream.d.ts +1 -1
- package/dist/lib/event-stream.js +1 -1
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +3 -3
- package/dist/lib/feed-broadcast.d.ts +19 -5
- package/dist/lib/feed-broadcast.js +41 -8
- 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.js +61 -0
- package/dist/lib/plugins.d.ts +14 -5
- package/dist/lib/plugins.js +29 -5
- package/dist/lib/project-probe.d.ts +1 -1
- package/dist/lib/project-probe.js +1 -1
- package/dist/lib/projects.d.ts +1 -1
- package/dist/lib/resources/types.d.ts +2 -1
- package/dist/lib/resources/workflows.d.ts +1 -1
- package/dist/lib/resources/workflows.js +22 -20
- 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/audit.js +1 -1
- package/dist/lib/secrets/bundles.d.ts +9 -0
- package/dist/lib/secrets/bundles.js +9 -23
- package/dist/lib/secrets/headless.d.ts +3 -15
- package/dist/lib/secrets/headless.js +5 -33
- package/dist/lib/secrets/index.d.ts +8 -0
- package/dist/lib/secrets/index.js +16 -36
- package/dist/lib/share/config.js +5 -7
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +1 -1
- package/dist/lib/types.d.ts +7 -1
- package/dist/lib/workflows.d.ts +19 -4
- package/dist/lib/workflows.js +81 -7
- package/package.json +1 -1
|
@@ -5,22 +5,13 @@
|
|
|
5
5
|
* importing bundles.ts (which already imports index.ts).
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
|
-
* True when the current process
|
|
9
|
-
* must NEVER raise a Keychain biometry prompt on the
|
|
10
|
-
* a prompt nobody is watching. Two signals, either sufficient:
|
|
8
|
+
* True when the current process was structurally launched by an agent runtime
|
|
9
|
+
* that must NEVER raise a Keychain biometry prompt on the user's screen:
|
|
11
10
|
* - `AGENTS_RUNTIME` is `headless`, `teams`, or `terminal` — i.e. ANY agent
|
|
12
11
|
* launch, interactive included, and inherited by everything spawned beneath
|
|
13
12
|
* one (set on the child env by `agents run --headless`, scheduled routines,
|
|
14
13
|
* teammates, and interactive runs — see exec.ts:430, runner.ts,
|
|
15
14
|
* teams/agents.ts).
|
|
16
|
-
* - neither stdin nor stdout is a TTY (a detached/backgrounded task whose
|
|
17
|
-
* stdio is redirected to a log — e.g. a release script run in the
|
|
18
|
-
* background as `( ... ) >log 2>&1 </dev/null`).
|
|
19
|
-
* `AGENTS_SECRETS_NO_PROMPT=1` forces headless-safe; `=0` force-allows a prompt
|
|
20
|
-
* even in a non-TTY context. An `eval "$(agents secrets export X)"` typed in a
|
|
21
|
-
* PLAIN shell has no AGENTS_RUNTIME, so it is not classified headless and still
|
|
22
|
-
* prompts. Run beneath an agent it inherits AGENTS_RUNTIME and resolves
|
|
23
|
-
* broker-only — the agent, not the human, is the caller there.
|
|
24
15
|
*
|
|
25
16
|
* Only **macOS keychain** reads pop an interactive Touch ID sheet — the secrets
|
|
26
17
|
* broker itself is a no-op off darwin (see agent.ts), and libsecret (Linux) /
|
|
@@ -33,31 +24,12 @@
|
|
|
33
24
|
* fast with an actionable error instead of hijacking Touch ID. This generalizes
|
|
34
25
|
* the per-caller broker-only pattern used across the headless secrets readers.
|
|
35
26
|
*/
|
|
36
|
-
export function isHeadlessSecretsContext(env = process.env, platform = process.platform
|
|
37
|
-
// Injected so the TTY branch below is testable: it is the branch that decides a
|
|
38
|
-
// plain human shell still prompts, which is this guard's entire safety argument,
|
|
39
|
-
// and reading process.* directly made it unreachable from a test.
|
|
40
|
-
tty = { stdin: process.stdin.isTTY, stdout: process.stdout.isTTY }) {
|
|
27
|
+
export function isHeadlessSecretsContext(env = process.env, platform = process.platform) {
|
|
41
28
|
if (platform !== 'darwin')
|
|
42
29
|
return false; // no biometry prompt to suppress off-darwin
|
|
43
|
-
|
|
44
|
-
if (override === '1')
|
|
45
|
-
return true;
|
|
46
|
-
if (override === '0')
|
|
47
|
-
return false;
|
|
48
|
-
// Every AGENT-LAUNCH runtime resolves broker-only, interactive included.
|
|
49
|
-
// `terminal` was missing, which made an agent terminal the one launch path
|
|
50
|
-
// still allowed to pop Touch ID: exec.ts sets AGENTS_RUNTIME='terminal' for an
|
|
51
|
-
// interactive run (exec.ts:430), that fell through to the TTY check below, and
|
|
52
|
-
// a TTY meant "a human is watching, so prompting is fine". It is not fine —
|
|
53
|
-
// opening a terminal is not a request to authenticate, and a launch that needs
|
|
54
|
-
// a locked bundle should say so and point at `agents secrets unlock`, not grab
|
|
55
|
-
// the fingerprint sensor. AGENTS_RUNTIME is INHERITED by everything spawned under
|
|
56
|
-
// an agent, so `agents secrets export` run beneath one resolves broker-only too —
|
|
57
|
-
// correctly: there the agent, not the human, is the caller. A plain shell carries
|
|
58
|
-
// no AGENTS_RUNTIME, so a person running it themselves still gets the sheet.
|
|
30
|
+
// Every agent-launch runtime resolves broker-only, interactive included.
|
|
59
31
|
const runtime = env.AGENTS_RUNTIME;
|
|
60
32
|
if (runtime === 'headless' || runtime === 'teams' || runtime === 'terminal')
|
|
61
33
|
return true;
|
|
62
|
-
return
|
|
34
|
+
return false;
|
|
63
35
|
}
|
|
@@ -143,6 +143,14 @@ export declare function healHmacKeyNoAclOnce(rec: HmacKeyRecord): boolean;
|
|
|
143
143
|
* see readAndResolveBundleEnv.
|
|
144
144
|
*/
|
|
145
145
|
export declare function keychainServiceAlias(item: string): string;
|
|
146
|
+
/**
|
|
147
|
+
* One-shot per process: activate hashing on machines with nothing to move,
|
|
148
|
+
* finish a crash-interrupted delete phase (silent), and run the interactive
|
|
149
|
+
* one-time re-key when cleartext-named items exist and a human is present.
|
|
150
|
+
* Never throws — a failed attempt leaves the process on cleartext names
|
|
151
|
+
* (exact pre-#316 behavior) and the next process retries.
|
|
152
|
+
*/
|
|
153
|
+
export declare function maybeAutoRekey(): void;
|
|
146
154
|
/** One re-keyed (or failed) item, by its old cleartext service name. */
|
|
147
155
|
export interface RekeyPlanItem {
|
|
148
156
|
oldService: string;
|
|
@@ -247,30 +247,7 @@ export function readHmacKeyRecord() {
|
|
|
247
247
|
catch {
|
|
248
248
|
return null;
|
|
249
249
|
}
|
|
250
|
-
|
|
251
|
-
// Converge a stale-ACL'd hmackey to silent, on the HOT read path. An old helper
|
|
252
|
-
// (pre the metadata/hmackey no-ACL migration fix) re-stamped this
|
|
253
|
-
// contractually-no-ACL item with a biometry ACL, so the read just above pops the
|
|
254
|
-
// generic "Agents CLI needs to authenticate" sheet on EVERY hashed lookup — the
|
|
255
|
-
// `agents devices list` stats probe the SessionStart hook runs, and every other
|
|
256
|
-
// background hashed read. `maybeAutoRekey`'s one-shot heal only fires on a
|
|
257
|
-
// cleartext-bundle resolve and is bypassed for the hmackey/hashed-name path
|
|
258
|
-
// (prepareServiceName returns early for HMAC_KEY_ITEM before maybeAutoRekey), so
|
|
259
|
-
// it never converged exactly these reads and the machine prompted forever.
|
|
260
|
-
// Re-store the record no-ACL once per machine here (the read that produced it has
|
|
261
|
-
// already happened — and already prompted if the item was ACL'd); every
|
|
262
|
-
// subsequent read, in this process and all future ones, is silent.
|
|
263
|
-
if (record && !record.healedNoAcl) {
|
|
264
|
-
try {
|
|
265
|
-
healHmacKeyNoAclOnce(record);
|
|
266
|
-
record.healedNoAcl = true;
|
|
267
|
-
}
|
|
268
|
-
catch {
|
|
269
|
-
// A failed no-ACL re-store leaves the item still ACL'd (a still-prompting
|
|
270
|
-
// read) rather than a silent wrong state; the next process retries.
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return record;
|
|
250
|
+
return parseHmacKeyRecord(raw);
|
|
274
251
|
}
|
|
275
252
|
function writeHmacKeyRecord(rec) {
|
|
276
253
|
// JSON.stringify drops undefined fields (used to clear pendingDeletes).
|
|
@@ -325,15 +302,13 @@ function resolveHashState() {
|
|
|
325
302
|
export function keychainServiceAlias(item) {
|
|
326
303
|
return prepareServiceName(item);
|
|
327
304
|
}
|
|
328
|
-
function prepareServiceName(item
|
|
305
|
+
function prepareServiceName(item) {
|
|
329
306
|
if (rawScopeDepth > 0)
|
|
330
307
|
return item;
|
|
331
308
|
if (!isOurItem(item))
|
|
332
309
|
return item;
|
|
333
310
|
if (item === HMAC_KEY_ITEM || item.startsWith(HASHED_SERVICE_PREFIX))
|
|
334
311
|
return item;
|
|
335
|
-
if (opts?.autoRekey)
|
|
336
|
-
maybeAutoRekey();
|
|
337
312
|
const st = resolveHashState();
|
|
338
313
|
if (!st.active || !st.key)
|
|
339
314
|
return item;
|
|
@@ -354,7 +329,6 @@ function prepareListPrefix(prefix) {
|
|
|
354
329
|
return { prefix };
|
|
355
330
|
if (prefix.startsWith(HASHED_SERVICE_PREFIX))
|
|
356
331
|
return { prefix };
|
|
357
|
-
maybeAutoRekey();
|
|
358
332
|
const st = resolveHashState();
|
|
359
333
|
if (!st.active || !st.key)
|
|
360
334
|
return { prefix };
|
|
@@ -429,7 +403,7 @@ function finishPendingDeletes(rec) {
|
|
|
429
403
|
* Never throws — a failed attempt leaves the process on cleartext names
|
|
430
404
|
* (exact pre-#316 behavior) and the next process retries.
|
|
431
405
|
*/
|
|
432
|
-
function maybeAutoRekey() {
|
|
406
|
+
export function maybeAutoRekey() {
|
|
433
407
|
if (autoRekeyAttempted || rekeyRunning || rawScopeDepth > 0)
|
|
434
408
|
return;
|
|
435
409
|
autoRekeyAttempted = true;
|
|
@@ -446,10 +420,14 @@ function maybeAutoRekey() {
|
|
|
446
420
|
return;
|
|
447
421
|
const st = resolveHashState();
|
|
448
422
|
if (st.active) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
423
|
+
if (st.record && !st.record.healedNoAcl) {
|
|
424
|
+
try {
|
|
425
|
+
healHmacKeyNoAclOnce(st.record);
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
/* the next explicit unlock retries */
|
|
429
|
+
}
|
|
430
|
+
}
|
|
453
431
|
if (st.record?.pendingDeletes?.length) {
|
|
454
432
|
try {
|
|
455
433
|
finishPendingDeletes(st.record);
|
|
@@ -820,7 +798,7 @@ export function getKeychainToken(item, context = {}) {
|
|
|
820
798
|
// Errors keep the requested (human-readable) name; the storage name may be
|
|
821
799
|
// an opaque hash.
|
|
822
800
|
const requested = item;
|
|
823
|
-
item = prepareServiceName(item
|
|
801
|
+
item = prepareServiceName(item);
|
|
824
802
|
if (backend)
|
|
825
803
|
return backend.get(item);
|
|
826
804
|
assertRawKeychainReadAllowed(requested, context);
|
|
@@ -848,6 +826,7 @@ export function getKeychainToken(item, context = {}) {
|
|
|
848
826
|
...process.env,
|
|
849
827
|
AGENTS_KEYCHAIN_PROMPT: keychainOperationPrompt(context),
|
|
850
828
|
AGENTS_KEYCHAIN_PROMPT_BASE: keychainOperationPrompt({ ...context, duration: undefined }),
|
|
829
|
+
AGENTS_KEYCHAIN_SKIP_AUTH_UI: context.silentNoAcl ? '1' : '0',
|
|
851
830
|
},
|
|
852
831
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
853
832
|
});
|
|
@@ -890,7 +869,7 @@ export function getKeychainTokens(items, context = {}) {
|
|
|
890
869
|
// whether those were cleartext (hashed here) or already-hashed (enumerated).
|
|
891
870
|
const requestedByStorage = new Map();
|
|
892
871
|
const storageItems = items.map((item) => {
|
|
893
|
-
const storage = prepareServiceName(item
|
|
872
|
+
const storage = prepareServiceName(item);
|
|
894
873
|
if (!requestedByStorage.has(storage))
|
|
895
874
|
requestedByStorage.set(storage, item);
|
|
896
875
|
return storage;
|
|
@@ -939,6 +918,7 @@ export function getKeychainTokens(items, context = {}) {
|
|
|
939
918
|
...process.env,
|
|
940
919
|
AGENTS_KEYCHAIN_PROMPT: keychainOperationPrompt(context),
|
|
941
920
|
AGENTS_KEYCHAIN_PROMPT_BASE: keychainOperationPrompt({ ...context, duration: undefined }),
|
|
921
|
+
AGENTS_KEYCHAIN_SKIP_AUTH_UI: context.silentNoAcl ? '1' : '0',
|
|
942
922
|
// The signed helper's own vocabulary is unchanged (it predates the rename
|
|
943
923
|
// and ships as a separately-versioned binary), so map to its legacy token.
|
|
944
924
|
AGENTS_KEYCHAIN_DEFAULT_POLICY: (context.defaultPolicy ?? 'hold') === 'hold' ? 'daily' : context.defaultPolicy,
|
|
@@ -1037,7 +1017,7 @@ export function setKeychainToken(item, value, opts) {
|
|
|
1037
1017
|
if (/[\x00=\r\n]/.test(item))
|
|
1038
1018
|
throw new Error('Secret item name contains invalid characters.');
|
|
1039
1019
|
const requested = item;
|
|
1040
|
-
item = prepareServiceName(item
|
|
1020
|
+
item = prepareServiceName(item);
|
|
1041
1021
|
if (backend) {
|
|
1042
1022
|
backend.set(item, value, opts);
|
|
1043
1023
|
return;
|
package/dist/lib/share/config.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// `cloudflare` bundle.
|
|
10
10
|
import { randomBytes } from 'node:crypto';
|
|
11
11
|
import { readMeta, updateMeta } from '../state.js';
|
|
12
|
-
import { bundleExists, bundleItemStore, bundlePolicy,
|
|
12
|
+
import { bundleExists, bundleItemStore, bundlePolicy, keychainRef, readAndResolveBundleEnv, readBundle, writeBundle, } from '../secrets/bundles.js';
|
|
13
13
|
import { secretsKeychainItem } from '../secrets/index.js';
|
|
14
14
|
export const SHARE_BUNDLE = 'share';
|
|
15
15
|
export const SHARE_TOKEN_KEY = 'WRITE_TOKEN';
|
|
@@ -76,10 +76,8 @@ export function storeWriteToken(token) {
|
|
|
76
76
|
export function readWriteTokenFromBundle() {
|
|
77
77
|
const { env } = readAndResolveBundleEnv(SHARE_BUNDLE, {
|
|
78
78
|
caller: 'share',
|
|
79
|
-
// Explicit
|
|
80
|
-
|
|
81
|
-
// an agent LAUNCH read (that is exec.ts's --secrets injection, always agentOnly).
|
|
82
|
-
agentOnly: isHeadlessSecretsContext(),
|
|
79
|
+
// Explicit share commands are reads, not authorization to authenticate.
|
|
80
|
+
agentOnly: true,
|
|
83
81
|
});
|
|
84
82
|
const token = env[SHARE_TOKEN_KEY];
|
|
85
83
|
if (!token) {
|
|
@@ -136,8 +134,8 @@ export function readCloudflareCreds(bundle = DEFAULT_CF_BUNDLE, override) {
|
|
|
136
134
|
}
|
|
137
135
|
const { env } = readAndResolveBundleEnv(bundle, {
|
|
138
136
|
caller: 'share',
|
|
139
|
-
//
|
|
140
|
-
agentOnly:
|
|
137
|
+
// Setup is still a read; only `agents secrets unlock` may authenticate.
|
|
138
|
+
agentOnly: true,
|
|
141
139
|
});
|
|
142
140
|
const find = (re) => {
|
|
143
141
|
for (const [k, v] of Object.entries(env))
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -113,7 +113,7 @@ export declare function getUserSecretsDir(): string;
|
|
|
113
113
|
* value-free usage telemetry (which bundle was created/imported/exported/viewed/
|
|
114
114
|
* accessed/unlocked, when, by whom), never a secret value. It is a derived index
|
|
115
115
|
* fed FROM the emitSecretAudit chokepoint alongside the append-only
|
|
116
|
-
* ~/.agents/events.jsonl audit log — the same way sessions.db indexes session
|
|
116
|
+
* ~/.agents/.history/events/events.jsonl audit log — the same way sessions.db indexes session
|
|
117
117
|
* metadata off the real session flow — not a second write path.
|
|
118
118
|
*/
|
|
119
119
|
export declare function getSecretsDbPath(): string;
|
package/dist/lib/state.js
CHANGED
|
@@ -372,7 +372,7 @@ export function getUserSecretsDir() { return USER_SECRETS_DIR; }
|
|
|
372
372
|
* value-free usage telemetry (which bundle was created/imported/exported/viewed/
|
|
373
373
|
* accessed/unlocked, when, by whom), never a secret value. It is a derived index
|
|
374
374
|
* fed FROM the emitSecretAudit chokepoint alongside the append-only
|
|
375
|
-
* ~/.agents/events.jsonl audit log — the same way sessions.db indexes session
|
|
375
|
+
* ~/.agents/.history/events/events.jsonl audit log — the same way sessions.db indexes session
|
|
376
376
|
* metadata off the real session flow — not a second write path.
|
|
377
377
|
*/
|
|
378
378
|
export function getSecretsDbPath() {
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -79,7 +79,7 @@ export interface BudgetConfig {
|
|
|
79
79
|
require_confirm_over?: number;
|
|
80
80
|
}
|
|
81
81
|
/** Preview features that users can opt into via `agents beta`. */
|
|
82
|
-
export type BetaFeatureName = 'factory'
|
|
82
|
+
export type BetaFeatureName = 'factory';
|
|
83
83
|
/** Subset of chalk color names used for agent-specific terminal output. */
|
|
84
84
|
export type ChalkColor = 'magenta' | 'green' | 'blue' | 'cyan' | 'yellowBright' | 'redBright' | 'whiteBright' | 'blueBright' | 'greenBright' | 'magentaBright' | 'cyanBright';
|
|
85
85
|
/** Static configuration for a single agent -- paths, capabilities, and format conventions. */
|
|
@@ -610,6 +610,12 @@ export interface DiscoveredPlugin {
|
|
|
610
610
|
commands: string[];
|
|
611
611
|
/** Subagent .md files in the plugin's agents/ directory (names without extension). */
|
|
612
612
|
agentDefs: string[];
|
|
613
|
+
/**
|
|
614
|
+
* Workflow directory names under the plugin's `workflows/` (each must contain
|
|
615
|
+
* WORKFLOW.md). Phase 5 packaging: plugins may package workflows as entrypoints;
|
|
616
|
+
* `agents run <name>` resolves them via project > user > plugin > extra > system.
|
|
617
|
+
*/
|
|
618
|
+
workflows: string[];
|
|
613
619
|
/** Memory fact basenames from the plugin's memory/ directory (without .md). */
|
|
614
620
|
memory: string[];
|
|
615
621
|
/** Executable files in the plugin's bin/ directory. */
|
package/dist/lib/workflows.d.ts
CHANGED
|
@@ -312,11 +312,25 @@ export declare const GROK_WORKFLOW_MARKER = "agents_workflow";
|
|
|
312
312
|
export declare function transformWorkflowForGrok(workflowPath: string, name: string): string;
|
|
313
313
|
/** Read the agents_workflow marker from a Grok `.rhai` file, if present. */
|
|
314
314
|
export declare function grokWorkflowMarker(filePath: string): string | null;
|
|
315
|
+
/**
|
|
316
|
+
* Plugin `workflows/` directories in discovery order (project → user → system →
|
|
317
|
+
* extra). Used by name resolution and listing so a plugin-packaged workflow is
|
|
318
|
+
* runnable via `agents run <name>` without a separate install into
|
|
319
|
+
* ~/.agents/workflows/ (Phase 5 packaging). Within the plugin band, project
|
|
320
|
+
* plugins beat user/system plugins (same first-hit-wins as other layers).
|
|
321
|
+
*/
|
|
322
|
+
export declare function listPluginWorkflowDirs(cwd?: string): string[];
|
|
323
|
+
/**
|
|
324
|
+
* True when `ref` is a single bare workflow name (no path separators, no `..`).
|
|
325
|
+
* Name lookup must not path-join multi-segment or traversal refs into search roots.
|
|
326
|
+
*/
|
|
327
|
+
export declare function isBareWorkflowName(ref: string): boolean;
|
|
315
328
|
/**
|
|
316
329
|
* Resolve an `agents run <workflow>` reference.
|
|
317
330
|
*
|
|
318
331
|
* Directories are accepted anywhere on disk when they contain WORKFLOW.md.
|
|
319
|
-
* Name lookup
|
|
332
|
+
* Name lookup precedence (docs/07-entrypoints): project > user > plugin > extra > system.
|
|
333
|
+
* Bare name only; `name@plugin` disambiguation is a follow-up.
|
|
320
334
|
*/
|
|
321
335
|
export declare function resolveWorkflowRef(ref: string, cwd?: string): string | null;
|
|
322
336
|
/**
|
|
@@ -326,10 +340,11 @@ export declare function resolveWorkflowRef(ref: string, cwd?: string): string |
|
|
|
326
340
|
*/
|
|
327
341
|
export declare function discoverWorkflowsFromRepo(repoPath: string): DiscoveredWorkflow[];
|
|
328
342
|
/**
|
|
329
|
-
* List all workflows in central storage.
|
|
330
|
-
*
|
|
343
|
+
* List all workflows in central storage + plugin packages.
|
|
344
|
+
* Precedence: user > plugin > extra > system (first writer wins; project is
|
|
345
|
+
* cwd-scoped and handled by resolveWorkflowRef / the resource handler).
|
|
331
346
|
*/
|
|
332
|
-
export declare function listInstalledWorkflows(): Map<string, InstalledWorkflow>;
|
|
347
|
+
export declare function listInstalledWorkflows(cwd?: string): Map<string, InstalledWorkflow>;
|
|
333
348
|
/** Copy a workflow directory into user central storage (~/.agents/workflows/<name>/). */
|
|
334
349
|
export declare function installWorkflowCentrally(sourcePath: string, name: string): {
|
|
335
350
|
success: boolean;
|
package/dist/lib/workflows.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as os from 'os';
|
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import * as yaml from 'yaml';
|
|
12
12
|
import { capableAgents, supports } from './capabilities.js';
|
|
13
|
-
import { getProjectAgentsDir, getSystemWorkflowsDir, getUserWorkflowsDir, getTrashWorkflowsDir, getEnabledExtraRepos, } from './state.js';
|
|
13
|
+
import { getProjectAgentsDir, getSystemWorkflowsDir, getUserWorkflowsDir, getTrashWorkflowsDir, getEnabledExtraRepos, getPluginsDir, getSystemPluginsDir, getProjectPluginsDir, } from './state.js';
|
|
14
14
|
import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
15
15
|
/**
|
|
16
16
|
* Hard upper bound on items a single `for_each` expands, absent an explicit
|
|
@@ -522,22 +522,94 @@ 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
|
+
* Plugin `workflows/` directories in discovery order (project → user → system →
|
|
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 = [];
|
|
534
|
+
const pluginsDirs = [];
|
|
535
|
+
const projectPlugins = getProjectPluginsDir(cwd);
|
|
536
|
+
if (projectPlugins)
|
|
537
|
+
pluginsDirs.push(projectPlugins);
|
|
538
|
+
pluginsDirs.push(getPluginsDir(), getSystemPluginsDir());
|
|
539
|
+
for (const extra of getEnabledExtraRepos()) {
|
|
540
|
+
pluginsDirs.push(path.join(extra.dir, 'plugins'));
|
|
541
|
+
}
|
|
542
|
+
for (const pluginsDir of pluginsDirs) {
|
|
543
|
+
if (!fs.existsSync(pluginsDir))
|
|
544
|
+
continue;
|
|
545
|
+
let entries;
|
|
546
|
+
try {
|
|
547
|
+
entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
for (const entry of entries) {
|
|
553
|
+
if (entry.name.startsWith('.'))
|
|
554
|
+
continue;
|
|
555
|
+
// Directories and symlinks-to-directories (plugin marketplaces often symlink).
|
|
556
|
+
const pluginRoot = path.join(pluginsDir, entry.name);
|
|
557
|
+
let isDir = entry.isDirectory();
|
|
558
|
+
if (!isDir && entry.isSymbolicLink()) {
|
|
559
|
+
try {
|
|
560
|
+
isDir = fs.statSync(pluginRoot).isDirectory();
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
isDir = false;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (!isDir)
|
|
567
|
+
continue;
|
|
568
|
+
const workflowsDir = path.join(pluginRoot, 'workflows');
|
|
569
|
+
if (fs.existsSync(workflowsDir))
|
|
570
|
+
pluginRoots.push(workflowsDir);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return pluginRoots;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* True when `ref` is a single bare workflow name (no path separators, no `..`).
|
|
577
|
+
* Name lookup must not path-join multi-segment or traversal refs into search roots.
|
|
578
|
+
*/
|
|
579
|
+
export function isBareWorkflowName(ref) {
|
|
580
|
+
if (!ref || ref === '.' || ref === '..')
|
|
581
|
+
return false;
|
|
582
|
+
if (ref.includes('/') || ref.includes('\\'))
|
|
583
|
+
return false;
|
|
584
|
+
if (ref.includes('..'))
|
|
585
|
+
return false;
|
|
586
|
+
// Reject absolute paths (posix or Windows).
|
|
587
|
+
if (path.isAbsolute(ref))
|
|
588
|
+
return false;
|
|
589
|
+
return path.basename(ref) === ref;
|
|
590
|
+
}
|
|
525
591
|
/**
|
|
526
592
|
* Resolve an `agents run <workflow>` reference.
|
|
527
593
|
*
|
|
528
594
|
* Directories are accepted anywhere on disk when they contain WORKFLOW.md.
|
|
529
|
-
* Name lookup
|
|
595
|
+
* Name lookup precedence (docs/07-entrypoints): project > user > plugin > extra > system.
|
|
596
|
+
* Bare name only; `name@plugin` disambiguation is a follow-up.
|
|
530
597
|
*/
|
|
531
598
|
export function resolveWorkflowRef(ref, cwd = process.cwd()) {
|
|
532
599
|
const direct = resolveWorkflowPath(ref, cwd);
|
|
533
600
|
if (direct)
|
|
534
601
|
return direct;
|
|
602
|
+
// Name lookup only — reject traversal / multi-segment so path.join(dir, ref)
|
|
603
|
+
// cannot escape a workflows root (absolute paths already handled above).
|
|
604
|
+
if (!isBareWorkflowName(ref))
|
|
605
|
+
return null;
|
|
535
606
|
const projectAgentsDir = getProjectAgentsDir(cwd);
|
|
536
607
|
const searchDirs = [
|
|
537
608
|
...(projectAgentsDir ? [path.join(projectAgentsDir, 'workflows')] : []),
|
|
538
609
|
getUserWorkflowsDir(),
|
|
539
|
-
|
|
610
|
+
...listPluginWorkflowDirs(cwd),
|
|
540
611
|
...getEnabledExtraRepos().map(r => path.join(r.dir, 'workflows')),
|
|
612
|
+
getSystemWorkflowsDir(),
|
|
541
613
|
];
|
|
542
614
|
for (const dir of searchDirs) {
|
|
543
615
|
const workflowPath = path.join(dir, ref);
|
|
@@ -592,16 +664,18 @@ export function discoverWorkflowsFromRepo(repoPath) {
|
|
|
592
664
|
return results;
|
|
593
665
|
}
|
|
594
666
|
/**
|
|
595
|
-
* List all workflows in central storage.
|
|
596
|
-
*
|
|
667
|
+
* List all workflows in central storage + plugin packages.
|
|
668
|
+
* Precedence: user > plugin > extra > system (first writer wins; project is
|
|
669
|
+
* cwd-scoped and handled by resolveWorkflowRef / the resource handler).
|
|
597
670
|
*/
|
|
598
|
-
export function listInstalledWorkflows() {
|
|
671
|
+
export function listInstalledWorkflows(cwd = process.cwd()) {
|
|
599
672
|
const result = new Map();
|
|
600
673
|
const extraRepos = getEnabledExtraRepos();
|
|
601
674
|
const searchDirs = [
|
|
602
675
|
getUserWorkflowsDir(),
|
|
603
|
-
|
|
676
|
+
...listPluginWorkflowDirs(cwd),
|
|
604
677
|
...extraRepos.map(r => path.join(r.dir, 'workflows')),
|
|
678
|
+
getSystemWorkflowsDir(),
|
|
605
679
|
];
|
|
606
680
|
for (const dir of searchDirs) {
|
|
607
681
|
if (!fs.existsSync(dir))
|
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.11",
|
|
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",
|