@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.
- package/CHANGELOG.md +67 -0
- package/README.md +70 -10
- package/dist/commands/browser.js +88 -16
- package/dist/commands/cli.d.ts +14 -0
- package/dist/commands/cli.js +244 -0
- package/dist/commands/commands.js +3 -3
- package/dist/commands/computer.js +18 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/exec.js +3 -3
- package/dist/commands/factory.d.ts +3 -14
- package/dist/commands/factory.js +3 -3
- package/dist/commands/hooks.js +3 -3
- package/dist/commands/mcp.js +29 -0
- package/dist/commands/plugins.js +11 -4
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/prune.js +39 -160
- package/dist/commands/pull.js +56 -3
- package/dist/commands/routines.js +106 -13
- package/dist/commands/secrets.js +6 -8
- package/dist/commands/sessions.d.ts +36 -7
- package/dist/commands/sessions.js +130 -53
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +37 -28
- package/dist/commands/skills.js +3 -3
- package/dist/commands/teams.js +13 -0
- package/dist/commands/versions.d.ts +4 -3
- package/dist/commands/versions.js +147 -124
- package/dist/commands/view.js +12 -12
- package/dist/index.js +34 -6
- package/dist/lib/acp/harnesses.js +8 -0
- package/dist/lib/agents.js +162 -9
- package/dist/lib/browser/cdp.d.ts +8 -1
- package/dist/lib/browser/cdp.js +40 -3
- package/dist/lib/browser/chrome.d.ts +13 -0
- package/dist/lib/browser/chrome.js +42 -3
- package/dist/lib/browser/domain-skills.d.ts +51 -0
- package/dist/lib/browser/domain-skills.js +157 -0
- package/dist/lib/browser/drivers/local.js +45 -4
- package/dist/lib/browser/drivers/ssh.js +1 -1
- package/dist/lib/browser/ipc.d.ts +8 -1
- package/dist/lib/browser/ipc.js +37 -28
- package/dist/lib/browser/profiles.d.ts +13 -0
- package/dist/lib/browser/profiles.js +41 -1
- package/dist/lib/browser/service.d.ts +3 -0
- package/dist/lib/browser/service.js +21 -5
- package/dist/lib/browser/types.d.ts +7 -0
- package/dist/lib/cli-resources.d.ts +109 -0
- package/dist/lib/cli-resources.js +255 -0
- package/dist/lib/cloud/rush.js +5 -5
- package/dist/lib/command-skills.js +0 -2
- package/dist/lib/computer-rpc.d.ts +3 -0
- package/dist/lib/computer-rpc.js +53 -0
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/exec.d.ts +3 -2
- package/dist/lib/exec.js +62 -6
- package/dist/lib/hooks.js +182 -0
- package/dist/lib/mcp.js +6 -0
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/overdue.d.ts +26 -0
- package/dist/lib/overdue.js +101 -0
- package/dist/lib/permissions.js +5 -1
- package/dist/lib/plugin-marketplace.js +1 -1
- package/dist/lib/profiles-presets.js +37 -0
- package/dist/lib/registry.d.ts +18 -0
- package/dist/lib/registry.js +44 -0
- package/dist/lib/resources/mcp.js +43 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/rotate.js +10 -4
- package/dist/lib/routines-format.d.ts +35 -0
- package/dist/lib/routines-format.js +173 -0
- package/dist/lib/routines.d.ts +7 -1
- package/dist/lib/routines.js +32 -12
- package/dist/lib/runner.js +19 -5
- package/dist/lib/scheduler.js +8 -1
- package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/CodeResources +0 -0
- package/dist/lib/secrets/{AgentsKeychain.app/Contents/Info.plist → Agents CLI.app/Contents/Info.plist } +4 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.d.ts +33 -2
- package/dist/lib/secrets/bundles.js +249 -26
- package/dist/lib/secrets/index.d.ts +10 -1
- package/dist/lib/secrets/index.js +143 -48
- package/dist/lib/session/active.d.ts +8 -0
- package/dist/lib/session/active.js +3 -2
- package/dist/lib/session/db.d.ts +10 -4
- package/dist/lib/session/db.js +16 -16
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +44 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +6 -2
- package/dist/lib/shims.js +88 -10
- package/dist/lib/state.d.ts +0 -1
- package/dist/lib/state.js +2 -15
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +153 -3
- package/dist/lib/teams/summarizer.js +18 -2
- package/dist/lib/teams/worktree.js +14 -3
- package/dist/lib/types.d.ts +7 -4
- package/dist/lib/types.js +6 -3
- package/dist/lib/versions.d.ts +10 -2
- package/dist/lib/versions.js +227 -35
- package/package.json +9 -9
- package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
- package/npm-shrinkwrap.json +0 -3162
- /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/_CodeSignature/CodeResources +0 -0
- /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
|
+
}
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -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. "
|
|
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[];
|
package/dist/lib/routines.js
CHANGED
|
@@ -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: '
|
|
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 === '
|
|
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
|
-
|
|
123
|
-
|
|
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 (
|
|
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
|
|
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. "
|
|
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
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
|
|
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) {
|
package/dist/lib/runner.js
CHANGED
|
@@ -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:
|
|
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) ||
|
|
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,
|
|
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,
|
|
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:
|
|
245
|
+
agent: effectiveAgent,
|
|
246
|
+
...(config.workflow ? { workflow: config.workflow } : {}),
|
|
233
247
|
pid: null,
|
|
234
248
|
status: 'running',
|
|
235
249
|
startedAt: new Date().toISOString(),
|
package/dist/lib/scheduler.js
CHANGED
|
@@ -24,7 +24,14 @@ export class JobScheduler {
|
|
|
24
24
|
}
|
|
25
25
|
schedule(config) {
|
|
26
26
|
this.unschedule(config.name);
|
|
27
|
-
|
|
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
|
}
|
|
Binary file
|
|
@@ -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>
|
|
8
|
+
<string>Agents CLI</string>
|
|
9
|
+
<key>CFBundleDisplayName</key>
|
|
10
|
+
<string>Agents CLI</string>
|
|
9
11
|
<key>CFBundleExecutable</key>
|
|
10
|
-
<string>
|
|
12
|
+
<string>Agents CLI</string>
|
|
11
13
|
<key>CFBundlePackageType</key>
|
|
12
14
|
<string>APPL</string>
|
|
13
15
|
<key>CFBundleVersion</key>
|
|
Binary file
|
|
@@ -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
|
|
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
|
-
|
|
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 {
|