@phnx-labs/agents-cli 1.19.2 → 1.20.3

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 (156) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +72 -12
  3. package/dist/browser.js +0 -0
  4. package/dist/commands/browser.js +88 -16
  5. package/dist/commands/cli.d.ts +14 -0
  6. package/dist/commands/cli.js +244 -0
  7. package/dist/commands/cloud.js +1 -1
  8. package/dist/commands/commands.js +27 -10
  9. package/dist/commands/computer.js +18 -1
  10. package/dist/commands/doctor.d.ts +1 -1
  11. package/dist/commands/doctor.js +2 -2
  12. package/dist/commands/exec.js +38 -18
  13. package/dist/commands/factory.d.ts +3 -14
  14. package/dist/commands/factory.js +3 -3
  15. package/dist/commands/feedback.d.ts +7 -0
  16. package/dist/commands/feedback.js +89 -0
  17. package/dist/commands/helper.d.ts +12 -0
  18. package/dist/commands/helper.js +87 -0
  19. package/dist/commands/hooks.js +89 -10
  20. package/dist/commands/mcp.js +166 -10
  21. package/dist/commands/packages.js +196 -27
  22. package/dist/commands/permissions.js +21 -6
  23. package/dist/commands/plugins.js +11 -4
  24. package/dist/commands/profiles.d.ts +8 -0
  25. package/dist/commands/profiles.js +118 -5
  26. package/dist/commands/prune.js +39 -160
  27. package/dist/commands/pull.js +58 -5
  28. package/dist/commands/routines.js +107 -14
  29. package/dist/commands/rules.js +8 -4
  30. package/dist/commands/secrets-migrate.d.ts +24 -0
  31. package/dist/commands/secrets-migrate.js +198 -0
  32. package/dist/commands/secrets-sync.d.ts +11 -0
  33. package/dist/commands/secrets-sync.js +155 -0
  34. package/dist/commands/secrets.js +79 -46
  35. package/dist/commands/sessions.d.ts +28 -0
  36. package/dist/commands/sessions.js +98 -33
  37. package/dist/commands/setup.d.ts +1 -0
  38. package/dist/commands/setup.js +37 -28
  39. package/dist/commands/skills.js +25 -8
  40. package/dist/commands/subagents.js +69 -49
  41. package/dist/commands/teams.js +61 -10
  42. package/dist/commands/utils.d.ts +33 -0
  43. package/dist/commands/utils.js +139 -0
  44. package/dist/commands/versions.d.ts +4 -3
  45. package/dist/commands/versions.js +134 -130
  46. package/dist/commands/view.d.ts +6 -0
  47. package/dist/commands/view.js +175 -19
  48. package/dist/commands/workflows.js +29 -6
  49. package/dist/computer.js +0 -0
  50. package/dist/index.js +38 -6
  51. package/dist/lib/acp/client.js +6 -1
  52. package/dist/lib/acp/harnesses.js +8 -0
  53. package/dist/lib/agents.d.ts +4 -0
  54. package/dist/lib/agents.js +125 -34
  55. package/dist/lib/auto-pull-worker.js +18 -1
  56. package/dist/lib/browser/cdp.d.ts +8 -1
  57. package/dist/lib/browser/cdp.js +40 -3
  58. package/dist/lib/browser/chrome.d.ts +13 -0
  59. package/dist/lib/browser/chrome.js +46 -3
  60. package/dist/lib/browser/domain-skills.d.ts +51 -0
  61. package/dist/lib/browser/domain-skills.js +157 -0
  62. package/dist/lib/browser/drivers/local.js +45 -4
  63. package/dist/lib/browser/drivers/ssh.js +2 -2
  64. package/dist/lib/browser/ipc.d.ts +8 -1
  65. package/dist/lib/browser/ipc.js +37 -28
  66. package/dist/lib/browser/profiles.d.ts +16 -3
  67. package/dist/lib/browser/profiles.js +44 -4
  68. package/dist/lib/browser/service.d.ts +3 -0
  69. package/dist/lib/browser/service.js +40 -5
  70. package/dist/lib/browser/types.d.ts +11 -4
  71. package/dist/lib/cli-resources.d.ts +137 -0
  72. package/dist/lib/cli-resources.js +477 -0
  73. package/dist/lib/cloud/factory.d.ts +1 -1
  74. package/dist/lib/cloud/factory.js +1 -1
  75. package/dist/lib/cloud/rush.js +5 -5
  76. package/dist/lib/command-skills.js +0 -2
  77. package/dist/lib/computer-rpc.d.ts +3 -0
  78. package/dist/lib/computer-rpc.js +53 -0
  79. package/dist/lib/daemon.js +20 -0
  80. package/dist/lib/events.d.ts +16 -2
  81. package/dist/lib/events.js +33 -2
  82. package/dist/lib/exec.d.ts +42 -13
  83. package/dist/lib/exec.js +127 -33
  84. package/dist/lib/help.js +11 -5
  85. package/dist/lib/hooks/cache.d.ts +38 -0
  86. package/dist/lib/hooks/cache.js +242 -0
  87. package/dist/lib/hooks/profile.d.ts +33 -0
  88. package/dist/lib/hooks/profile.js +129 -0
  89. package/dist/lib/hooks.d.ts +0 -10
  90. package/dist/lib/hooks.js +246 -11
  91. package/dist/lib/mcp.d.ts +15 -0
  92. package/dist/lib/mcp.js +46 -0
  93. package/dist/lib/migrate.js +1 -1
  94. package/dist/lib/overdue.d.ts +26 -0
  95. package/dist/lib/overdue.js +101 -0
  96. package/dist/lib/permissions.d.ts +13 -0
  97. package/dist/lib/permissions.js +55 -1
  98. package/dist/lib/plugin-marketplace.js +1 -1
  99. package/dist/lib/plugins.js +15 -1
  100. package/dist/lib/profiles-presets.d.ts +26 -0
  101. package/dist/lib/profiles-presets.js +216 -0
  102. package/dist/lib/profiles.d.ts +34 -0
  103. package/dist/lib/profiles.js +112 -1
  104. package/dist/lib/resources/mcp.js +37 -0
  105. package/dist/lib/resources.d.ts +1 -1
  106. package/dist/lib/rotate.js +10 -4
  107. package/dist/lib/routines-format.d.ts +47 -0
  108. package/dist/lib/routines-format.js +194 -0
  109. package/dist/lib/routines.d.ts +8 -2
  110. package/dist/lib/routines.js +34 -14
  111. package/dist/lib/runner.js +83 -15
  112. package/dist/lib/scheduler.js +8 -1
  113. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  114. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  115. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
  116. package/dist/lib/secrets/bundles.d.ts +34 -17
  117. package/dist/lib/secrets/bundles.js +210 -36
  118. package/dist/lib/secrets/index.d.ts +49 -30
  119. package/dist/lib/secrets/index.js +126 -115
  120. package/dist/lib/secrets/install-helper.d.ts +45 -0
  121. package/dist/lib/secrets/install-helper.js +165 -0
  122. package/dist/lib/secrets/linux.js +4 -4
  123. package/dist/lib/secrets/sync.d.ts +56 -0
  124. package/dist/lib/secrets/sync.js +180 -0
  125. package/dist/lib/session/active.d.ts +8 -0
  126. package/dist/lib/session/active.js +3 -2
  127. package/dist/lib/session/db.d.ts +0 -4
  128. package/dist/lib/session/db.js +0 -26
  129. package/dist/lib/session/parse.d.ts +1 -0
  130. package/dist/lib/session/parse.js +44 -0
  131. package/dist/lib/session/render.js +4 -4
  132. package/dist/lib/session/types.d.ts +2 -2
  133. package/dist/lib/session/types.js +1 -1
  134. package/dist/lib/shims.d.ts +5 -2
  135. package/dist/lib/shims.js +70 -38
  136. package/dist/lib/state.d.ts +14 -2
  137. package/dist/lib/state.js +51 -20
  138. package/dist/lib/teams/agents.d.ts +5 -4
  139. package/dist/lib/teams/agents.js +48 -22
  140. package/dist/lib/teams/api.d.ts +2 -1
  141. package/dist/lib/teams/api.js +4 -3
  142. package/dist/lib/teams/parsers.d.ts +1 -1
  143. package/dist/lib/teams/parsers.js +153 -3
  144. package/dist/lib/teams/summarizer.js +18 -2
  145. package/dist/lib/teams/worktree.js +14 -3
  146. package/dist/lib/types.d.ts +63 -4
  147. package/dist/lib/types.js +8 -3
  148. package/dist/lib/usage.d.ts +27 -2
  149. package/dist/lib/usage.js +100 -17
  150. package/dist/lib/versions.d.ts +45 -3
  151. package/dist/lib/versions.js +455 -60
  152. package/package.json +15 -14
  153. package/scripts/install-helper.js +97 -0
  154. package/scripts/postinstall.js +16 -0
  155. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  156. package/npm-shrinkwrap.json +0 -3162
@@ -0,0 +1,194 @@
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
+ * Maximum display length for a repo cell. Display strings longer than this
141
+ * are truncated with an ellipsis so column alignment is preserved.
142
+ * Consumers that render the column should use this constant as the column width.
143
+ */
144
+ export const REPO_DISPLAY_MAX = 24;
145
+ /**
146
+ * Parse a repo string into a display label and an optional hyperlink target.
147
+ *
148
+ * Rules:
149
+ * - null / undefined / empty / non-string → display '-', href null
150
+ * - 'owner/name' (one slash) → display 'owner/name', href 'https://github.com/owner/name/pulls'
151
+ * - 'https://...' or 'http://...' → display hostname+path, href the URL verbatim
152
+ * - anything else → display raw string, href null
153
+ *
154
+ * The display string is truncated to REPO_DISPLAY_MAX characters (with a
155
+ * trailing '…') when it would otherwise exceed the column width. The href
156
+ * is always the full untruncated URL so hyperlinks remain functional.
157
+ *
158
+ * NEVER throws — mirrors the contract of humanizeCron.
159
+ */
160
+ export function formatRepoLink(repo) {
161
+ if (repo == null || typeof repo !== 'string' || repo.trim() === '') {
162
+ return { display: '-', href: null };
163
+ }
164
+ const trimmed = repo.trim();
165
+ let display;
166
+ let href;
167
+ // Absolute URL
168
+ if (trimmed.startsWith('https://') || trimmed.startsWith('http://')) {
169
+ try {
170
+ const url = new URL(trimmed);
171
+ display = url.hostname + url.pathname.replace(/\/$/, '');
172
+ href = trimmed;
173
+ }
174
+ catch {
175
+ display = trimmed;
176
+ href = null;
177
+ }
178
+ }
179
+ else if (/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(trimmed)) {
180
+ // GitHub shorthand: owner/name (exactly one slash, no scheme, no extra slashes)
181
+ display = trimmed;
182
+ href = `https://github.com/${trimmed}/pulls`;
183
+ }
184
+ else {
185
+ // Anything else: plain text, no link
186
+ display = trimmed;
187
+ href = null;
188
+ }
189
+ // Truncate display to column width; href stays untruncated.
190
+ if (display.length > REPO_DISPLAY_MAX) {
191
+ display = display.slice(0, REPO_DISPLAY_MAX - 1) + '…';
192
+ }
193
+ return { display, href };
194
+ }
@@ -18,12 +18,14 @@ export interface JobConfig {
18
18
  name: string;
19
19
  schedule: string;
20
20
  agent: AgentId;
21
- mode: 'plan' | 'edit' | 'full';
21
+ workflow?: string;
22
+ mode: 'plan' | 'edit' | 'auto' | 'skip' | '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,14 +119,24 @@ 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
  }
128
- if (config.mode && !['plan', 'edit', 'full'].includes(config.mode)) {
129
- errors.push('mode must be plan, edit, or full');
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
+ }
138
+ if (config.mode && !['plan', 'edit', 'auto', 'skip', 'full'].includes(config.mode)) {
139
+ errors.push("mode must be plan, edit, auto, or skip ('full' accepted as alias for skip)");
130
140
  }
131
141
  if (config.effort && !['low', 'medium', 'high', 'xhigh', 'max', 'auto'].includes(config.effort)) {
132
142
  errors.push('effort must be low, medium, high, xhigh, max, or auto');
@@ -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) {
@@ -14,7 +14,8 @@ import { resolveJobPrompt, parseTimeout, writeRunMeta, getRunDir, } from './rout
14
14
  import { getRunsDir } from './state.js';
15
15
  import { prepareJobHome, buildSpawnEnv } from './sandbox.js';
16
16
  import { resolveModel, buildReasoningFlags } from './models.js';
17
- import { createTimer, maybeRotate, truncate } from './events.js';
17
+ import { createTimer, maybeRotate, redactPrompt } from './events.js';
18
+ import { normalizeMode } from './exec.js';
18
19
  /** CLI command templates per agent, with {prompt} as a placeholder. */
19
20
  const AGENT_COMMANDS = {
20
21
  claude: ['claude', '-p', '--verbose', '{prompt}', '--output-format', 'stream-json', '--permission-mode', 'plan'],
@@ -23,18 +24,33 @@ const AGENT_COMMANDS = {
23
24
  };
24
25
  /** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
25
26
  export function buildJobCommand(config, resolvedPrompt) {
27
+ // Workflow branch: delegate to `agents run <workflow>` which handles subagent
28
+ // injection, WORKFLOW.md orchestration, and model selection via frontmatter.
29
+ // appendModelAndReasoning is intentionally skipped — the workflow frontmatter
30
+ // owns model selection. No --timeout flag: the runner enforces its own SIGTERM/SIGKILL.
31
+ if (config.workflow) {
32
+ const cmd = ['agents', 'run', config.workflow, resolvedPrompt, '--mode', config.mode];
33
+ return cmd;
34
+ }
26
35
  const template = AGENT_COMMANDS[config.agent];
27
36
  if (!template) {
28
37
  throw new Error(`Unsupported agent for daemon jobs: ${config.agent}`);
29
38
  }
30
39
  let cmd = template.map((part) => part.replace('{prompt}', resolvedPrompt));
40
+ // Canonicalize mode (accepts legacy `full` as alias for `skip`).
41
+ const mode = normalizeMode(config.mode);
31
42
  if (config.agent === 'claude') {
32
- if (config.mode === 'edit') {
43
+ if (mode === 'edit') {
33
44
  const planIndex = cmd.indexOf('plan');
34
45
  if (planIndex !== -1)
35
46
  cmd[planIndex] = 'acceptEdits';
36
47
  }
37
- else if (config.mode === 'full') {
48
+ else if (mode === 'auto') {
49
+ const planIndex = cmd.indexOf('plan');
50
+ if (planIndex !== -1)
51
+ cmd[planIndex] = 'auto';
52
+ }
53
+ else if (mode === 'skip') {
38
54
  // Replace --permission-mode plan with --dangerously-skip-permissions
39
55
  const pmIndex = cmd.indexOf('--permission-mode');
40
56
  if (pmIndex !== -1)
@@ -55,10 +71,10 @@ export function buildJobCommand(config, resolvedPrompt) {
55
71
  appendModelAndReasoning(cmd, config);
56
72
  }
57
73
  if (config.agent === 'codex') {
58
- if (config.mode === 'edit') {
74
+ if (mode === 'edit') {
59
75
  cmd.push('--full-auto');
60
76
  }
61
- else if (config.mode === 'full') {
77
+ else if (mode === 'skip') {
62
78
  // Remove sandbox restriction, just --full-auto
63
79
  const sbIndex = cmd.indexOf('--sandbox');
64
80
  if (sbIndex !== -1)
@@ -68,7 +84,10 @@ export function buildJobCommand(config, resolvedPrompt) {
68
84
  appendModelAndReasoning(cmd, config);
69
85
  }
70
86
  if (config.agent === 'gemini') {
71
- if (config.mode === 'edit' || config.mode === 'full') {
87
+ if (mode === 'edit') {
88
+ cmd.push('--approval-mode', 'auto_edit');
89
+ }
90
+ else if (mode === 'skip') {
72
91
  cmd.push('--yolo');
73
92
  }
74
93
  appendModelAndReasoning(cmd, config);
@@ -114,7 +133,7 @@ export async function executeJob(config) {
114
133
  version: config.version,
115
134
  jobName: config.name,
116
135
  mode: config.mode,
117
- prompt: truncate(config.prompt, 200),
136
+ ...redactPrompt(config.prompt),
118
137
  schedule: config.schedule,
119
138
  });
120
139
  const resolvedPrompt = resolveJobPrompt(config);
@@ -130,10 +149,14 @@ export async function executeJob(config) {
130
149
  if (config.timezone) {
131
150
  spawnEnv.TZ = config.timezone;
132
151
  }
152
+ // Workflows run via `agents run <workflow>` which delegates to claude under the hood.
153
+ // Use 'claude' as the effective agent for report extraction and metadata when workflow is set.
154
+ const effectiveAgent = config.workflow ? 'claude' : config.agent;
133
155
  const meta = {
134
156
  jobName: config.name,
135
157
  runId,
136
- agent: config.agent,
158
+ agent: effectiveAgent,
159
+ ...(config.workflow ? { workflow: config.workflow } : {}),
137
160
  pid: null,
138
161
  status: 'running',
139
162
  startedAt: new Date().toISOString(),
@@ -141,7 +164,7 @@ export async function executeJob(config) {
141
164
  exitCode: null,
142
165
  };
143
166
  writeRunMeta(meta);
144
- const timeoutMs = parseTimeout(config.timeout) || 30 * 60 * 1000;
167
+ const timeoutMs = parseTimeout(config.timeout) || 10 * 60 * 1000;
145
168
  return new Promise((resolve) => {
146
169
  const child = spawn(cmd[0], cmd.slice(1), {
147
170
  stdio: ['ignore', stdoutFd, stdoutFd],
@@ -173,7 +196,7 @@ export async function executeJob(config) {
173
196
  meta.completedAt = new Date().toISOString();
174
197
  writeRunMeta(meta);
175
198
  timer.end({ status: 'timeout', runId });
176
- const reportPath = extractAndSaveReport(stdoutPath, config.agent, runDir);
199
+ const reportPath = extractAndSaveReport(stdoutPath, effectiveAgent, runDir);
177
200
  resolve({ meta, reportPath });
178
201
  }, timeoutMs);
179
202
  child.on('exit', (code) => {
@@ -190,7 +213,7 @@ export async function executeJob(config) {
190
213
  meta.completedAt = new Date().toISOString();
191
214
  writeRunMeta(meta);
192
215
  timer.end({ status: meta.status, exitCode: code ?? undefined, runId });
193
- const reportPath = extractAndSaveReport(stdoutPath, config.agent, runDir);
216
+ const reportPath = extractAndSaveReport(stdoutPath, effectiveAgent, runDir);
194
217
  resolve({ meta, reportPath });
195
218
  });
196
219
  child.on('error', (err) => {
@@ -226,10 +249,12 @@ export async function executeJobDetached(config) {
226
249
  if (config.timezone) {
227
250
  spawnEnv.TZ = config.timezone;
228
251
  }
252
+ const effectiveAgent = config.workflow ? 'claude' : config.agent;
229
253
  const meta = {
230
254
  jobName: config.name,
231
255
  runId,
232
- agent: config.agent,
256
+ agent: effectiveAgent,
257
+ ...(config.workflow ? { workflow: config.workflow } : {}),
233
258
  pid: null,
234
259
  status: 'running',
235
260
  startedAt: new Date().toISOString(),
@@ -307,6 +332,39 @@ export function extractReport(stdoutPath, agentType) {
307
332
  return null;
308
333
  }
309
334
  }
335
+ /** Derive the final status of a detached run by reading the agent's stream-json
336
+ * tail. Detached children fire-and-forget, so we never see their exit code
337
+ * directly — but Claude's stream-json terminates with a `type: result` line
338
+ * that carries `is_error`. If we find it, the run completed cleanly (modulo
339
+ * agent-reported error). If not, the process likely died mid-stream and the
340
+ * caller should treat the run as failed. */
341
+ function inferFinalStatusFromLog(stdoutPath, agent) {
342
+ if (!fs.existsSync(stdoutPath))
343
+ return null;
344
+ try {
345
+ const content = fs.readFileSync(stdoutPath, 'utf-8');
346
+ const lines = content.split('\n').filter((l) => l.trim());
347
+ // Walk backwards over the last few lines — the result marker is always
348
+ // at the tail. Cap the scan so a huge stdout doesn't iterate forever.
349
+ for (let i = lines.length - 1, scanned = 0; i >= 0 && scanned < 20; i--, scanned++) {
350
+ try {
351
+ const parsed = JSON.parse(lines[i]);
352
+ if (agent === 'claude' && parsed.type === 'result') {
353
+ return parsed.is_error
354
+ ? { status: 'failed', exitCode: 1 }
355
+ : { status: 'completed', exitCode: 0 };
356
+ }
357
+ }
358
+ catch {
359
+ // malformed JSONL line — keep scanning
360
+ }
361
+ }
362
+ return null;
363
+ }
364
+ catch {
365
+ return null;
366
+ }
367
+ }
310
368
  /** Scan all runs marked "running" and finalize any whose process has exited. */
311
369
  export function monitorRunningJobs() {
312
370
  const runsDir = getRunsDir();
@@ -332,11 +390,21 @@ export function monitorRunningJobs() {
332
390
  process.kill(meta.pid, 0);
333
391
  }
334
392
  catch { /* process no longer running */
335
- meta.status = 'failed';
393
+ const runDirPath = path.join(jobRunsPath, runDirEntry.name);
394
+ const stdoutPath = path.join(runDirPath, 'stdout.log');
395
+ // Prefer the agent's own success/error marker; fall back to "failed"
396
+ // only when the stream ended without one (process killed mid-run).
397
+ const inferred = inferFinalStatusFromLog(stdoutPath, meta.agent);
398
+ if (inferred) {
399
+ meta.status = inferred.status;
400
+ meta.exitCode = inferred.exitCode;
401
+ }
402
+ else {
403
+ meta.status = 'failed';
404
+ }
336
405
  meta.completedAt = new Date().toISOString();
337
406
  writeRunMeta(meta);
338
- const stdoutPath = path.join(jobRunsPath, runDirEntry.name, 'stdout.log');
339
- extractAndSaveReport(stdoutPath, meta.agent, path.join(jobRunsPath, runDirEntry.name));
407
+ extractAndSaveReport(stdoutPath, meta.agent, runDirPath);
340
408
  }
341
409
  }
342
410
  catch { /* corrupt or unreadable meta.json */ }
@@ -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,15 +5,7 @@
5
5
  <key>files</key>
6
6
  <dict/>
7
7
  <key>files2</key>
8
- <dict>
9
- <key>embedded.provisionprofile</key>
10
- <dict>
11
- <key>hash2</key>
12
- <data>
13
- 2vfA/eR3dTYgNc/fXhdADUPkp5tRIepPzE3FCLfDx4w=
14
- </data>
15
- </dict>
16
- </dict>
8
+ <dict/>
17
9
  <key>rules</key>
18
10
  <dict>
19
11
  <key>^Resources/</key>
@@ -1,15 +1,15 @@
1
1
  /**
2
- * Secret bundles -- named sets of keychain-backed environment variables.
2
+ * Secret bundles named sets of keychain-backed environment variables.
3
3
  *
4
4
  * Bundle metadata (name, description, vars map) is stored in the macOS
5
- * Keychain as a JSON blob under `agents-cli.bundles.<name>`. Bundles created
6
- * with `--icloud-sync` write the metadata to the iCloud-synced keychain so
7
- * the full bundle definition (not just secret values) propagates across
8
- * the user's Macs. Nothing about secrets ever lives in plaintext on disk.
5
+ * Keychain as a JSON blob under `agents-cli.bundles.<name>`. Secret values
6
+ * live one per keychain item under `agents-cli.secrets.<bundle>.<key>`.
7
+ * Every item is device-local and gated by Touch ID / device passcode — see
8
+ * src/lib/secrets/index.ts for the access-control story. Nothing about
9
+ * secrets ever lives in plaintext on disk.
9
10
  *
10
- * Secret values keep their old layout: one keychain item per key under
11
- * `agents-cli.secrets.<bundle>.<key>`, sync-state matching the bundle's
12
- * `icloud_sync` flag.
11
+ * Cross-machine sync is handled by src/lib/secrets/sync.ts via an explicit
12
+ * encrypted export/import flow; the bundle layer is sync-agnostic.
13
13
  */
14
14
  import { type BundleValue, type SecretRef } from './index.js';
15
15
  /** Allowed values for a secret's `type` metadata field. */
@@ -28,8 +28,6 @@ export interface SecretsBundle {
28
28
  name: string;
29
29
  description?: string;
30
30
  allow_exec?: boolean;
31
- /** When true, keychain-backed values and bundle metadata sync via iCloud Keychain. */
32
- icloud_sync?: boolean;
33
31
  /** ISO 8601 UTC timestamp. Set once on the first writeBundle() for a bundle. */
34
32
  created_at?: string;
35
33
  /** ISO 8601 UTC timestamp. Refreshed on every writeBundle(). */
@@ -49,6 +47,7 @@ export declare const RESERVED_ENV_NAMES: Set<string>;
49
47
  export declare function bundleToEnvPrefix(name: string): string;
50
48
  export declare function isReservedEnvName(key: string): boolean;
51
49
  export declare function isLoaderOrInterpreterEnv(name: string): boolean;
50
+ export declare function sanitizeProcessEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
52
51
  /** Validate a bundle name against the allowed pattern. Throws on invalid input. */
53
52
  export declare function validateBundleName(name: string): void;
54
53
  export declare function validateEnvKey(key: string): void;
@@ -74,14 +73,32 @@ export declare function describeBundle(bundle: SecretsBundle): BundleEntryInfo[]
74
73
  /** Options for resolveBundleEnv. */
75
74
  export interface ResolveBundleOptions {
76
75
  /**
77
- * Human-readable label for who is requesting the secrets. Shown to the
78
- * user under the Touch ID prompt so they know what's about to read.
79
- * Example: "agent claude", "command curl", "browser profile".
80
- * When omitted the prompt only names the bundle.
76
+ * Human-readable label for who is requesting the secrets. Currently
77
+ * informational only the helper's Touch ID prompt is set by the OS and
78
+ * cannot be reliably customized once we drop the per-batch reason path,
79
+ * but we keep this in the API so call sites stay explicit about who's
80
+ * about to read the bundle.
81
81
  */
82
82
  caller?: string;
83
83
  }
84
- export declare function resolveBundleEnv(bundle: SecretsBundle, opts?: ResolveBundleOptions): Record<string, string>;
84
+ export declare function resolveBundleEnv(bundle: SecretsBundle, _opts?: ResolveBundleOptions): Record<string, string>;
85
+ /**
86
+ * Read a bundle's metadata AND resolve its env in a single Touch ID prompt.
87
+ *
88
+ * `readBundle` + `resolveBundleEnv` issued two separate `LAContext` calls
89
+ * (metadata read via `get-auth`, then secret values via `get-batch`) which
90
+ * surfaced as two consecutive Touch ID prompts. macOS does not honor
91
+ * "Always Allow" for items protected with `kSecAttrAccessControl`+biometry,
92
+ * so caching at the OS level was never an option. This collapses both reads
93
+ * into one `get-batch` call: we enumerate the bundle's secret items first
94
+ * (silent — `list` returns attrs only and does not trigger biometry) and
95
+ * include the metadata item in the same batch. One prompt, correctly scoped
96
+ * to the bundle name and caller.
97
+ */
98
+ export declare function readAndResolveBundleEnv(name: string, opts?: ResolveBundleOptions): {
99
+ bundle: SecretsBundle;
100
+ env: Record<string, string>;
101
+ };
85
102
  export declare function keychainRef(key: string): string;
86
103
  /** Options for rotateBundleSecret. */
87
104
  export interface RotateOptions {
@@ -114,8 +131,8 @@ export interface RenameOptions {
114
131
  * 4) write new bundle metadata
115
132
  * 5) delete the old per-key keychain items + old metadata
116
133
  *
117
- * Steps 1-4 are reversible. If 5 partially fails (e.g. iCloud Keychain
118
- * hiccup), running `rename` again is a safe no-op for the source items.
134
+ * Steps 1-4 are reversible. If 5 partially fails, running `rename` again is
135
+ * a safe no-op for the source items.
119
136
  */
120
137
  export declare function renameBundle(oldName: string, newName: string, opts?: RenameOptions): void;
121
138
  export declare function keychainItemsForBundle(bundle: SecretsBundle): Array<{