@phnx-labs/agents-cli 1.20.61 → 1.20.63
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 +57 -0
- package/README.md +10 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +13 -3
- package/dist/commands/exec.js +51 -1
- package/dist/commands/funnel.d.ts +5 -0
- package/dist/commands/funnel.js +62 -0
- package/dist/commands/hosts.js +42 -0
- package/dist/commands/repo.d.ts +4 -4
- package/dist/commands/repo.js +30 -19
- package/dist/commands/routines.js +73 -16
- package/dist/commands/sessions-sync.d.ts +1 -0
- package/dist/commands/sessions-sync.js +16 -2
- package/dist/commands/sessions.js +8 -1
- package/dist/commands/setup.js +9 -0
- package/dist/commands/ssh.js +72 -2
- package/dist/commands/sync-provision.d.ts +23 -0
- package/dist/commands/sync-provision.js +107 -0
- package/dist/commands/webhook.d.ts +9 -0
- package/dist/commands/webhook.js +93 -0
- package/dist/index.js +6 -2
- package/dist/lib/agents.d.ts +26 -0
- package/dist/lib/agents.js +100 -28
- package/dist/lib/browser/ipc.js +5 -4
- package/dist/lib/browser/profiles.d.ts +13 -0
- package/dist/lib/browser/profiles.js +17 -0
- package/dist/lib/browser/service.d.ts +12 -1
- package/dist/lib/browser/service.js +48 -13
- package/dist/lib/browser/sessions-list.d.ts +40 -0
- package/dist/lib/browser/sessions-list.js +190 -0
- package/dist/lib/commands.js +29 -0
- package/dist/lib/convert.d.ts +11 -0
- package/dist/lib/convert.js +22 -0
- package/dist/lib/daemon.js +2 -0
- package/dist/lib/devices/fleet.d.ts +62 -0
- package/dist/lib/devices/fleet.js +128 -0
- package/dist/lib/doctor-diff.js +17 -1
- package/dist/lib/funnel.d.ts +5 -0
- package/dist/lib/funnel.js +23 -0
- package/dist/lib/git.d.ts +21 -5
- package/dist/lib/git.js +64 -14
- package/dist/lib/goose-commands.d.ts +41 -0
- package/dist/lib/goose-commands.js +176 -0
- package/dist/lib/hooks.js +134 -0
- package/dist/lib/hosts/credentials.d.ts +28 -0
- package/dist/lib/hosts/credentials.js +48 -0
- package/dist/lib/hosts/dispatch.d.ts +25 -0
- package/dist/lib/hosts/dispatch.js +68 -2
- package/dist/lib/hosts/passthrough.d.ts +13 -10
- package/dist/lib/hosts/passthrough.js +119 -29
- package/dist/lib/mailbox-gc.js +4 -16
- package/dist/lib/migrate.d.ts +12 -0
- package/dist/lib/migrate.js +55 -1
- package/dist/lib/permissions.d.ts +15 -0
- package/dist/lib/permissions.js +99 -0
- package/dist/lib/plugins.d.ts +20 -0
- package/dist/lib/plugins.js +118 -0
- package/dist/lib/resources/permissions.js +2 -0
- package/dist/lib/routines.d.ts +29 -10
- package/dist/lib/routines.js +47 -15
- package/dist/lib/session/active.d.ts +8 -1
- package/dist/lib/session/active.js +1 -0
- package/dist/lib/session/parse.js +12 -0
- package/dist/lib/session/state.d.ts +33 -0
- package/dist/lib/session/state.js +40 -0
- package/dist/lib/session/sync/agents.d.ts +2 -0
- package/dist/lib/session/sync/agents.js +39 -1
- package/dist/lib/session/sync/config.d.ts +8 -0
- package/dist/lib/session/sync/config.js +6 -1
- package/dist/lib/session/sync/provision.d.ts +49 -0
- package/dist/lib/session/sync/provision.js +91 -0
- package/dist/lib/session/sync/sync.d.ts +3 -0
- package/dist/lib/session/sync/sync.js +26 -6
- package/dist/lib/session/sync/transcript-crypto.d.ts +77 -0
- package/dist/lib/session/sync/transcript-crypto.js +147 -0
- package/dist/lib/staleness/detectors/permissions.js +23 -0
- package/dist/lib/staleness/detectors/subagents.js +36 -0
- package/dist/lib/staleness/writers/commands.js +7 -0
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/staleness/writers/subagents.js +22 -3
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +11 -0
- package/dist/lib/state.d.ts +10 -2
- package/dist/lib/state.js +14 -2
- package/dist/lib/subagents.d.ts +32 -0
- package/dist/lib/subagents.js +238 -0
- package/dist/lib/triggers/webhook.d.ts +70 -27
- package/dist/lib/triggers/webhook.js +264 -43
- package/package.json +1 -1
|
@@ -66,6 +66,8 @@ export const loadMessage = async () => (await import('../../commands/message.js'
|
|
|
66
66
|
export const loadFeed = async () => (await import('../../commands/feed.js')).registerFeedCommand;
|
|
67
67
|
export const loadServe = async () => (await import('../../commands/serve.js')).registerServeCommand;
|
|
68
68
|
export const loadAudit = async () => (await import('../../commands/audit.js')).registerAuditCommands;
|
|
69
|
+
export const loadWebhook = async () => (await import('../../commands/webhook.js')).registerWebhookCommand;
|
|
70
|
+
export const loadFunnel = async () => (await import('../../commands/funnel.js')).registerFunnelCommand;
|
|
69
71
|
/**
|
|
70
72
|
* Commands whose modules pull in the SQLite-backed session/cloud stack. They are
|
|
71
73
|
* registered AFTER `applyGlobalHelpConventions` (mirroring main's order: help
|
|
@@ -151,8 +153,15 @@ export const COMMAND_LOADERS = {
|
|
|
151
153
|
events: [loadEvents],
|
|
152
154
|
ssh: [loadSsh],
|
|
153
155
|
devices: [loadSsh],
|
|
156
|
+
// `fleet` is a commander alias of `devices` (see commands/ssh.ts); list it so
|
|
157
|
+
// lazy registration loads the devices tree when the user types `agents fleet`.
|
|
158
|
+
fleet: [loadSsh],
|
|
154
159
|
pull: [loadPull],
|
|
155
160
|
push: [loadPush],
|
|
161
|
+
// `repos` is the canonical command name; `repo` remains a convenience alias
|
|
162
|
+
// (see commands/repo.ts). List both so lazy registration loads the tree
|
|
163
|
+
// whichever the user types.
|
|
164
|
+
repos: [loadRepo],
|
|
156
165
|
repo: [loadRepo],
|
|
157
166
|
setup: [loadSetup],
|
|
158
167
|
sessions: [loadSessions],
|
|
@@ -162,4 +171,6 @@ export const COMMAND_LOADERS = {
|
|
|
162
171
|
feed: [loadFeed],
|
|
163
172
|
serve: [loadServe],
|
|
164
173
|
audit: [loadAudit],
|
|
174
|
+
webhook: [loadWebhook],
|
|
175
|
+
funnel: [loadFunnel],
|
|
165
176
|
};
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -109,13 +109,21 @@ export declare function getCacheDir(): string;
|
|
|
109
109
|
export declare function getPackagesDir(): string;
|
|
110
110
|
/** Path to routine YAML definitions (~/.agents/routines/). */
|
|
111
111
|
export declare function getRoutinesDir(): string;
|
|
112
|
+
/**
|
|
113
|
+
* Path to built-in routine definitions shipped in the system repo
|
|
114
|
+
* (`~/.agents/.system/routines/`). Unioned under the user routines dir by
|
|
115
|
+
* listJobs()/readJob(): a routine shipped here fires for every install, and a
|
|
116
|
+
* user routine of the same name overrides it (a user copy with `enabled: false`
|
|
117
|
+
* disables the built-in). The daemon fires these; the directory need not exist.
|
|
118
|
+
*/
|
|
119
|
+
export declare function getSystemRoutinesDir(): string;
|
|
112
120
|
/**
|
|
113
121
|
* Path to a project-scoped routines directory (`<project>/.agents/routines/`),
|
|
114
122
|
* or null when no project `.agents/` is found by walking up from cwd.
|
|
115
123
|
*
|
|
116
124
|
* Project routines participate in `list`/`view`/`run` for inspection but are
|
|
117
|
-
* NOT fired by the daemon (which runs from $HOME and only
|
|
118
|
-
* Opt-in firing for project routines is tracked as a follow-up.
|
|
125
|
+
* NOT fired by the daemon (which runs from $HOME and loads only user + system
|
|
126
|
+
* routines). Opt-in firing for project routines is tracked as a follow-up.
|
|
119
127
|
*/
|
|
120
128
|
export declare function getProjectRoutinesDir(cwd?: string): string | null;
|
|
121
129
|
/** Path to routine execution logs (~/.agents/.history/runs/). */
|
package/dist/lib/state.js
CHANGED
|
@@ -69,6 +69,10 @@ const SYSTEM_PERMISSIONS_DIR = path.join(SYSTEM_AGENTS_DIR, 'permissions');
|
|
|
69
69
|
const SYSTEM_SUBAGENTS_DIR = path.join(SYSTEM_AGENTS_DIR, 'subagents');
|
|
70
70
|
const SYSTEM_WORKFLOWS_DIR = path.join(SYSTEM_AGENTS_DIR, 'workflows');
|
|
71
71
|
const SYSTEM_PLUGINS_DIR = path.join(SYSTEM_AGENTS_DIR, 'plugins');
|
|
72
|
+
// Built-in routines shipped in the system repo (gh:phnx-labs/.agents-system).
|
|
73
|
+
// Unioned under user routines by listJobs()/readJob() so a routine shipped here
|
|
74
|
+
// fires for every install, while a user routine of the same name overrides it.
|
|
75
|
+
const SYSTEM_ROUTINES_DIR = path.join(SYSTEM_AGENTS_DIR, 'routines');
|
|
72
76
|
const SYSTEM_PROMPTCUTS_FILE = path.join(SYSTEM_AGENTS_DIR, 'hooks', 'promptcuts.yaml');
|
|
73
77
|
const SYSTEM_MCP_CONFIG_FILE = path.join(SYSTEM_AGENTS_DIR, 'mcp.json');
|
|
74
78
|
const SYSTEM_INSTRUCTIONS_FILE = path.join(SYSTEM_AGENTS_DIR, 'instructions.md');
|
|
@@ -296,13 +300,21 @@ export function getCacheDir() { return CACHE_DIR; }
|
|
|
296
300
|
export function getPackagesDir() { return PACKAGES_DIR; }
|
|
297
301
|
/** Path to routine YAML definitions (~/.agents/routines/). */
|
|
298
302
|
export function getRoutinesDir() { return ROUTINES_DIR; }
|
|
303
|
+
/**
|
|
304
|
+
* Path to built-in routine definitions shipped in the system repo
|
|
305
|
+
* (`~/.agents/.system/routines/`). Unioned under the user routines dir by
|
|
306
|
+
* listJobs()/readJob(): a routine shipped here fires for every install, and a
|
|
307
|
+
* user routine of the same name overrides it (a user copy with `enabled: false`
|
|
308
|
+
* disables the built-in). The daemon fires these; the directory need not exist.
|
|
309
|
+
*/
|
|
310
|
+
export function getSystemRoutinesDir() { return SYSTEM_ROUTINES_DIR; }
|
|
299
311
|
/**
|
|
300
312
|
* Path to a project-scoped routines directory (`<project>/.agents/routines/`),
|
|
301
313
|
* or null when no project `.agents/` is found by walking up from cwd.
|
|
302
314
|
*
|
|
303
315
|
* Project routines participate in `list`/`view`/`run` for inspection but are
|
|
304
|
-
* NOT fired by the daemon (which runs from $HOME and only
|
|
305
|
-
* Opt-in firing for project routines is tracked as a follow-up.
|
|
316
|
+
* NOT fired by the daemon (which runs from $HOME and loads only user + system
|
|
317
|
+
* routines). Opt-in firing for project routines is tracked as a follow-up.
|
|
306
318
|
*/
|
|
307
319
|
export function getProjectRoutinesDir(cwd = process.cwd()) {
|
|
308
320
|
const projectAgentsDir = getProjectAgentsDir(cwd);
|
package/dist/lib/subagents.d.ts
CHANGED
|
@@ -68,6 +68,27 @@ export declare function transformSubagentForDroid(subagentDir: string): string;
|
|
|
68
68
|
* See GitHub docs for custom agents.
|
|
69
69
|
*/
|
|
70
70
|
export declare const transformSubagentForCopilot: typeof transformSubagentForDroid;
|
|
71
|
+
/**
|
|
72
|
+
* Transform a subagent into a Cursor CLI custom subagent `.md` file.
|
|
73
|
+
*
|
|
74
|
+
* Cursor loads subagents from `.cursor/agents/*.md` (project) or
|
|
75
|
+
* `~/.cursor/agents/*.md` (user) — Markdown with YAML frontmatter
|
|
76
|
+
* (name, description, model; also readonly/is_background, which our
|
|
77
|
+
* frontmatter schema doesn't carry). Cursor has no `color` field, so this is
|
|
78
|
+
* an alias of transformSubagentForDroid, same as Copilot.
|
|
79
|
+
* See https://cursor.com/docs/subagents.
|
|
80
|
+
*/
|
|
81
|
+
export declare const transformSubagentForCursor: typeof transformSubagentForDroid;
|
|
82
|
+
/**
|
|
83
|
+
* Transform a subagent into a ForgeCode custom subagent `.md` file.
|
|
84
|
+
*
|
|
85
|
+
* ForgeCode loads named agents from `.forge/agents/*.md` (project) or
|
|
86
|
+
* `~/.forge/agents/*.md` (user) — Markdown with YAML frontmatter (id, title,
|
|
87
|
+
* description, tools, model, temperature) + a system-prompt body. ForgeCode has
|
|
88
|
+
* no `color` field, so this is an alias of transformSubagentForDroid, same as
|
|
89
|
+
* Copilot/Cursor. See https://forgecode.dev/docs/agent-definition-guide/.
|
|
90
|
+
*/
|
|
91
|
+
export declare const transformSubagentForForge: typeof transformSubagentForDroid;
|
|
71
92
|
/**
|
|
72
93
|
* Transform a subagent into Antigravity's custom-agent markdown shape.
|
|
73
94
|
*
|
|
@@ -138,6 +159,17 @@ export declare function transformSubagentForCodex(subagentDir: string): string;
|
|
|
138
159
|
* set so the subagent can actually run.
|
|
139
160
|
*/
|
|
140
161
|
export declare function transformSubagentForKiro(subagentDir: string): string;
|
|
162
|
+
/**
|
|
163
|
+
* Transform a subagent into a Goose recipe YAML file.
|
|
164
|
+
*
|
|
165
|
+
* Goose has no dedicated subagent file format — a named subagent IS a recipe.
|
|
166
|
+
* Goose resolves named agents from `~/.config/goose/agents/<name>.yaml` (global)
|
|
167
|
+
* and delegates to them by name in autonomous mode. The recipe schema mirrors the
|
|
168
|
+
* one agents-cli already emits for Goose workflow recipes (`writeGooseSubrecipe`):
|
|
169
|
+
* `version`, `title`, `description`, `instructions`, `prompt`, plus an optional
|
|
170
|
+
* `settings.goose_model`. See goose-docs.ai context-engineering/subagents.
|
|
171
|
+
*/
|
|
172
|
+
export declare function transformSubagentForGoose(subagentDir: string): string;
|
|
141
173
|
/**
|
|
142
174
|
* Sync a subagent to an OpenClaw workspace
|
|
143
175
|
* Copies full directory, renames AGENT.md to AGENTS.md
|
package/dist/lib/subagents.js
CHANGED
|
@@ -274,6 +274,27 @@ export function transformSubagentForDroid(subagentDir) {
|
|
|
274
274
|
* See GitHub docs for custom agents.
|
|
275
275
|
*/
|
|
276
276
|
export const transformSubagentForCopilot = transformSubagentForDroid;
|
|
277
|
+
/**
|
|
278
|
+
* Transform a subagent into a Cursor CLI custom subagent `.md` file.
|
|
279
|
+
*
|
|
280
|
+
* Cursor loads subagents from `.cursor/agents/*.md` (project) or
|
|
281
|
+
* `~/.cursor/agents/*.md` (user) — Markdown with YAML frontmatter
|
|
282
|
+
* (name, description, model; also readonly/is_background, which our
|
|
283
|
+
* frontmatter schema doesn't carry). Cursor has no `color` field, so this is
|
|
284
|
+
* an alias of transformSubagentForDroid, same as Copilot.
|
|
285
|
+
* See https://cursor.com/docs/subagents.
|
|
286
|
+
*/
|
|
287
|
+
export const transformSubagentForCursor = transformSubagentForDroid;
|
|
288
|
+
/**
|
|
289
|
+
* Transform a subagent into a ForgeCode custom subagent `.md` file.
|
|
290
|
+
*
|
|
291
|
+
* ForgeCode loads named agents from `.forge/agents/*.md` (project) or
|
|
292
|
+
* `~/.forge/agents/*.md` (user) — Markdown with YAML frontmatter (id, title,
|
|
293
|
+
* description, tools, model, temperature) + a system-prompt body. ForgeCode has
|
|
294
|
+
* no `color` field, so this is an alias of transformSubagentForDroid, same as
|
|
295
|
+
* Copilot/Cursor. See https://forgecode.dev/docs/agent-definition-guide/.
|
|
296
|
+
*/
|
|
297
|
+
export const transformSubagentForForge = transformSubagentForDroid;
|
|
277
298
|
/**
|
|
278
299
|
* Transform a subagent into Antigravity's custom-agent markdown shape.
|
|
279
300
|
*
|
|
@@ -493,6 +514,45 @@ export function transformSubagentForKiro(subagentDir) {
|
|
|
493
514
|
}
|
|
494
515
|
return JSON.stringify(config, null, 2);
|
|
495
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* Transform a subagent into a Goose recipe YAML file.
|
|
519
|
+
*
|
|
520
|
+
* Goose has no dedicated subagent file format — a named subagent IS a recipe.
|
|
521
|
+
* Goose resolves named agents from `~/.config/goose/agents/<name>.yaml` (global)
|
|
522
|
+
* and delegates to them by name in autonomous mode. The recipe schema mirrors the
|
|
523
|
+
* one agents-cli already emits for Goose workflow recipes (`writeGooseSubrecipe`):
|
|
524
|
+
* `version`, `title`, `description`, `instructions`, `prompt`, plus an optional
|
|
525
|
+
* `settings.goose_model`. See goose-docs.ai context-engineering/subagents.
|
|
526
|
+
*/
|
|
527
|
+
export function transformSubagentForGoose(subagentDir) {
|
|
528
|
+
const agentMd = path.join(subagentDir, 'AGENT.md');
|
|
529
|
+
const frontmatter = parseSubagentFrontmatter(agentMd);
|
|
530
|
+
const body = getSubagentBody(agentMd);
|
|
531
|
+
if (!frontmatter) {
|
|
532
|
+
throw new Error(`Invalid AGENT.md in ${subagentDir}`);
|
|
533
|
+
}
|
|
534
|
+
const files = fs.readdirSync(subagentDir)
|
|
535
|
+
.filter(f => f.endsWith('.md') && f !== 'AGENT.md')
|
|
536
|
+
.sort();
|
|
537
|
+
let prompt = body || frontmatter.description || frontmatter.name;
|
|
538
|
+
for (const file of files) {
|
|
539
|
+
const content = fs.readFileSync(path.join(subagentDir, file), 'utf-8').trim();
|
|
540
|
+
const sectionName = file.replace('.md', '');
|
|
541
|
+
const title = sectionName.charAt(0).toUpperCase() + sectionName.slice(1).toLowerCase();
|
|
542
|
+
prompt += `\n\n## ${title}\n\n${content}`;
|
|
543
|
+
}
|
|
544
|
+
const recipe = {
|
|
545
|
+
version: '1.0.0',
|
|
546
|
+
title: frontmatter.name || path.basename(subagentDir),
|
|
547
|
+
description: frontmatter.description || frontmatter.name || path.basename(subagentDir),
|
|
548
|
+
instructions: prompt,
|
|
549
|
+
prompt,
|
|
550
|
+
};
|
|
551
|
+
if (frontmatter.model) {
|
|
552
|
+
recipe.settings = { goose_model: frontmatter.model };
|
|
553
|
+
}
|
|
554
|
+
return yaml.stringify(recipe);
|
|
555
|
+
}
|
|
496
556
|
/**
|
|
497
557
|
* Sync a subagent to an OpenClaw workspace
|
|
498
558
|
* Copies full directory, renames AGENT.md to AGENTS.md
|
|
@@ -609,6 +669,51 @@ export function installSubagentToAgent(subagentDir, subagentName, agent, agentHo
|
|
|
609
669
|
return { success: false, error: String(err) };
|
|
610
670
|
}
|
|
611
671
|
}
|
|
672
|
+
else if (agent === 'cursor') {
|
|
673
|
+
// Cursor: flattened .md custom subagent under ~/.cursor/agents/
|
|
674
|
+
const agentsDir = path.join(agentHome, '.cursor', 'agents');
|
|
675
|
+
if (!fs.existsSync(agentsDir)) {
|
|
676
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
const transformed = transformSubagentForCursor(subagentDir);
|
|
680
|
+
fs.writeFileSync(safeJoin(agentsDir, `${subagentName}.md`), transformed);
|
|
681
|
+
return { success: true };
|
|
682
|
+
}
|
|
683
|
+
catch (err) {
|
|
684
|
+
return { success: false, error: String(err) };
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
else if (agent === 'forge') {
|
|
688
|
+
// ForgeCode: flattened .md custom subagent under ~/.forge/agents/
|
|
689
|
+
const agentsDir = path.join(agentHome, '.forge', 'agents');
|
|
690
|
+
if (!fs.existsSync(agentsDir)) {
|
|
691
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
const transformed = transformSubagentForForge(subagentDir);
|
|
695
|
+
fs.writeFileSync(safeJoin(agentsDir, `${subagentName}.md`), transformed);
|
|
696
|
+
return { success: true };
|
|
697
|
+
}
|
|
698
|
+
catch (err) {
|
|
699
|
+
return { success: false, error: String(err) };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
else if (agent === 'goose') {
|
|
703
|
+
// Goose: recipe YAML custom-agent file under ~/.config/goose/agents/
|
|
704
|
+
const agentsDir = path.join(agentHome, '.config', 'goose', 'agents');
|
|
705
|
+
if (!fs.existsSync(agentsDir)) {
|
|
706
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
const transformed = transformSubagentForGoose(subagentDir);
|
|
710
|
+
fs.writeFileSync(safeJoin(agentsDir, `${subagentName}.yaml`), transformed);
|
|
711
|
+
return { success: true };
|
|
712
|
+
}
|
|
713
|
+
catch (err) {
|
|
714
|
+
return { success: false, error: String(err) };
|
|
715
|
+
}
|
|
716
|
+
}
|
|
612
717
|
else {
|
|
613
718
|
// Other agents don't support subagents yet
|
|
614
719
|
return { success: false, error: `Agent '${agent}' does not support subagents` };
|
|
@@ -670,6 +775,27 @@ export function removeSubagentFromAgent(subagentName, agent, agentHome) {
|
|
|
670
775
|
}
|
|
671
776
|
return { success: true };
|
|
672
777
|
}
|
|
778
|
+
else if (agent === 'cursor') {
|
|
779
|
+
const targetPath = safeJoin(path.join(agentHome, '.cursor', 'agents'), `${subagentName}.md`);
|
|
780
|
+
if (fs.existsSync(targetPath)) {
|
|
781
|
+
fs.unlinkSync(targetPath);
|
|
782
|
+
}
|
|
783
|
+
return { success: true };
|
|
784
|
+
}
|
|
785
|
+
else if (agent === 'forge') {
|
|
786
|
+
const targetPath = safeJoin(path.join(agentHome, '.forge', 'agents'), `${subagentName}.md`);
|
|
787
|
+
if (fs.existsSync(targetPath)) {
|
|
788
|
+
fs.unlinkSync(targetPath);
|
|
789
|
+
}
|
|
790
|
+
return { success: true };
|
|
791
|
+
}
|
|
792
|
+
else if (agent === 'goose') {
|
|
793
|
+
const targetPath = safeJoin(path.join(agentHome, '.config', 'goose', 'agents'), `${subagentName}.yaml`);
|
|
794
|
+
if (fs.existsSync(targetPath)) {
|
|
795
|
+
fs.unlinkSync(targetPath);
|
|
796
|
+
}
|
|
797
|
+
return { success: true };
|
|
798
|
+
}
|
|
673
799
|
else {
|
|
674
800
|
return { success: true }; // No-op for unsupported agents
|
|
675
801
|
}
|
|
@@ -886,6 +1012,61 @@ export function listSubagentsForAgent(agentId, home) {
|
|
|
886
1012
|
});
|
|
887
1013
|
}
|
|
888
1014
|
}
|
|
1015
|
+
else if (agentId === 'cursor') {
|
|
1016
|
+
// Cursor: flat `<name>.md` files under ~/.cursor/agents/
|
|
1017
|
+
const agentsDir = path.join(home, '.cursor', 'agents');
|
|
1018
|
+
if (!fs.existsSync(agentsDir))
|
|
1019
|
+
return subagents;
|
|
1020
|
+
for (const file of fs.readdirSync(agentsDir)) {
|
|
1021
|
+
if (!file.endsWith('.md'))
|
|
1022
|
+
continue;
|
|
1023
|
+
const filePath = path.join(agentsDir, file);
|
|
1024
|
+
if (!fs.statSync(filePath).isFile())
|
|
1025
|
+
continue;
|
|
1026
|
+
const name = file.replace(/\.md$/, '');
|
|
1027
|
+
const frontmatter = parseSubagentFrontmatter(filePath) ?? { name, description: '' };
|
|
1028
|
+
subagents.push({ name, path: filePath, files: [file], frontmatter });
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
else if (agentId === 'forge') {
|
|
1032
|
+
// ForgeCode: flat `<name>.md` files under ~/.forge/agents/
|
|
1033
|
+
const agentsDir = path.join(home, '.forge', 'agents');
|
|
1034
|
+
if (!fs.existsSync(agentsDir))
|
|
1035
|
+
return subagents;
|
|
1036
|
+
for (const file of fs.readdirSync(agentsDir)) {
|
|
1037
|
+
if (!file.endsWith('.md'))
|
|
1038
|
+
continue;
|
|
1039
|
+
const filePath = path.join(agentsDir, file);
|
|
1040
|
+
if (!fs.statSync(filePath).isFile())
|
|
1041
|
+
continue;
|
|
1042
|
+
const name = file.replace(/\.md$/, '');
|
|
1043
|
+
const frontmatter = parseSubagentFrontmatter(filePath) ?? { name, description: '' };
|
|
1044
|
+
subagents.push({ name, path: filePath, files: [file], frontmatter });
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
else if (agentId === 'goose') {
|
|
1048
|
+
// Goose: recipe YAML files under ~/.config/goose/agents/
|
|
1049
|
+
const agentsDir = path.join(home, '.config', 'goose', 'agents');
|
|
1050
|
+
if (!fs.existsSync(agentsDir))
|
|
1051
|
+
return subagents;
|
|
1052
|
+
for (const file of fs.readdirSync(agentsDir)) {
|
|
1053
|
+
if (!file.endsWith('.yaml'))
|
|
1054
|
+
continue;
|
|
1055
|
+
const filePath = path.join(agentsDir, file);
|
|
1056
|
+
if (!fs.statSync(filePath).isFile())
|
|
1057
|
+
continue;
|
|
1058
|
+
const name = file.replace(/\.yaml$/, '');
|
|
1059
|
+
let frontmatter;
|
|
1060
|
+
try {
|
|
1061
|
+
const recipe = yaml.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
1062
|
+
frontmatter = { name: recipe?.title || name, description: recipe?.description || '' };
|
|
1063
|
+
}
|
|
1064
|
+
catch {
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
subagents.push({ name, path: filePath, files: [file], frontmatter });
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
889
1070
|
return subagents;
|
|
890
1071
|
}
|
|
891
1072
|
/**
|
|
@@ -984,6 +1165,42 @@ export function diffVersionSubagents(agent, version) {
|
|
|
984
1165
|
}
|
|
985
1166
|
}
|
|
986
1167
|
}
|
|
1168
|
+
else if (agent === 'cursor') {
|
|
1169
|
+
const agentsDir = path.join(versionHome, '.cursor', 'agents');
|
|
1170
|
+
if (fs.existsSync(agentsDir)) {
|
|
1171
|
+
for (const file of fs.readdirSync(agentsDir)) {
|
|
1172
|
+
if (!file.endsWith('.md'))
|
|
1173
|
+
continue;
|
|
1174
|
+
const name = path.basename(file, '.md');
|
|
1175
|
+
if (!discovered.has(name))
|
|
1176
|
+
orphans.push(name);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
else if (agent === 'forge') {
|
|
1181
|
+
const agentsDir = path.join(versionHome, '.forge', 'agents');
|
|
1182
|
+
if (fs.existsSync(agentsDir)) {
|
|
1183
|
+
for (const file of fs.readdirSync(agentsDir)) {
|
|
1184
|
+
if (!file.endsWith('.md'))
|
|
1185
|
+
continue;
|
|
1186
|
+
const name = path.basename(file, '.md');
|
|
1187
|
+
if (!discovered.has(name))
|
|
1188
|
+
orphans.push(name);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
else if (agent === 'goose') {
|
|
1193
|
+
const agentsDir = path.join(versionHome, '.config', 'goose', 'agents');
|
|
1194
|
+
if (fs.existsSync(agentsDir)) {
|
|
1195
|
+
for (const file of fs.readdirSync(agentsDir)) {
|
|
1196
|
+
if (!file.endsWith('.yaml'))
|
|
1197
|
+
continue;
|
|
1198
|
+
const name = path.basename(file, '.yaml');
|
|
1199
|
+
if (!discovered.has(name))
|
|
1200
|
+
orphans.push(name);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
987
1204
|
return { agent, version, orphans: orphans.sort() };
|
|
988
1205
|
}
|
|
989
1206
|
/**
|
|
@@ -1071,6 +1288,27 @@ export function removeSubagentFromVersion(agent, version, subagentName) {
|
|
|
1071
1288
|
fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.json.${stamp}`));
|
|
1072
1289
|
}
|
|
1073
1290
|
}
|
|
1291
|
+
else if (agent === 'cursor') {
|
|
1292
|
+
const targetPath = path.join(versionHome, '.cursor', 'agents', `${subagentName}.md`);
|
|
1293
|
+
if (fs.existsSync(targetPath)) {
|
|
1294
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
1295
|
+
fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.md.${stamp}`));
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
else if (agent === 'forge') {
|
|
1299
|
+
const targetPath = path.join(versionHome, '.forge', 'agents', `${subagentName}.md`);
|
|
1300
|
+
if (fs.existsSync(targetPath)) {
|
|
1301
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
1302
|
+
fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.md.${stamp}`));
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
else if (agent === 'goose') {
|
|
1306
|
+
const targetPath = path.join(versionHome, '.config', 'goose', 'agents', `${subagentName}.yaml`);
|
|
1307
|
+
if (fs.existsSync(targetPath)) {
|
|
1308
|
+
fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
|
|
1309
|
+
fs.renameSync(targetPath, path.join(trashDir, `${subagentName}.yaml.${stamp}`));
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1074
1312
|
return { success: true };
|
|
1075
1313
|
}
|
|
1076
1314
|
catch (err) {
|
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Public webhook trigger receiver for routines.
|
|
3
3
|
*
|
|
4
4
|
* A routine may declare a `trigger` block instead of (or alongside) a cron
|
|
5
5
|
* `schedule` (see `JobConfig.trigger` in `../routines.ts`). This module turns
|
|
6
|
-
*
|
|
7
|
-
* dispatches those routines through the exact same path a cron fire uses
|
|
6
|
+
* incoming GitHub or Linear webhooks into the set of routines they should fire,
|
|
7
|
+
* and dispatches those routines through the exact same path a cron fire uses
|
|
8
8
|
* (`executeJobDetached`).
|
|
9
9
|
*
|
|
10
|
-
* The matching logic
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The matching logic is pure, so it can be unit-tested without a daemon or HTTP
|
|
11
|
+
* server. The listener adds the public-ingress requirements: raw-body HMAC
|
|
12
|
+
* verification, idempotency, source allow-listing, and rate limiting.
|
|
13
13
|
*/
|
|
14
14
|
import * as http from 'http';
|
|
15
|
+
import type { IncomingHttpHeaders } from 'http';
|
|
15
16
|
import type { JobConfig, RunMeta } from '../routines.js';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*/
|
|
21
|
-
export interface GithubWebhook {
|
|
22
|
-
/** The GitHub event name, e.g. `pull_request`, `push`, `issue_comment`. */
|
|
17
|
+
export type WebhookSource = 'github' | 'linear';
|
|
18
|
+
export interface IncomingWebhook {
|
|
19
|
+
/** Delivery source, derived from `/hooks/<source>` or one-shot command flags. */
|
|
20
|
+
source: WebhookSource;
|
|
21
|
+
/** Source event name: GitHub header event or Linear payload `type`. */
|
|
23
22
|
event: string;
|
|
24
|
-
/**
|
|
23
|
+
/** Decoded JSON request body. */
|
|
25
24
|
payload: Record<string, unknown>;
|
|
26
25
|
}
|
|
26
|
+
export type GithubWebhook = IncomingWebhook & {
|
|
27
|
+
source: 'github';
|
|
28
|
+
};
|
|
29
|
+
export type LinearWebhook = IncomingWebhook & {
|
|
30
|
+
source: 'linear';
|
|
31
|
+
};
|
|
27
32
|
/** Read `repository.full_name` (`owner/name`) from a webhook payload, if present. */
|
|
28
33
|
export declare function webhookRepo(payload: Record<string, unknown>): string | null;
|
|
29
34
|
/**
|
|
@@ -37,14 +42,13 @@ export declare function webhookRepo(payload: Record<string, unknown>): string |
|
|
|
37
42
|
*/
|
|
38
43
|
export declare function webhookBranches(event: string, payload: Record<string, unknown>): string[];
|
|
39
44
|
/** True when a single job's trigger matches the given webhook. Pure. */
|
|
40
|
-
export declare function jobMatchesWebhook(job: JobConfig, webhook:
|
|
45
|
+
export declare function jobMatchesWebhook(job: JobConfig, webhook: IncomingWebhook): boolean;
|
|
41
46
|
/**
|
|
42
47
|
* Pure matcher: given a set of jobs and an incoming webhook, return the jobs
|
|
43
|
-
* whose `trigger` matches
|
|
44
|
-
*
|
|
45
|
-
* time-based jobs are unaffected by webhook delivery.
|
|
48
|
+
* whose `trigger` matches. Jobs without a trigger (schedule-only routines) are
|
|
49
|
+
* never selected — proving time-based jobs are unaffected by webhook delivery.
|
|
46
50
|
*/
|
|
47
|
-
export declare function matchJobsToWebhook(jobs: JobConfig[], webhook:
|
|
51
|
+
export declare function matchJobsToWebhook(jobs: JobConfig[], webhook: IncomingWebhook): JobConfig[];
|
|
48
52
|
/** Options for firing webhook-matched jobs (dispatch is injectable for tests). */
|
|
49
53
|
export interface FireWebhookOptions {
|
|
50
54
|
/** Job source. Defaults to all persisted routines (`listJobs()`). */
|
|
@@ -55,31 +59,70 @@ export interface FireWebhookOptions {
|
|
|
55
59
|
* matching without spawning real agent processes.
|
|
56
60
|
*/
|
|
57
61
|
dispatch?: (config: JobConfig) => Promise<RunMeta>;
|
|
62
|
+
/** Matched job names that already completed for this delivery. */
|
|
63
|
+
skipJobNames?: ReadonlySet<string>;
|
|
64
|
+
/** Called immediately after a single matched job dispatch succeeds. */
|
|
65
|
+
onJobFired?: (job: JobConfig, fired: FiredJob) => void;
|
|
58
66
|
}
|
|
59
67
|
/** Result of firing one matched job. */
|
|
60
68
|
export interface FiredJob {
|
|
61
69
|
jobName: string;
|
|
62
70
|
runId: string;
|
|
63
71
|
}
|
|
72
|
+
export declare class WebhookDispatchError extends Error {
|
|
73
|
+
readonly fired: FiredJob[];
|
|
74
|
+
readonly failures: {
|
|
75
|
+
jobName: string;
|
|
76
|
+
error: Error;
|
|
77
|
+
}[];
|
|
78
|
+
constructor(message: string, fired: FiredJob[], failures: {
|
|
79
|
+
jobName: string;
|
|
80
|
+
error: Error;
|
|
81
|
+
}[]);
|
|
82
|
+
}
|
|
64
83
|
/**
|
|
65
84
|
* Match an incoming webhook against the persisted routines and fire each match
|
|
66
85
|
* through the cron dispatch path. Returns one entry per fired job.
|
|
67
86
|
*/
|
|
68
|
-
export declare function fireWebhookJobs(webhook:
|
|
87
|
+
export declare function fireWebhookJobs(webhook: IncomingWebhook, options?: FireWebhookOptions): Promise<FiredJob[]>;
|
|
88
|
+
export declare function verifyGithubSignature(headers: IncomingHttpHeaders, rawBody: Buffer, secret: string): boolean;
|
|
89
|
+
export declare function verifyLinearSignature(headers: IncomingHttpHeaders, rawBody: Buffer, secret: string): boolean;
|
|
90
|
+
export declare function verifyLinearTimestamp(payload: Record<string, unknown>, now?: number, toleranceMs?: number): boolean;
|
|
91
|
+
export interface WebhookSecrets {
|
|
92
|
+
github?: string;
|
|
93
|
+
linear?: string;
|
|
94
|
+
}
|
|
95
|
+
export interface DeliveryStore {
|
|
96
|
+
seen(id: string): boolean;
|
|
97
|
+
mark(id: string): void;
|
|
98
|
+
completedJobs(id: string): ReadonlySet<string>;
|
|
99
|
+
markJob(id: string, jobName: string): void;
|
|
100
|
+
}
|
|
101
|
+
export declare function createMemoryDeliveryStore(maxEntries?: number): DeliveryStore;
|
|
102
|
+
export interface RateLimiter {
|
|
103
|
+
take(key: string): boolean;
|
|
104
|
+
}
|
|
105
|
+
export declare function createMemoryRateLimiter(limit: number, windowMs: number): RateLimiter;
|
|
69
106
|
/** Options for the local webhook http listener. */
|
|
70
107
|
export interface WebhookServerOptions {
|
|
71
108
|
port?: number;
|
|
72
109
|
host?: string;
|
|
110
|
+
/** HMAC signing secrets keyed by source. */
|
|
111
|
+
secrets: WebhookSecrets;
|
|
73
112
|
/** Override the fire options (mainly for tests). */
|
|
74
113
|
fire?: FireWebhookOptions;
|
|
75
114
|
/** Called after each delivery is handled (mainly for tests/observability). */
|
|
76
|
-
onDelivery?: (
|
|
115
|
+
onDelivery?: (webhook: IncomingWebhook, fired: FiredJob[]) => void;
|
|
116
|
+
deliveryStore?: DeliveryStore;
|
|
117
|
+
rateLimiter?: RateLimiter;
|
|
118
|
+
rateLimitPerMinute?: number;
|
|
119
|
+
maxBodyBytes?: number;
|
|
77
120
|
}
|
|
78
121
|
/**
|
|
79
|
-
* Start a
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* the
|
|
122
|
+
* Start a localhost-bound receiver. It accepts only:
|
|
123
|
+
* POST /hooks/github with X-Hub-Signature-256
|
|
124
|
+
* POST /hooks/linear with Linear-Signature + fresh webhookTimestamp
|
|
125
|
+
*
|
|
126
|
+
* Returns the underlying server so callers can `close()` it.
|
|
84
127
|
*/
|
|
85
|
-
export declare function startWebhookServer(options
|
|
128
|
+
export declare function startWebhookServer(options: WebhookServerOptions): http.Server;
|