@phnx-labs/agents-cli 1.19.1 → 1.20.0

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.
Files changed (109) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +70 -10
  3. package/dist/commands/browser.js +88 -16
  4. package/dist/commands/cli.d.ts +14 -0
  5. package/dist/commands/cli.js +244 -0
  6. package/dist/commands/commands.js +3 -3
  7. package/dist/commands/computer.js +18 -1
  8. package/dist/commands/doctor.d.ts +1 -1
  9. package/dist/commands/doctor.js +2 -2
  10. package/dist/commands/exec.js +3 -3
  11. package/dist/commands/factory.d.ts +3 -14
  12. package/dist/commands/factory.js +3 -3
  13. package/dist/commands/hooks.js +3 -3
  14. package/dist/commands/mcp.js +29 -0
  15. package/dist/commands/plugins.js +11 -4
  16. package/dist/commands/profiles.js +1 -1
  17. package/dist/commands/prune.js +39 -160
  18. package/dist/commands/pull.js +56 -3
  19. package/dist/commands/routines.js +106 -13
  20. package/dist/commands/secrets.js +6 -8
  21. package/dist/commands/sessions.d.ts +36 -7
  22. package/dist/commands/sessions.js +130 -53
  23. package/dist/commands/setup.d.ts +1 -0
  24. package/dist/commands/setup.js +37 -28
  25. package/dist/commands/skills.js +3 -3
  26. package/dist/commands/teams.js +13 -0
  27. package/dist/commands/versions.d.ts +4 -3
  28. package/dist/commands/versions.js +147 -124
  29. package/dist/commands/view.js +12 -12
  30. package/dist/index.js +34 -6
  31. package/dist/lib/acp/harnesses.js +8 -0
  32. package/dist/lib/agents.js +162 -9
  33. package/dist/lib/browser/cdp.d.ts +8 -1
  34. package/dist/lib/browser/cdp.js +40 -3
  35. package/dist/lib/browser/chrome.d.ts +13 -0
  36. package/dist/lib/browser/chrome.js +42 -3
  37. package/dist/lib/browser/domain-skills.d.ts +51 -0
  38. package/dist/lib/browser/domain-skills.js +157 -0
  39. package/dist/lib/browser/drivers/local.js +45 -4
  40. package/dist/lib/browser/drivers/ssh.js +1 -1
  41. package/dist/lib/browser/ipc.d.ts +8 -1
  42. package/dist/lib/browser/ipc.js +37 -28
  43. package/dist/lib/browser/profiles.d.ts +13 -0
  44. package/dist/lib/browser/profiles.js +41 -1
  45. package/dist/lib/browser/service.d.ts +3 -0
  46. package/dist/lib/browser/service.js +21 -5
  47. package/dist/lib/browser/types.d.ts +7 -0
  48. package/dist/lib/cli-resources.d.ts +109 -0
  49. package/dist/lib/cli-resources.js +255 -0
  50. package/dist/lib/cloud/rush.js +5 -5
  51. package/dist/lib/command-skills.js +0 -2
  52. package/dist/lib/computer-rpc.d.ts +3 -0
  53. package/dist/lib/computer-rpc.js +53 -0
  54. package/dist/lib/daemon.js +20 -0
  55. package/dist/lib/exec.d.ts +3 -2
  56. package/dist/lib/exec.js +62 -6
  57. package/dist/lib/hooks.js +182 -0
  58. package/dist/lib/mcp.js +6 -0
  59. package/dist/lib/migrate.js +1 -1
  60. package/dist/lib/overdue.d.ts +26 -0
  61. package/dist/lib/overdue.js +101 -0
  62. package/dist/lib/permissions.js +5 -1
  63. package/dist/lib/plugin-marketplace.js +1 -1
  64. package/dist/lib/profiles-presets.js +37 -0
  65. package/dist/lib/registry.d.ts +18 -0
  66. package/dist/lib/registry.js +44 -0
  67. package/dist/lib/resources/mcp.js +43 -1
  68. package/dist/lib/resources/types.d.ts +1 -1
  69. package/dist/lib/resources.d.ts +1 -1
  70. package/dist/lib/rotate.js +10 -4
  71. package/dist/lib/routines-format.d.ts +35 -0
  72. package/dist/lib/routines-format.js +173 -0
  73. package/dist/lib/routines.d.ts +7 -1
  74. package/dist/lib/routines.js +32 -12
  75. package/dist/lib/runner.js +19 -5
  76. package/dist/lib/scheduler.js +8 -1
  77. package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/CodeResources +0 -0
  78. package/dist/lib/secrets/{AgentsKeychain.app/Contents/Info.plist → Agents CLI.app/Contents/Info.plist } +4 -2
  79. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  80. package/dist/lib/secrets/bundles.d.ts +33 -2
  81. package/dist/lib/secrets/bundles.js +249 -26
  82. package/dist/lib/secrets/index.d.ts +10 -1
  83. package/dist/lib/secrets/index.js +143 -48
  84. package/dist/lib/session/active.d.ts +8 -0
  85. package/dist/lib/session/active.js +3 -2
  86. package/dist/lib/session/db.d.ts +10 -4
  87. package/dist/lib/session/db.js +16 -16
  88. package/dist/lib/session/parse.d.ts +1 -0
  89. package/dist/lib/session/parse.js +44 -0
  90. package/dist/lib/session/types.d.ts +1 -1
  91. package/dist/lib/session/types.js +1 -1
  92. package/dist/lib/shims.d.ts +6 -2
  93. package/dist/lib/shims.js +88 -10
  94. package/dist/lib/state.d.ts +0 -1
  95. package/dist/lib/state.js +2 -15
  96. package/dist/lib/teams/agents.js +1 -1
  97. package/dist/lib/teams/parsers.d.ts +1 -1
  98. package/dist/lib/teams/parsers.js +153 -3
  99. package/dist/lib/teams/summarizer.js +18 -2
  100. package/dist/lib/teams/worktree.js +14 -3
  101. package/dist/lib/types.d.ts +7 -4
  102. package/dist/lib/types.js +6 -3
  103. package/dist/lib/versions.d.ts +10 -2
  104. package/dist/lib/versions.js +227 -35
  105. package/package.json +9 -9
  106. package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
  107. package/npm-shrinkwrap.json +0 -3162
  108. /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/_CodeSignature/CodeResources +0 -0
  109. /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/embedded.provisionprofile +0 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Pure display helpers for `agents routines list`.
3
+ *
4
+ * No external dependencies. All functions are pure (no I/O, no side effects).
5
+ */
6
+ // ---------------------------------------------------------------------------
7
+ // humanizeCron
8
+ // ---------------------------------------------------------------------------
9
+ const DAY_NAMES = ['Sundays', 'Mondays', 'Tuesdays', 'Wednesdays', 'Thursdays', 'Fridays', 'Saturdays'];
10
+ /**
11
+ * Convert a cron expression to a human-readable phrase.
12
+ *
13
+ * Handles the common patterns. For anything unrecognized, returns the raw
14
+ * expression so the user still sees something useful. NEVER throws.
15
+ */
16
+ export function humanizeCron(expr, _tz) {
17
+ try {
18
+ const parts = expr.trim().split(/\s+/);
19
+ if (parts.length !== 5)
20
+ return expr;
21
+ const [minute, hour, dom, month, dow] = parts;
22
+ // every minute: * * * * *
23
+ if (minute === '*' && hour === '*' && dom === '*' && month === '*' && dow === '*') {
24
+ return 'every minute';
25
+ }
26
+ // every N minutes: */N * * * *
27
+ const everyMinMatch = minute.match(/^\*\/(\d+)$/);
28
+ if (everyMinMatch && hour === '*' && dom === '*' && month === '*' && dow === '*') {
29
+ const n = parseInt(everyMinMatch[1], 10);
30
+ return `every ${n} minute${n === 1 ? '' : 's'}`;
31
+ }
32
+ // every N hours: 0 */N * * *
33
+ const everyHourMatch = hour.match(/^\*\/(\d+)$/);
34
+ if (everyHourMatch && minute === '0' && dom === '*' && month === '*' && dow === '*') {
35
+ const n = parseInt(everyHourMatch[1], 10);
36
+ return `every ${n} hour${n === 1 ? '' : 's'}`;
37
+ }
38
+ // Only proceed with time-of-day patterns when hour and minute are fixed integers
39
+ const hourNum = /^\d+$/.test(hour) ? parseInt(hour, 10) : null;
40
+ const minNum = /^\d+$/.test(minute) ? parseInt(minute, 10) : null;
41
+ if (hourNum === null || minNum === null)
42
+ return expr;
43
+ const timeStr = formatTime12(hourNum, minNum);
44
+ // daily at HH:MM: M H * * *
45
+ if (dom === '*' && month === '*' && dow === '*') {
46
+ return `daily at ${timeStr}`;
47
+ }
48
+ // weekdays: M H * * 1-5
49
+ if (dom === '*' && month === '*' && dow === '1-5') {
50
+ return `weekdays at ${timeStr}`;
51
+ }
52
+ // specific day of week: M H * * D (single digit 0-6)
53
+ if (dom === '*' && month === '*' && /^\d$/.test(dow)) {
54
+ const dayIdx = parseInt(dow, 10);
55
+ if (dayIdx >= 0 && dayIdx <= 6) {
56
+ return `${DAY_NAMES[dayIdx]} at ${timeStr}`;
57
+ }
58
+ }
59
+ // specific day of month: M H D * *
60
+ if (/^\d+$/.test(dom) && month === '*' && dow === '*') {
61
+ const d = parseInt(dom, 10);
62
+ return `monthly on day ${d} at ${timeStr}`;
63
+ }
64
+ // every N hours with fixed minute: M */N * * * (already handled above for M=0; catch M != 0)
65
+ if (everyHourMatch && dom === '*' && month === '*' && dow === '*') {
66
+ const n = parseInt(everyHourMatch[1], 10);
67
+ return `every ${n} hour${n === 1 ? '' : 's'} at :${String(minNum).padStart(2, '0')}`;
68
+ }
69
+ return expr;
70
+ }
71
+ catch {
72
+ return expr;
73
+ }
74
+ }
75
+ /** Format an hour (0-23) + minute (0-59) as "H:MM AM/PM". */
76
+ function formatTime12(hour, minute) {
77
+ const period = hour < 12 ? 'AM' : 'PM';
78
+ const h = hour % 12 === 0 ? 12 : hour % 12;
79
+ const m = String(minute).padStart(2, '0');
80
+ return `${h}:${m} ${period}`;
81
+ }
82
+ // ---------------------------------------------------------------------------
83
+ // humanizeNextRun
84
+ // ---------------------------------------------------------------------------
85
+ const WEEKDAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
86
+ const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
87
+ /**
88
+ * Convert a next-run Date into a human phrase relative to `now`.
89
+ *
90
+ * - null → '-'
91
+ * - same calendar day → 'today 9:00 AM'
92
+ * - next calendar day → 'tomorrow 9:00 AM'
93
+ * - within 7 days → 'Mon 9:00 AM'
94
+ * - further out → 'Jun 15, 9:00 AM'
95
+ */
96
+ export function humanizeNextRun(date, now, tz) {
97
+ if (!date)
98
+ return '-';
99
+ try {
100
+ const locale = 'en-US';
101
+ const tzOpts = tz ? { timeZone: tz } : {};
102
+ // Extract calendar date components for both dates using the same timezone.
103
+ const toYMD = (d) => {
104
+ const fmt = new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'numeric', day: 'numeric', ...tzOpts });
105
+ const parts = fmt.formatToParts(d);
106
+ const get = (type) => parseInt(parts.find((p) => p.type === type)?.value ?? '0', 10);
107
+ return { y: get('year'), m: get('month'), day: get('day') };
108
+ };
109
+ const nowYMD = toYMD(now);
110
+ const dateYMD = toYMD(date);
111
+ // Diff in whole calendar days (ignoring time-of-day)
112
+ const nowMidnight = Date.UTC(nowYMD.y, nowYMD.m - 1, nowYMD.day);
113
+ const dateMidnight = Date.UTC(dateYMD.y, dateYMD.m - 1, dateYMD.day);
114
+ const diffDays = Math.round((dateMidnight - nowMidnight) / 86400000);
115
+ // Time string for the date
116
+ const timeFmt = new Intl.DateTimeFormat(locale, { hour: 'numeric', minute: '2-digit', hour12: true, ...tzOpts });
117
+ const timeStr = timeFmt.format(date);
118
+ if (diffDays === 0)
119
+ return `today ${timeStr}`;
120
+ if (diffDays === 1)
121
+ return `tomorrow ${timeStr}`;
122
+ if (diffDays < 7) {
123
+ const weekdayIdx = new Intl.DateTimeFormat(locale, { weekday: 'short', ...tzOpts })
124
+ .formatToParts(date)
125
+ .find((p) => p.type === 'weekday')?.value;
126
+ return `${weekdayIdx ?? WEEKDAY_NAMES[date.getDay()]} ${timeStr}`;
127
+ }
128
+ // Further out: "Jun 15, 9:00 AM"
129
+ const monthName = MONTH_NAMES[dateYMD.m - 1];
130
+ return `${monthName} ${dateYMD.day}, ${timeStr}`;
131
+ }
132
+ catch {
133
+ return date.toLocaleString();
134
+ }
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // formatRepoLink
138
+ // ---------------------------------------------------------------------------
139
+ /**
140
+ * Parse a repo string into a display label and an optional hyperlink target.
141
+ *
142
+ * Rules:
143
+ * - undefined / empty → display '-', href null
144
+ * - 'owner/name' (one slash) → display 'owner/name', href 'https://github.com/owner/name/pulls'
145
+ * - 'https://...' or 'http://…' → display hostname+path, href the URL verbatim
146
+ * - anything else → display raw string, href null
147
+ */
148
+ export function formatRepoLink(repo) {
149
+ if (!repo || repo.trim() === '') {
150
+ return { display: '-', href: null };
151
+ }
152
+ const trimmed = repo.trim();
153
+ // Absolute URL
154
+ if (trimmed.startsWith('https://') || trimmed.startsWith('http://')) {
155
+ try {
156
+ const url = new URL(trimmed);
157
+ const display = url.hostname + url.pathname.replace(/\/$/, '');
158
+ return { display, href: trimmed };
159
+ }
160
+ catch {
161
+ return { display: trimmed, href: null };
162
+ }
163
+ }
164
+ // GitHub shorthand: owner/name (exactly one slash, no scheme, no extra slashes)
165
+ if (/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(trimmed)) {
166
+ return {
167
+ display: trimmed,
168
+ href: `https://github.com/${trimmed}/pulls`,
169
+ };
170
+ }
171
+ // Anything else: plain text, no link
172
+ return { display: trimmed, href: null };
173
+ }
@@ -18,12 +18,14 @@ export interface JobConfig {
18
18
  name: string;
19
19
  schedule: string;
20
20
  agent: AgentId;
21
+ workflow?: string;
21
22
  mode: 'plan' | 'edit' | 'full';
22
23
  effort: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
23
24
  timeout: string;
24
25
  enabled: boolean;
25
26
  prompt: string;
26
27
  timezone?: string;
28
+ repo?: string;
27
29
  variables?: Record<string, string>;
28
30
  sandbox?: boolean;
29
31
  allow?: JobAllowConfig;
@@ -36,6 +38,7 @@ export interface RunMeta {
36
38
  jobName: string;
37
39
  runId: string;
38
40
  agent: AgentId;
41
+ workflow?: string;
39
42
  pid: number | null;
40
43
  status: 'running' | 'completed' | 'failed' | 'timeout';
41
44
  startedAt: string;
@@ -56,7 +59,10 @@ export declare function setJobEnabled(name: string, enabled: boolean): void;
56
59
  export declare function validateJob(config: Partial<JobConfig>): string[];
57
60
  /** Expand built-in and user-defined template variables in a job's prompt string. */
58
61
  export declare function resolveJobPrompt(config: JobConfig): string;
59
- /** Parse a human-readable timeout string (e.g. "30m", "2h", "1h30m") into milliseconds. */
62
+ /** Parse a human-readable timeout string (e.g. "10m", "2h", "1h30m", "3d", "1w") into milliseconds.
63
+ * Accepts combinations of w (weeks), d (days), h (hours), m (minutes).
64
+ * Returns null if the string is empty, matches nothing, totals zero, or exceeds 1 week.
65
+ */
60
66
  export declare function parseTimeout(timeout: string): number | null;
61
67
  /** List all run metadata entries for a job, sorted chronologically. */
62
68
  export declare function listRuns(jobName: string): RunMeta[];
@@ -17,7 +17,7 @@ import { ALL_AGENT_IDS } from './agents.js';
17
17
  const JOB_DEFAULTS = {
18
18
  mode: 'plan',
19
19
  effort: 'auto',
20
- timeout: '30m',
20
+ timeout: '10m',
21
21
  enabled: true,
22
22
  };
23
23
  /** List all job configs from ~/.agents/routines/. */
@@ -73,7 +73,7 @@ export function writeJob(config) {
73
73
  delete output.mode;
74
74
  if (output.effort === 'auto')
75
75
  delete output.effort;
76
- if (output.timeout === '30m')
76
+ if (output.timeout === '10m')
77
77
  delete output.timeout;
78
78
  if (output.enabled === true)
79
79
  delete output.enabled;
@@ -119,12 +119,22 @@ export function validateJob(config) {
119
119
  errors.push(`invalid cron expression: "${config.schedule}"`);
120
120
  }
121
121
  }
122
- if (!config.agent || typeof config.agent !== 'string') {
123
- errors.push('agent is required');
122
+ const hasAgent = Boolean(config.agent && typeof config.agent === 'string');
123
+ const hasWorkflow = Boolean(config.workflow && typeof config.workflow === 'string');
124
+ if (!hasAgent && !hasWorkflow) {
125
+ errors.push('exactly one of agent or workflow is required');
124
126
  }
125
- if (config.agent && !ALL_AGENT_IDS.includes(config.agent)) {
127
+ else if (hasAgent && hasWorkflow) {
128
+ errors.push('exactly one of agent or workflow must be set (not both)');
129
+ }
130
+ if (hasAgent && config.agent && !ALL_AGENT_IDS.includes(config.agent)) {
126
131
  errors.push(`agent must be one of: ${ALL_AGENT_IDS.join(', ')}`);
127
132
  }
133
+ if (hasWorkflow && config.workflow) {
134
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(config.workflow)) {
135
+ errors.push('workflow must be a lowercase alphanumeric name (hyphens and underscores allowed, e.g. autodev)');
136
+ }
137
+ }
128
138
  if (config.mode && !['plan', 'edit', 'full'].includes(config.mode)) {
129
139
  errors.push('mode must be plan, edit, or full');
130
140
  }
@@ -135,7 +145,7 @@ export function validateJob(config) {
135
145
  errors.push('prompt is required');
136
146
  }
137
147
  if (config.timeout && !parseTimeout(config.timeout)) {
138
- errors.push('timeout must be like 30m, 2h, 1h30m');
148
+ errors.push('timeout must be like 10m, 2h, 3d, 1w (max 1w)');
139
149
  }
140
150
  return errors;
141
151
  }
@@ -179,15 +189,25 @@ export function resolveJobPrompt(config) {
179
189
  }
180
190
  return prompt;
181
191
  }
182
- /** Parse a human-readable timeout string (e.g. "30m", "2h", "1h30m") into milliseconds. */
192
+ /** Parse a human-readable timeout string (e.g. "10m", "2h", "1h30m", "3d", "1w") into milliseconds.
193
+ * Accepts combinations of w (weeks), d (days), h (hours), m (minutes).
194
+ * Returns null if the string is empty, matches nothing, totals zero, or exceeds 1 week.
195
+ */
183
196
  export function parseTimeout(timeout) {
184
- const match = timeout.match(/^(?:(\d+)h)?(?:(\d+)m)?$/);
197
+ const match = timeout.match(/^(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$/);
185
198
  if (!match)
186
199
  return null;
187
- const hours = parseInt(match[1] || '0', 10);
188
- const minutes = parseInt(match[2] || '0', 10);
189
- const ms = (hours * 60 + minutes) * 60 * 1000;
190
- return ms > 0 ? ms : null;
200
+ const weeks = parseInt(match[1] || '0', 10);
201
+ const days = parseInt(match[2] || '0', 10);
202
+ const hours = parseInt(match[3] || '0', 10);
203
+ const minutes = parseInt(match[4] || '0', 10);
204
+ const ms = ((weeks * 7 + days) * 24 * 60 + hours * 60 + minutes) * 60 * 1000;
205
+ if (ms <= 0)
206
+ return null;
207
+ const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000; // 604800000
208
+ if (ms > ONE_WEEK_MS)
209
+ return null;
210
+ return ms;
191
211
  }
192
212
  /** List all run metadata entries for a job, sorted chronologically. */
193
213
  export function listRuns(jobName) {
@@ -23,6 +23,14 @@ const AGENT_COMMANDS = {
23
23
  };
24
24
  /** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
25
25
  export function buildJobCommand(config, resolvedPrompt) {
26
+ // Workflow branch: delegate to `agents run <workflow>` which handles subagent
27
+ // injection, WORKFLOW.md orchestration, and model selection via frontmatter.
28
+ // appendModelAndReasoning is intentionally skipped — the workflow frontmatter
29
+ // owns model selection. No --timeout flag: the runner enforces its own SIGTERM/SIGKILL.
30
+ if (config.workflow) {
31
+ const cmd = ['agents', 'run', config.workflow, resolvedPrompt, '--mode', config.mode];
32
+ return cmd;
33
+ }
26
34
  const template = AGENT_COMMANDS[config.agent];
27
35
  if (!template) {
28
36
  throw new Error(`Unsupported agent for daemon jobs: ${config.agent}`);
@@ -130,10 +138,14 @@ export async function executeJob(config) {
130
138
  if (config.timezone) {
131
139
  spawnEnv.TZ = config.timezone;
132
140
  }
141
+ // Workflows run via `agents run <workflow>` which delegates to claude under the hood.
142
+ // Use 'claude' as the effective agent for report extraction and metadata when workflow is set.
143
+ const effectiveAgent = config.workflow ? 'claude' : config.agent;
133
144
  const meta = {
134
145
  jobName: config.name,
135
146
  runId,
136
- agent: config.agent,
147
+ agent: effectiveAgent,
148
+ ...(config.workflow ? { workflow: config.workflow } : {}),
137
149
  pid: null,
138
150
  status: 'running',
139
151
  startedAt: new Date().toISOString(),
@@ -141,7 +153,7 @@ export async function executeJob(config) {
141
153
  exitCode: null,
142
154
  };
143
155
  writeRunMeta(meta);
144
- const timeoutMs = parseTimeout(config.timeout) || 30 * 60 * 1000;
156
+ const timeoutMs = parseTimeout(config.timeout) || 10 * 60 * 1000;
145
157
  return new Promise((resolve) => {
146
158
  const child = spawn(cmd[0], cmd.slice(1), {
147
159
  stdio: ['ignore', stdoutFd, stdoutFd],
@@ -173,7 +185,7 @@ export async function executeJob(config) {
173
185
  meta.completedAt = new Date().toISOString();
174
186
  writeRunMeta(meta);
175
187
  timer.end({ status: 'timeout', runId });
176
- const reportPath = extractAndSaveReport(stdoutPath, config.agent, runDir);
188
+ const reportPath = extractAndSaveReport(stdoutPath, effectiveAgent, runDir);
177
189
  resolve({ meta, reportPath });
178
190
  }, timeoutMs);
179
191
  child.on('exit', (code) => {
@@ -190,7 +202,7 @@ export async function executeJob(config) {
190
202
  meta.completedAt = new Date().toISOString();
191
203
  writeRunMeta(meta);
192
204
  timer.end({ status: meta.status, exitCode: code ?? undefined, runId });
193
- const reportPath = extractAndSaveReport(stdoutPath, config.agent, runDir);
205
+ const reportPath = extractAndSaveReport(stdoutPath, effectiveAgent, runDir);
194
206
  resolve({ meta, reportPath });
195
207
  });
196
208
  child.on('error', (err) => {
@@ -226,10 +238,12 @@ export async function executeJobDetached(config) {
226
238
  if (config.timezone) {
227
239
  spawnEnv.TZ = config.timezone;
228
240
  }
241
+ const effectiveAgent = config.workflow ? 'claude' : config.agent;
229
242
  const meta = {
230
243
  jobName: config.name,
231
244
  runId,
232
- agent: config.agent,
245
+ agent: effectiveAgent,
246
+ ...(config.workflow ? { workflow: config.workflow } : {}),
233
247
  pid: null,
234
248
  status: 'running',
235
249
  startedAt: new Date().toISOString(),
@@ -24,7 +24,14 @@ export class JobScheduler {
24
24
  }
25
25
  schedule(config) {
26
26
  this.unschedule(config.name);
27
- const cron = new Cron(config.schedule, async () => {
27
+ // catch: true a throw from one job's callback should not kill the
28
+ // whole cron loop. Each invocation of onTrigger is already wrapped in
29
+ // try/catch, but a synchronous throw before the await would otherwise
30
+ // bubble up; defense in depth.
31
+ const cronOptions = { catch: true };
32
+ if (config.timezone)
33
+ cronOptions.timezone = config.timezone;
34
+ const cron = new Cron(config.schedule, cronOptions, async () => {
28
35
  try {
29
36
  await this.onTrigger(config);
30
37
  }
@@ -5,9 +5,11 @@
5
5
  <key>CFBundleIdentifier</key>
6
6
  <string>com.phnx-labs.agents-keychain</string>
7
7
  <key>CFBundleName</key>
8
- <string>AgentsKeychain</string>
8
+ <string>Agents CLI</string>
9
+ <key>CFBundleDisplayName</key>
10
+ <string>Agents CLI</string>
9
11
  <key>CFBundleExecutable</key>
10
- <string>AgentsKeychain</string>
12
+ <string>Agents CLI</string>
11
13
  <key>CFBundlePackageType</key>
12
14
  <string>APPL</string>
13
15
  <key>CFBundleVersion</key>
@@ -62,7 +62,11 @@ export declare function validateSecretType(t: string): asserts t is SecretType;
62
62
  export declare function validateExpiresFutureDated(iso: string): void;
63
63
  export declare function bundleExists(name: string): boolean;
64
64
  export declare function readBundle(name: string): SecretsBundle;
65
- export declare function writeBundle(bundle: SecretsBundle): void;
65
+ export interface WriteBundleOptions {
66
+ /** Emit a secrets.set audit event. Internal bookkeeping writes turn this off. */
67
+ emitEvent?: boolean;
68
+ }
69
+ export declare function writeBundle(bundle: SecretsBundle, opts?: WriteBundleOptions): void;
66
70
  export declare function deleteBundle(name: string): boolean;
67
71
  export declare function listBundles(): SecretsBundle[];
68
72
  export interface BundleEntryInfo {
@@ -71,7 +75,34 @@ export interface BundleEntryInfo {
71
75
  detail: string;
72
76
  }
73
77
  export declare function describeBundle(bundle: SecretsBundle): BundleEntryInfo[];
74
- export declare function resolveBundleEnv(bundle: SecretsBundle): Record<string, string>;
78
+ /** Options for resolveBundleEnv. */
79
+ export interface ResolveBundleOptions {
80
+ /**
81
+ * Human-readable label for who is requesting the secrets. Shown to the
82
+ * user under the Touch ID prompt so they know what's about to read.
83
+ * Example: "agent claude", "command curl", "browser profile".
84
+ * When omitted the prompt only names the bundle.
85
+ */
86
+ caller?: string;
87
+ }
88
+ export declare function resolveBundleEnv(bundle: SecretsBundle, opts?: ResolveBundleOptions): Record<string, string>;
89
+ /**
90
+ * Read a bundle's metadata AND resolve its env in a single Touch ID prompt.
91
+ *
92
+ * `readBundle` + `resolveBundleEnv` issued two separate `LAContext` calls
93
+ * (metadata read via `get-auth`, then secret values via `get-batch`) which
94
+ * surfaced as two consecutive Touch ID prompts. macOS does not honor
95
+ * "Always Allow" for items protected with `kSecAttrAccessControl`+biometry,
96
+ * so caching at the OS level was never an option. This collapses both reads
97
+ * into one `get-batch` call: we enumerate the bundle's secret items first
98
+ * (silent — `list` returns attrs only and does not trigger biometry) and
99
+ * include the metadata item in the same batch. One prompt, correctly scoped
100
+ * to the bundle name and caller.
101
+ */
102
+ export declare function readAndResolveBundleEnv(name: string, opts?: ResolveBundleOptions): {
103
+ bundle: SecretsBundle;
104
+ env: Record<string, string>;
105
+ };
75
106
  export declare function keychainRef(key: string): string;
76
107
  /** Options for rotateBundleSecret. */
77
108
  export interface RotateOptions {