@phnx-labs/agents-cli 1.20.87 → 1.20.89

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 (94) hide show
  1. package/CHANGELOG.md +323 -0
  2. package/README.md +12 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/commands.js +7 -7
  5. package/dist/commands/doctor.d.ts +0 -19
  6. package/dist/commands/doctor.js +219 -305
  7. package/dist/commands/exec.js +7 -19
  8. package/dist/commands/factory.js +26 -2
  9. package/dist/commands/funnel.js +16 -1
  10. package/dist/commands/inspect.js +3 -5
  11. package/dist/commands/menubar.js +117 -34
  12. package/dist/commands/routines.js +25 -3
  13. package/dist/commands/secrets-rotate-passphrase.d.ts +17 -0
  14. package/dist/commands/secrets-rotate-passphrase.js +96 -0
  15. package/dist/commands/secrets.js +2 -0
  16. package/dist/commands/sessions.d.ts +7 -1
  17. package/dist/commands/sessions.js +40 -12
  18. package/dist/commands/ssh.js +3 -3
  19. package/dist/commands/usage.d.ts +3 -2
  20. package/dist/commands/usage.js +2 -9
  21. package/dist/commands/webhook.js +7 -2
  22. package/dist/lib/agents.d.ts +31 -1
  23. package/dist/lib/agents.js +55 -0
  24. package/dist/lib/command-skills.d.ts +10 -0
  25. package/dist/lib/command-skills.js +14 -0
  26. package/dist/lib/commands.js +28 -2
  27. package/dist/lib/daemon.d.ts +29 -0
  28. package/dist/lib/daemon.js +75 -6
  29. package/dist/lib/devices/doctor-findings.d.ts +167 -0
  30. package/dist/lib/devices/doctor-findings.js +893 -0
  31. package/dist/lib/devices/fleet-divergence.d.ts +22 -0
  32. package/dist/lib/devices/fleet-divergence.js +34 -10
  33. package/dist/lib/devices/fleet-inventory.d.ts +17 -6
  34. package/dist/lib/devices/fleet-inventory.js +56 -8
  35. package/dist/lib/events.d.ts +1 -1
  36. package/dist/lib/exec.d.ts +14 -3
  37. package/dist/lib/exec.js +41 -8
  38. package/dist/lib/factory/snapshot.d.ts +78 -0
  39. package/dist/lib/factory/snapshot.js +209 -0
  40. package/dist/lib/fs-atomic.d.ts +14 -1
  41. package/dist/lib/fs-atomic.js +35 -3
  42. package/dist/lib/funnel.d.ts +1 -0
  43. package/dist/lib/funnel.js +8 -0
  44. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  45. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  46. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -2
  47. package/dist/lib/menubar/install-menubar.d.ts +53 -2
  48. package/dist/lib/menubar/install-menubar.js +183 -28
  49. package/dist/lib/platform/process.d.ts +2 -0
  50. package/dist/lib/platform/process.js +5 -3
  51. package/dist/lib/project-resources.js +34 -20
  52. package/dist/lib/resources.d.ts +8 -0
  53. package/dist/lib/resources.js +34 -1
  54. package/dist/lib/routines-placement.d.ts +2 -1
  55. package/dist/lib/routines-placement.js +8 -4
  56. package/dist/lib/routines.d.ts +57 -1
  57. package/dist/lib/routines.js +74 -1
  58. package/dist/lib/runner.d.ts +16 -1
  59. package/dist/lib/runner.js +58 -16
  60. package/dist/lib/sandbox.d.ts +2 -0
  61. package/dist/lib/sandbox.js +38 -0
  62. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  63. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  64. package/dist/lib/secrets/bundles.js +9 -34
  65. package/dist/lib/secrets/filestore.d.ts +152 -34
  66. package/dist/lib/secrets/filestore.js +676 -123
  67. package/dist/lib/secrets/rc-hygiene.d.ts +0 -6
  68. package/dist/lib/secrets/rc-hygiene.js +0 -24
  69. package/dist/lib/session/active.d.ts +6 -6
  70. package/dist/lib/session/active.js +6 -6
  71. package/dist/lib/session/discover.d.ts +5 -0
  72. package/dist/lib/session/discover.js +137 -1
  73. package/dist/lib/session/parse.d.ts +2 -0
  74. package/dist/lib/session/parse.js +76 -37
  75. package/dist/lib/session/remote-active.d.ts +4 -1
  76. package/dist/lib/session/remote-active.js +8 -2
  77. package/dist/lib/session/sync/agents.js +0 -0
  78. package/dist/lib/session/types.d.ts +1 -1
  79. package/dist/lib/session/types.js +1 -1
  80. package/dist/lib/session/viewing-in.d.ts +31 -0
  81. package/dist/lib/session/viewing-in.js +47 -0
  82. package/dist/lib/staleness/detectors/commands.js +14 -5
  83. package/dist/lib/staleness/types.d.ts +2 -0
  84. package/dist/lib/staleness/writers/commands.js +13 -7
  85. package/dist/lib/state.d.ts +17 -0
  86. package/dist/lib/state.js +30 -2
  87. package/dist/lib/triggers/handlers.d.ts +95 -0
  88. package/dist/lib/triggers/handlers.js +384 -0
  89. package/dist/lib/triggers/webhook.d.ts +10 -2
  90. package/dist/lib/triggers/webhook.js +65 -11
  91. package/dist/lib/usage.d.ts +72 -1
  92. package/dist/lib/usage.js +21 -27
  93. package/dist/lib/versions.js +30 -13
  94. package/package.json +1 -1
@@ -26,6 +26,53 @@ import { hostFromPid } from './active.js';
26
26
  import { enumerateGhosttyTabs, assignGhosttyTabs } from './ghostty-tabs.js';
27
27
  import { getTerminalsDir } from '../state.js';
28
28
  const execFileAsync = promisify(execFile);
29
+ /**
30
+ * The one display form of "where is this session being watched" — `'codium tab 3'`
31
+ * when a client is attached, the bare app name when the tab can't be resolved, and
32
+ * `'detached'` when the pane is live but nobody is looking at it (the terminal that
33
+ * displayed it was closed or crashed).
34
+ *
35
+ * `undefined` means **we do not know**, and it covers two different situations that
36
+ * must not be confused with `'detached'`: a session that isn't tmux-hosted (no pane
37
+ * to attach to, so it isn't on this axis at all), and a tmux session whose pane the
38
+ * locator could not resolve (`mapPanesToTargets` returned nothing for this socket,
39
+ * or the row was never enriched). {@link resolveViewingIn} answers `undefined` for
40
+ * BOTH "no client attached" and "could not locate the pane", so the pane's resolved
41
+ * `tmuxTarget` is what separates them: without it there is no evidence of absence,
42
+ * only absence of evidence. Claiming `'detached'` there would invent an orphaned
43
+ * session — and a consumer acts on that claim (Factory's picker pre-ticks every
44
+ * detached row for rescue), so the wrong answer resumes a session nobody asked for.
45
+ *
46
+ * Shared by the `--active` row renderer and the `--json` serializer so a machine
47
+ * consumer reads exactly the string a human sees, instead of re-deriving "detached"
48
+ * from an absent field.
49
+ */
50
+ export function viewingInLabel(s) {
51
+ if (s.provenance?.mux?.kind !== 'tmux' || !s.provenance.mux.pane)
52
+ return undefined;
53
+ if (!s.viewingIn)
54
+ return s.tmuxTarget ? 'detached' : undefined;
55
+ return s.viewingIn.tab != null ? `${s.viewingIn.app} tab ${s.viewingIn.tab}` : s.viewingIn.app;
56
+ }
57
+ /**
58
+ * Wire form -> internal form for a row arriving from another machine's
59
+ * `--active --json`. The fan-out reaches peers whose CLI may predate
60
+ * {@link viewingInLabel} and still emit the `{app, tab}` object, so this boundary
61
+ * normalizes both shapes into one — the ONLY place either shape is accepted.
62
+ * `'detached'` maps to undefined, which is what "no attached client" means
63
+ * internally; {@link viewingInLabel} regenerates the word from the pane.
64
+ */
65
+ export function parseViewingIn(raw) {
66
+ if (raw && typeof raw === 'object') {
67
+ const app = raw.app;
68
+ const tab = raw.tab;
69
+ return typeof app === 'string' ? { app, tab: typeof tab === 'number' ? tab : undefined } : undefined;
70
+ }
71
+ if (typeof raw !== 'string' || !raw || raw === 'detached')
72
+ return undefined;
73
+ const m = raw.match(/^(.+?) tab (\d+)$/);
74
+ return m ? { app: m[1], tab: parseInt(m[2], 10) } : { app: raw };
75
+ }
29
76
  /** Apps whose tab index is published by the extension via live-terminals.json. */
30
77
  const EDITOR_APPS = new Set(['code', 'cursor', 'codium', 'windsurf']);
31
78
  /** The tmux session name a session's pane belongs to, from `session:window.pane`. */
@@ -1,13 +1,14 @@
1
1
  /**
2
- * Commands detector — mirrors versions.ts:343-357. Inspects the version home,
2
+ * Commands detector — mirrors the command dispatch in versions.ts. Inspects the version home,
3
3
  * returns command names. Honors the commands-as-skills marker for skills-only
4
- * agents (kimi, Codex >= 0.117.0, …); falls back to scanning
5
- * `{agentDir}/<commandsSubdir>/` for the native path.
4
+ * agents (Kimi, Codex >= 0.117.0, …), treats the native file as authoritative
5
+ * for dual-write targets (the skill copy is deliberately absent on a name
6
+ * collision), and scans `{agentDir}/<commandsSubdir>/` for native-only targets.
6
7
  */
7
8
  import * as fs from 'fs';
8
9
  import * as path from 'path';
9
10
  import { AGENTS, MANAGED_AGENT_IDS, agentConfigDirName } from '../../agents.js';
10
- import { shouldInstallCommandAsSkill, listCommandSkillsInVersion } from '../../command-skills.js';
11
+ import { listCommandSkillsInVersion, shouldInstallCommandAsSkill, } from '../../command-skills.js';
11
12
  import { lazyAgentMap } from '../writers/lazy-map.js';
12
13
  function buildCommandsDetector(agent) {
13
14
  return {
@@ -23,9 +24,17 @@ function buildCommandsDetector(agent) {
23
24
  if (!fs.existsSync(commandsDir))
24
25
  return [];
25
26
  const ext = agentConfig.format === 'toml' ? '.toml' : '.md';
26
- return fs.readdirSync(commandsDir)
27
+ const nativeCommands = fs.readdirSync(commandsDir)
27
28
  .filter(f => f.endsWith(ext))
28
29
  .map(f => f.replace(new RegExp(`\\${ext}$`), ''));
30
+ // For a dual-write target the native file is the authoritative record that
31
+ // the command synced. The skill copy is derived, and
32
+ // installCommandSkillToVersion deliberately writes none when a real skill
33
+ // source already owns the name -- requiring both copies reported those
34
+ // commands missing forever and drove an `agents refresh` loop no sync could
35
+ // clear. This also matches `agents doctor`/`prune`, which read the
36
+ // unfiltered listCommandsInVersionHome.
37
+ return nativeCommands;
29
38
  },
30
39
  };
31
40
  }
@@ -44,6 +44,8 @@ export interface SyncManifest {
44
44
  v: typeof MANIFEST_VERSION;
45
45
  syncedAt: string;
46
46
  commands: Record<string, FileEntry>;
47
+ /** Command names the version writer emitted during the preceding full sync. */
48
+ writtenCommands?: string[];
47
49
  skills: Record<string, DirEntry>;
48
50
  hooks: Record<string, FileEntry>;
49
51
  rules: RulesEntry;
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Commands writer.
3
3
  *
4
- * Two physical formats, picked per-(agent, version) at write time:
4
+ * Two physical formats, selected per-(agent, version) at write time. Most
5
+ * agents receive one format; dual-write registry targets receive both:
5
6
  *
6
7
  * - command-as-skill — fires when `shouldInstallCommandAsSkill(agent, version)`
7
8
  * is true. Used for Codex >= 0.117.0 (commands capability ends, skills
8
9
  * capability remains) and agents with skills but no native command-file dir
9
- * such as kimi. Writes `{agentDir}/skills/<name>/SKILL.md` with the
10
+ * such as Kimi. Cursor also receives this format in addition to its IDE
11
+ * command file. Writes `{agentDir}/skills/<name>/SKILL.md` with the
10
12
  * `agents_command` marker; the agent picks it up as a slash-command equivalent.
11
13
  *
12
14
  * - native command file — `{agentDir}/<commandsSubdir>/<name>.md` (or .toml
@@ -23,7 +25,7 @@ import { supports } from '../../capabilities.js';
23
25
  import { safeJoin } from '../../paths.js';
24
26
  import { markdownToToml } from '../../convert.js';
25
27
  import { commandAppliesTo, parseCommandMetadata } from '../../commands.js';
26
- import { installCommandSkillToVersion, shouldInstallCommandAsSkill } from '../../command-skills.js';
28
+ import { installCommandSkillToVersion, shouldAlsoInstallCommandAsSkill, shouldInstallCommandAsSkill, } from '../../command-skills.js';
27
29
  import { installGooseCommandToVersion } from '../../goose-commands.js';
28
30
  import { resolveCommandSource, trustedSkillRoots } from './sources.js';
29
31
  import { lazyAgentMap } from './lazy-map.js';
@@ -35,6 +37,7 @@ function buildCommandsWriter(agent) {
35
37
  const agentConfig = AGENTS[agent];
36
38
  const agentDir = path.join(versionHome, agentConfigDirName(agent));
37
39
  const commandsAsSkills = shouldInstallCommandAsSkill(agent, version);
40
+ const commandsAlsoAsSkills = shouldAlsoInstallCommandAsSkill(agent, version);
38
41
  const supportsCommands = supports(agent, 'commands', version).ok;
39
42
  // Version-gated agents (e.g. goose skills >= 1.25.0) are registered but
40
43
  // may be called at a version too old for both paths — skip gracefully.
@@ -54,23 +57,25 @@ function buildCommandsWriter(agent) {
54
57
  const metadata = parseCommandMetadata(srcFile);
55
58
  if (!commandAppliesTo(agent, version, metadata).ok)
56
59
  continue;
57
- if (commandsAsSkills) {
60
+ if (commandsAsSkills || commandsAlsoAsSkills) {
58
61
  const installed = installCommandSkillToVersion(agentDir, cmd, srcFile, skillRoots);
62
+ // installed.skipped means a real skill source already owns this name,
63
+ // which is a deliberate no-op, not a failure — the native file is still written.
59
64
  if (!installed.success)
60
65
  continue;
61
66
  }
62
- else if (agent === 'goose') {
67
+ if (supportsCommands && agent === 'goose') {
63
68
  // Goose: recipe YAML + config.yaml slash_commands entry, not a file copy.
64
69
  const installed = installGooseCommandToVersion(versionHome, cmd, srcFile);
65
70
  if (!installed.success)
66
71
  continue;
67
72
  }
68
- else if (agentConfig.format === 'toml') {
73
+ else if (supportsCommands && agentConfig.format === 'toml') {
69
74
  const content = fs.readFileSync(srcFile, 'utf-8');
70
75
  const tomlContent = markdownToToml(cmd, content);
71
76
  fs.writeFileSync(safeJoin(commandsTarget, `${cmd}.toml`), tomlContent);
72
77
  }
73
- else {
78
+ else if (supportsCommands) {
74
79
  fs.copyFileSync(srcFile, safeJoin(commandsTarget, `${cmd}.md`));
75
80
  }
76
81
  synced.push(cmd);
@@ -84,6 +89,7 @@ function buildCommandsWriter(agent) {
84
89
  // Registration covers two cases:
85
90
  // - native commands (claude, codex < 0.117.0, grok, etc.) — `commands` cap
86
91
  // - commands-as-skills (kimi, codex >= 0.117.0)
92
+ // - dual-write commands plus command-skills (cursor)
87
93
  //
88
94
  // Agents that have skills but use a NATIVE non-file slash-command system
89
95
  // (openclaw → Gateway-based commands) are NOT registered. They declare
@@ -109,6 +109,12 @@ 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 webhook handler YAML definitions (~/.agents/webhooks/). Handlers are
114
+ * one-off triggers for agents/workflows/commands/routines, layered the same way
115
+ * as routines (project > user > system).
116
+ */
117
+ export declare function getWebhooksDir(): string;
112
118
  /**
113
119
  * Path to built-in routine definitions shipped in the system repo
114
120
  * (`~/.agents/.system/routines/`). Unioned under the user routines dir by
@@ -117,6 +123,11 @@ export declare function getRoutinesDir(): string;
117
123
  * disables the built-in). The daemon fires these; the directory need not exist.
118
124
  */
119
125
  export declare function getSystemRoutinesDir(): string;
126
+ /**
127
+ * Path to built-in webhook handler definitions shipped in the system repo
128
+ * (`~/.agents/.system/webhooks/`). Layered under user handlers by `listHandlers()`.
129
+ */
130
+ export declare function getSystemWebhooksDir(): string;
120
131
  /**
121
132
  * Path to a project-scoped routines directory (`<project>/.agents/routines/`),
122
133
  * or null when no project `.agents/` is found by walking up from cwd.
@@ -130,6 +141,12 @@ export declare function getSystemRoutinesDir(): string;
130
141
  * it does not auto-enable firing. See `lib/routines-project.ts`.
131
142
  */
132
143
  export declare function getProjectRoutinesDir(cwd?: string): string | null;
144
+ /**
145
+ * Path to a project-scoped webhook handlers directory
146
+ * (`<project>/.agents/webhooks/`), or null when no project `.agents/` is found
147
+ * by walking up from cwd.
148
+ */
149
+ export declare function getProjectWebhooksDir(cwd?: string): string | null;
133
150
  /** Path to routine execution logs (~/.agents/.history/runs/). */
134
151
  export declare function getRunsDir(): string;
135
152
  /** Path to monitor YAML definitions (~/.agents/monitors/). */
package/dist/lib/state.js CHANGED
@@ -72,6 +72,7 @@ const SYSTEM_PLUGINS_DIR = path.join(SYSTEM_AGENTS_DIR, 'plugins');
72
72
  // Unioned under user routines by listJobs()/readJob() so a routine shipped here
73
73
  // fires for every install, while a user routine of the same name overrides it.
74
74
  const SYSTEM_ROUTINES_DIR = path.join(SYSTEM_AGENTS_DIR, 'routines');
75
+ const SYSTEM_WEBHOOKS_DIR = path.join(SYSTEM_AGENTS_DIR, 'webhooks');
75
76
  const SYSTEM_PROMPTCUTS_FILE = path.join(SYSTEM_AGENTS_DIR, 'hooks', 'promptcuts.yaml');
76
77
  const SYSTEM_MCP_CONFIG_FILE = path.join(SYSTEM_AGENTS_DIR, 'mcp.json');
77
78
  const SYSTEM_INSTRUCTIONS_FILE = path.join(SYSTEM_AGENTS_DIR, 'instructions.md');
@@ -82,6 +83,7 @@ const HISTORY_DIR = path.join(USER_AGENTS_DIR, '.history');
82
83
  const CACHE_DIR = path.join(USER_AGENTS_DIR, '.cache');
83
84
  // Top-level user dirs (config/definitions only — runtime moves into .history/.cache).
84
85
  const ROUTINES_DIR = path.join(USER_AGENTS_DIR, 'routines');
86
+ const WEBHOOKS_DIR = path.join(USER_AGENTS_DIR, 'webhooks');
85
87
  // Monitor definitions (event-triggered watchers). Sibling of ROUTINES_DIR: a
86
88
  // monitor is a routine whose trigger is a watched source instead of a clock.
87
89
  const MONITORS_DIR = path.join(USER_AGENTS_DIR, 'monitors');
@@ -306,7 +308,13 @@ export function getCacheDir() { return CACHE_DIR; }
306
308
  /** Path to cloned packages (~/.agents/.cache/packages/). */
307
309
  export function getPackagesDir() { return PACKAGES_DIR; }
308
310
  /** Path to routine YAML definitions (~/.agents/routines/). */
309
- export function getRoutinesDir() { return ROUTINES_DIR; }
311
+ export function getRoutinesDir() { return process.env.AGENTS_ROUTINES_DIR ?? ROUTINES_DIR; }
312
+ /**
313
+ * Path to webhook handler YAML definitions (~/.agents/webhooks/). Handlers are
314
+ * one-off triggers for agents/workflows/commands/routines, layered the same way
315
+ * as routines (project > user > system).
316
+ */
317
+ export function getWebhooksDir() { return process.env.AGENTS_WEBHOOKS_DIR ?? WEBHOOKS_DIR; }
310
318
  /**
311
319
  * Path to built-in routine definitions shipped in the system repo
312
320
  * (`~/.agents/.system/routines/`). Unioned under the user routines dir by
@@ -314,7 +322,12 @@ export function getRoutinesDir() { return ROUTINES_DIR; }
314
322
  * user routine of the same name overrides it (a user copy with `enabled: false`
315
323
  * disables the built-in). The daemon fires these; the directory need not exist.
316
324
  */
317
- export function getSystemRoutinesDir() { return SYSTEM_ROUTINES_DIR; }
325
+ export function getSystemRoutinesDir() { return process.env.AGENTS_SYSTEM_ROUTINES_DIR ?? SYSTEM_ROUTINES_DIR; }
326
+ /**
327
+ * Path to built-in webhook handler definitions shipped in the system repo
328
+ * (`~/.agents/.system/webhooks/`). Layered under user handlers by `listHandlers()`.
329
+ */
330
+ export function getSystemWebhooksDir() { return process.env.AGENTS_SYSTEM_WEBHOOKS_DIR ?? SYSTEM_WEBHOOKS_DIR; }
318
331
  /**
319
332
  * Path to a project-scoped routines directory (`<project>/.agents/routines/`),
320
333
  * or null when no project `.agents/` is found by walking up from cwd.
@@ -333,6 +346,17 @@ export function getProjectRoutinesDir(cwd = process.cwd()) {
333
346
  return null;
334
347
  return path.join(projectAgentsDir, 'routines');
335
348
  }
349
+ /**
350
+ * Path to a project-scoped webhook handlers directory
351
+ * (`<project>/.agents/webhooks/`), or null when no project `.agents/` is found
352
+ * by walking up from cwd.
353
+ */
354
+ export function getProjectWebhooksDir(cwd = process.cwd()) {
355
+ const projectAgentsDir = getProjectAgentsDir(cwd);
356
+ if (!projectAgentsDir)
357
+ return null;
358
+ return path.join(projectAgentsDir, 'webhooks');
359
+ }
336
360
  /** Path to routine execution logs (~/.agents/.history/runs/). */
337
361
  export function getRunsDir() { return RUNS_DIR; }
338
362
  /** Path to monitor YAML definitions (~/.agents/monitors/). */
@@ -506,6 +530,8 @@ export function ensureAgentsDir() {
506
530
  fs.mkdirSync(PACKAGES_DIR, opts);
507
531
  if (!fs.existsSync(ROUTINES_DIR))
508
532
  fs.mkdirSync(ROUTINES_DIR, opts);
533
+ if (!fs.existsSync(WEBHOOKS_DIR))
534
+ fs.mkdirSync(WEBHOOKS_DIR, opts);
509
535
  if (!fs.existsSync(RUNS_DIR))
510
536
  fs.mkdirSync(RUNS_DIR, opts);
511
537
  if (!fs.existsSync(VERSIONS_DIR))
@@ -524,6 +550,8 @@ export function ensureAgentsDir() {
524
550
  fs.mkdirSync(SYSTEM_PERMISSIONS_DIR, opts);
525
551
  if (!fs.existsSync(SYSTEM_SUBAGENTS_DIR))
526
552
  fs.mkdirSync(SYSTEM_SUBAGENTS_DIR, opts);
553
+ if (!fs.existsSync(SYSTEM_WEBHOOKS_DIR))
554
+ fs.mkdirSync(SYSTEM_WEBHOOKS_DIR, opts);
527
555
  try {
528
556
  fs.chmodSync(SYSTEM_AGENTS_DIR, 0o700);
529
557
  }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Webhook handler config layer.
3
+ *
4
+ * Handlers are one-off triggers stored in `~/.agents/webhooks/*.yml` (plus
5
+ * project/system layers). They complement routine triggers: a matching webhook
6
+ * can run an agent, workflow, shell command, or delegate to a routine.
7
+ */
8
+ import type { JobConfig, RunMeta, WebhookContext } from '../routines.js';
9
+ import type { AgentId } from '../types.js';
10
+ import type { IncomingWebhook, WebhookSource } from './webhook.js';
11
+ export interface WebhookHandler {
12
+ name: string;
13
+ enabled?: boolean;
14
+ devices?: string[];
15
+ source: WebhookSource;
16
+ event?: string;
17
+ action?: string;
18
+ stateTo?: string;
19
+ stateFrom?: string;
20
+ teamKey?: string;
21
+ label?: string;
22
+ repo?: string;
23
+ branch?: string;
24
+ /**
25
+ * Where to run the action. A device name (`yosemite-s0`) runs there over SSH;
26
+ * `fleet` picks any eligible online worker; `fleet/<platform>` (or
27
+ * `<platform>/fleet`, or a bare `linux`/`macos`/`windows`) restricts that pick
28
+ * to one platform. Omitted runs locally.
29
+ */
30
+ host?: string;
31
+ run?: {
32
+ agent?: AgentId;
33
+ workflow?: string;
34
+ command?: string;
35
+ prompt?: string;
36
+ /** Environment variables injected into the spawned process. */
37
+ env?: Record<string, string>;
38
+ };
39
+ routine?: string;
40
+ }
41
+ export interface FiredHandler {
42
+ handlerName: string;
43
+ runId?: string;
44
+ exitCode?: number;
45
+ output?: string;
46
+ }
47
+ export interface HandlerHostResolution {
48
+ /** Resolved execution host, or undefined to run locally. */
49
+ host?: string;
50
+ /** Strategy that should be set on the JobConfig. */
51
+ hostStrategy?: 'host' | 'fleet';
52
+ }
53
+ /**
54
+ * Resolve a handler `host` expression to a concrete host or local execution.
55
+ *
56
+ * - Specific device name (e.g. `yosemite-s0`) → run there over SSH, or locally
57
+ * if it names this machine.
58
+ * - `fleet` → pick any online worker device.
59
+ * - `fleet/<platform>` or `<platform>/fleet` (e.g. `fleet/linux`, `linux/fleet`)
60
+ * → pick any online worker on that platform. `linux` alone is accepted as a
61
+ * shorthand for `fleet/linux`.
62
+ *
63
+ * Throws when a fleet expression matches no eligible device, rather than
64
+ * silently falling back to this machine — `fleet/linux` must never land on a
65
+ * macOS box.
66
+ */
67
+ export declare function resolveHandlerHost(host: string | undefined): HandlerHostResolution;
68
+ /**
69
+ * List all webhook handlers, scanning project > user > system webhook dirs.
70
+ * Higher layers shadow lower ones of the same name (first-seen wins).
71
+ */
72
+ export declare function listHandlers(cwd?: string): WebhookHandler[];
73
+ /** Pure matcher: does this handler match the incoming webhook? */
74
+ export declare function handlerMatchesWebhook(handler: WebhookHandler, webhook: IncomingWebhook): boolean;
75
+ /**
76
+ * Build the variable-substitution context for a webhook. Linear events expose
77
+ * `issue` and `updatedFrom`; GitHub events expose `repository`, `pull_request`,
78
+ * and `issue`.
79
+ */
80
+ export declare function buildWebhookContext(webhook: IncomingWebhook): WebhookContext;
81
+ export interface ExecuteHandlerOptions {
82
+ dispatchAgent?: (config: JobConfig) => Promise<RunMeta>;
83
+ dispatchWorkflow?: (config: JobConfig) => Promise<RunMeta>;
84
+ execCommand?: (command: string) => Promise<{
85
+ exitCode: number;
86
+ output: string;
87
+ }>;
88
+ dispatchRoutine?: (config: JobConfig) => Promise<RunMeta>;
89
+ }
90
+ /**
91
+ * Execute a handler's action. Runs the configured agent/workflow/command or
92
+ * delegates to a routine, substituting `{{...}}` placeholders from the webhook
93
+ * context.
94
+ */
95
+ export declare function executeHandler(handler: WebhookHandler, webhook: IncomingWebhook, opts?: ExecuteHandlerOptions): Promise<FiredHandler>;