@phnx-labs/agents-cli 1.20.5 → 1.20.6

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 (49) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/browser.js +31 -4
  3. package/dist/commands/computer.js +10 -2
  4. package/dist/commands/defaults.d.ts +7 -0
  5. package/dist/commands/defaults.js +89 -0
  6. package/dist/commands/exec.js +24 -6
  7. package/dist/commands/rules.js +3 -3
  8. package/dist/commands/secrets.js +46 -9
  9. package/dist/commands/setup.js +2 -2
  10. package/dist/commands/teams.js +108 -11
  11. package/dist/commands/view.d.ts +12 -1
  12. package/dist/commands/view.js +121 -38
  13. package/dist/index.js +38 -21
  14. package/dist/lib/agents.d.ts +10 -6
  15. package/dist/lib/agents.js +23 -14
  16. package/dist/lib/browser/chrome.d.ts +10 -0
  17. package/dist/lib/browser/chrome.js +84 -3
  18. package/dist/lib/exec.js +24 -4
  19. package/dist/lib/migrate.js +6 -4
  20. package/dist/lib/permissions.d.ts +23 -0
  21. package/dist/lib/permissions.js +89 -7
  22. package/dist/lib/plugin-marketplace.js +1 -1
  23. package/dist/lib/project-launch.d.ts +5 -0
  24. package/dist/lib/project-launch.js +37 -0
  25. package/dist/lib/pty-server.js +7 -4
  26. package/dist/lib/resources/rules.js +1 -1
  27. package/dist/lib/resources/skills.js +1 -1
  28. package/dist/lib/resources.d.ts +2 -0
  29. package/dist/lib/resources.js +2 -1
  30. package/dist/lib/rotate.js +6 -18
  31. package/dist/lib/run-config.d.ts +9 -0
  32. package/dist/lib/run-config.js +35 -0
  33. package/dist/lib/run-defaults.d.ts +42 -0
  34. package/dist/lib/run-defaults.js +180 -0
  35. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  36. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  37. package/dist/lib/secrets/install-helper.d.ts +11 -3
  38. package/dist/lib/secrets/install-helper.js +48 -6
  39. package/dist/lib/secrets/linux.d.ts +12 -0
  40. package/dist/lib/secrets/linux.js +30 -16
  41. package/dist/lib/shims.d.ts +9 -1
  42. package/dist/lib/shims.js +35 -3
  43. package/dist/lib/staleness/detectors/hooks.js +1 -1
  44. package/dist/lib/staleness/writers/hooks.js +1 -1
  45. package/dist/lib/teams/api.d.ts +67 -0
  46. package/dist/lib/teams/api.js +78 -0
  47. package/dist/lib/types.d.ts +15 -6
  48. package/dist/lib/versions.js +4 -4
  49. package/package.json +5 -2
package/dist/lib/exec.js CHANGED
@@ -118,6 +118,7 @@ export function buildExecEnv(options) {
118
118
  }
119
119
  delete result.CODEX_HOME;
120
120
  delete result.COPILOT_HOME;
121
+ delete result.KIMI_CODE_HOME;
121
122
  }
122
123
  else if (options.agent === 'codex') {
123
124
  const cwd = options.cwd || process.cwd();
@@ -130,6 +131,7 @@ export function buildExecEnv(options) {
130
131
  }
131
132
  delete result.CLAUDE_CONFIG_DIR;
132
133
  delete result.COPILOT_HOME;
134
+ delete result.KIMI_CODE_HOME;
133
135
  }
134
136
  else if (options.agent === 'copilot') {
135
137
  // Copilot honors COPILOT_HOME (relocates ~/.copilot, including settings,
@@ -145,11 +147,28 @@ export function buildExecEnv(options) {
145
147
  }
146
148
  delete result.CLAUDE_CONFIG_DIR;
147
149
  delete result.CODEX_HOME;
150
+ delete result.KIMI_CODE_HOME;
151
+ }
152
+ else if (options.agent === 'kimi') {
153
+ // Kimi honors KIMI_CODE_HOME (relocates ~/.kimi-code, including config,
154
+ // skills, hooks, sessions). Pin it at the per-version home.
155
+ const cwd = options.cwd || process.cwd();
156
+ const resolvedVersion = options.version ?? resolveVersion('kimi', cwd);
157
+ const version = options.version
158
+ ? resolvedVersion
159
+ : (resolvedVersion && isVersionInstalled('kimi', resolvedVersion) ? resolvedVersion : null);
160
+ if (version) {
161
+ result.KIMI_CODE_HOME = path.join(getVersionHomePath('kimi', version), '.kimi-code');
162
+ }
163
+ delete result.CLAUDE_CONFIG_DIR;
164
+ delete result.CODEX_HOME;
165
+ delete result.COPILOT_HOME;
148
166
  }
149
167
  else {
150
168
  delete result.CLAUDE_CONFIG_DIR;
151
169
  delete result.CODEX_HOME;
152
170
  delete result.COPILOT_HOME;
171
+ delete result.KIMI_CODE_HOME;
153
172
  }
154
173
  return {
155
174
  ...result,
@@ -322,14 +341,15 @@ export const AGENT_COMMANDS = {
322
341
  modelFlag: '--model',
323
342
  },
324
343
  kimi: {
325
- base: ['kimi-code'],
344
+ base: ['kimi'],
326
345
  promptFlag: '-p',
327
346
  modeFlags: {
328
- plan: [],
347
+ plan: ['--plan'],
329
348
  edit: [],
330
- skip: [],
349
+ auto: ['--auto'],
350
+ skip: ['--yolo'],
331
351
  },
332
- jsonFlags: [],
352
+ jsonFlags: ['--output-format', 'stream-json'],
333
353
  modelFlag: '--model',
334
354
  },
335
355
  };
@@ -8,6 +8,7 @@ import * as fs from 'fs';
8
8
  import * as path from 'path';
9
9
  import * as os from 'os';
10
10
  import * as yaml from 'yaml';
11
+ import { AGENTS, agentConfigDirName } from './agents.js';
11
12
  const HOME = process.env.HOME ?? os.homedir();
12
13
  const USER_DIR = path.join(HOME, '.agents');
13
14
  /** Canonical system-repo location (post-fold). */
@@ -672,12 +673,13 @@ function repairAgentConfigSymlinks() {
672
673
  }
673
674
  let repaired = 0;
674
675
  for (const { agent, version } of defaults) {
675
- const userTarget = fs.existsSync(path.join(HISTORY_DIR, 'versions', agent, version, 'home', `.${agent}`))
676
- ? path.join(HISTORY_DIR, 'versions', agent, version, 'home', `.${agent}`)
677
- : path.join(USER_DIR, 'versions', agent, version, 'home', `.${agent}`);
676
+ const configDirName = agent in AGENTS ? agentConfigDirName(agent) : `.${agent}`;
677
+ const userTarget = fs.existsSync(path.join(HISTORY_DIR, 'versions', agent, version, 'home', configDirName))
678
+ ? path.join(HISTORY_DIR, 'versions', agent, version, 'home', configDirName)
679
+ : path.join(USER_DIR, 'versions', agent, version, 'home', configDirName);
678
680
  if (!fs.existsSync(userTarget))
679
681
  continue;
680
- const symlinkPath = path.join(HOME, `.${agent}`);
682
+ const symlinkPath = path.join(HOME, configDirName);
681
683
  let stat = null;
682
684
  try {
683
685
  stat = fs.lstatSync(symlinkPath);
@@ -144,6 +144,29 @@ export type GrokRule = {
144
144
  tool: string;
145
145
  pattern?: string;
146
146
  };
147
+ export type KimiRule = {
148
+ decision: 'allow' | 'deny';
149
+ pattern: string;
150
+ };
151
+ /**
152
+ * Convert a canonical permission set to Kimi Code's `[permission].rules` format.
153
+ * Kimi (`~/.kimi-code/config.toml`) reads rules of the form
154
+ * [[permission.rules]]
155
+ * decision = "allow"
156
+ * pattern = "Bash(git status*)"
157
+ * Tool names are capitalized and the Bash arg-glob uses a trailing `*` (no
158
+ * Claude `:*` separator). Without this conversion the canonical strings match
159
+ * nothing in Kimi's engine and every tool call falls through to a prompt.
160
+ *
161
+ * Each `:*` Bash rule expands to TWO patterns (`cmd*` and `cmd*​/**`) so the
162
+ * command auto-approves whether or not its arguments contain a slash — see
163
+ * `kimiBashPatterns` for why Kimi's picomatch matcher needs both.
164
+ */
165
+ export declare function convertToKimiFormat(set: PermissionSet): {
166
+ permission: {
167
+ rules: KimiRule[];
168
+ };
169
+ };
147
170
  /**
148
171
  * Convert canonical permission set to OpenCode format.
149
172
  * OpenCode uses: { permission: { bash: { "git *": "allow", "rm *": "deny" } } }
@@ -557,6 +557,94 @@ function canonicalToGrokRule(perm, action) {
557
557
  }
558
558
  return { action, tool, pattern };
559
559
  }
560
+ /**
561
+ * Parse a canonical permission string preserving the tool's original casing.
562
+ * `parseCanonicalPattern` lowercases the tool name, which is fine for Grok
563
+ * (lowercase tool vocabulary) but wrong for Kimi, whose tool names are
564
+ * capitalized (`Bash`, `Read`, `Grep`). Bare tool names (no parens, e.g.
565
+ * `Read` or an MCP id like `mcp__server__tool`) return `pattern: null`.
566
+ */
567
+ function parseCanonicalPreserveCase(perm) {
568
+ const m = perm.match(/^([\w-]+)\((.*)\)$/);
569
+ if (m)
570
+ return { tool: m[1], pattern: m[2] };
571
+ return { tool: perm, pattern: null };
572
+ }
573
+ /**
574
+ * Translate a canonical Bash arg-glob (`cmd:*`) into the Kimi pattern(s) that
575
+ * actually match that command's invocations.
576
+ *
577
+ * Kimi matches Bash arg-globs with picomatch, where `*` does NOT cross `/` and a
578
+ * `**` only globstars when it is its own path segment (`*​/**`, `**​/`). A plain
579
+ * `cmd*` therefore matches `git status -s` but NOT `git push origin feat/x` or
580
+ * `cat dir/file` — any argument containing a slash falls through to a prompt
581
+ * (verified interactively against kimi 0.12.1). We emit TWO patterns so the
582
+ * command auto-approves whether its args contain a slash or not:
583
+ * - `cmd*` — no-slash args (and the bare command; `*` is zero-or-more).
584
+ * - `cmd*​/**` — args with a path: `*` consumes up to the first `/`, then the
585
+ * bounded globstar crosses the remaining slashes.
586
+ * "git push:*" -> ["git push*", "git push*​/**"].
587
+ */
588
+ function kimiBashPatterns(pattern) {
589
+ if (pattern === '*' || pattern === '**')
590
+ return ['*'];
591
+ if (pattern.endsWith(':*')) {
592
+ const prefix = pattern.slice(0, -2);
593
+ return [`${prefix}*`, `${prefix}*/**`];
594
+ }
595
+ // Exact command (no `:*`, e.g. `env`, `pwd`, `true`) — no path args expected.
596
+ return [pattern];
597
+ }
598
+ function canonicalToKimiRules(perm, decision) {
599
+ if (BLANKET_BASH_FORMS.has(perm)) {
600
+ return [{ decision, pattern: 'Bash' }];
601
+ }
602
+ const { tool, pattern } = parseCanonicalPreserveCase(perm);
603
+ // Bare tool name (no parens) — name-only match. Covers `Read`, `Grep`, and
604
+ // MCP tool ids, which Kimi can only match by name anyway.
605
+ if (pattern === null) {
606
+ return [{ decision, pattern: tool }];
607
+ }
608
+ if (tool.toLowerCase() === 'bash') {
609
+ return kimiBashPatterns(pattern).map((p) => ({
610
+ decision,
611
+ pattern: p === '*' ? 'Bash' : `Bash(${p})`,
612
+ }));
613
+ }
614
+ // Non-Bash built-ins (Read/Write/Edit/Grep/Glob/WebFetch...) share Kimi's
615
+ // capitalized tool vocabulary, so pass the tool+pattern through. A `**`/`*`
616
+ // glob means "any" — collapse to a name-only rule.
617
+ if (pattern === '*' || pattern === '**') {
618
+ return [{ decision, pattern: tool }];
619
+ }
620
+ return [{ decision, pattern: `${tool}(${pattern})` }];
621
+ }
622
+ /**
623
+ * Convert a canonical permission set to Kimi Code's `[permission].rules` format.
624
+ * Kimi (`~/.kimi-code/config.toml`) reads rules of the form
625
+ * [[permission.rules]]
626
+ * decision = "allow"
627
+ * pattern = "Bash(git status*)"
628
+ * Tool names are capitalized and the Bash arg-glob uses a trailing `*` (no
629
+ * Claude `:*` separator). Without this conversion the canonical strings match
630
+ * nothing in Kimi's engine and every tool call falls through to a prompt.
631
+ *
632
+ * Each `:*` Bash rule expands to TWO patterns (`cmd*` and `cmd*​/**`) so the
633
+ * command auto-approves whether or not its arguments contain a slash — see
634
+ * `kimiBashPatterns` for why Kimi's picomatch matcher needs both.
635
+ */
636
+ export function convertToKimiFormat(set) {
637
+ const rules = [];
638
+ for (const perm of set.allow) {
639
+ rules.push(...canonicalToKimiRules(perm, 'allow'));
640
+ }
641
+ if (set.deny) {
642
+ for (const perm of set.deny) {
643
+ rules.push(...canonicalToKimiRules(perm, 'deny'));
644
+ }
645
+ }
646
+ return { permission: { rules } };
647
+ }
560
648
  /**
561
649
  * Convert canonical permission set to OpenCode format.
562
650
  * OpenCode uses: { permission: { bash: { "git *": "allow", "rm *": "deny" } } }
@@ -1107,13 +1195,7 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
1107
1195
  if (fs.existsSync(configPath)) {
1108
1196
  config = TOML.parse(fs.readFileSync(configPath, 'utf-8'));
1109
1197
  }
1110
- const newRules = [];
1111
- for (const allow of set.allow) {
1112
- newRules.push({ decision: 'allow', pattern: allow });
1113
- }
1114
- for (const deny of set.deny || []) {
1115
- newRules.push({ decision: 'deny', pattern: deny });
1116
- }
1198
+ const newRules = convertToKimiFormat(set).permission.rules;
1117
1199
  if (merge) {
1118
1200
  const existingPermission = (typeof config.permission === 'object' && config.permission !== null && !Array.isArray(config.permission))
1119
1201
  ? config.permission
@@ -26,9 +26,9 @@
26
26
  * the catalog name and on-disk layout are derived, never hard-coded per call.
27
27
  */
28
28
  import * as fs from 'fs';
29
+ import { agentConfigDirName } from './agents.js';
29
30
  import * as path from 'path';
30
31
  import { getPluginsDir, getEnabledExtraRepos, getProjectPluginsDir } from './state.js';
31
- import { agentConfigDirName } from './agents.js';
32
32
  /**
33
33
  * Canonical name for the user-repo marketplace (~/.agents/plugins/). Kept as an
34
34
  * exported constant for callers that operate on the user repo directly and for
@@ -60,6 +60,11 @@ export interface LaunchSyncResult {
60
60
  /**
61
61
  * Run the launch-time project compile. Safe to call on every agent launch:
62
62
  * each step is idempotent and skips when its inputs are missing.
63
+ *
64
+ * After a successful run, touches the shim-side skip-fast sentinel at
65
+ * `~/.agents/.cache/launch-sync/<agent>@<version>@<projectslug>` so the next
66
+ * shim invocation can skip the node spawn entirely when no source dir is
67
+ * newer than the sentinel (shim schema v17+).
63
68
  */
64
69
  export declare function runLaunchSync(opts: LaunchSyncOptions): LaunchSyncResult;
65
70
  export { pluginInstallDir };
@@ -42,6 +42,7 @@
42
42
  */
43
43
  import * as crypto from 'crypto';
44
44
  import * as fs from 'fs';
45
+ import * as os from 'os';
45
46
  import * as path from 'path';
46
47
  import { supports } from './capabilities.js';
47
48
  import { getEnabledExtraRepos, getExtraPluginsDir, getPluginsDir, getProjectAgentsDir, getProjectPluginsDir, getSystemPluginsDir, } from './state.js';
@@ -52,6 +53,11 @@ import { MARKETPLACE_NAME, PROJECT_MARKETPLACE_NAME, SYSTEM_MARKETPLACE_NAME, ad
52
53
  /**
53
54
  * Run the launch-time project compile. Safe to call on every agent launch:
54
55
  * each step is idempotent and skips when its inputs are missing.
56
+ *
57
+ * After a successful run, touches the shim-side skip-fast sentinel at
58
+ * `~/.agents/.cache/launch-sync/<agent>@<version>@<projectslug>` so the next
59
+ * shim invocation can skip the node spawn entirely when no source dir is
60
+ * newer than the sentinel (shim schema v17+).
55
61
  */
56
62
  export function runLaunchSync(opts) {
57
63
  const result = {
@@ -74,8 +80,39 @@ export function runLaunchSync(opts) {
74
80
  result.workspaceSkipped = mirror.skipped;
75
81
  // Step 3: scoped plugin marketplaces
76
82
  result.marketplaces = synthesizeScopedMarketplaces(opts.agent, opts.version, opts.cwd);
83
+ // Touch the shim's skip-fast sentinel. Best-effort — if this fails the
84
+ // shim just won't skip on the next launch, which is correct fallback.
85
+ touchLaunchSentinel(opts.agent, opts.version, opts.cwd);
77
86
  return result;
78
87
  }
88
+ /**
89
+ * Path of the shim's skip-fast sentinel for this (agent, version, cwd) tuple.
90
+ * Must match the SHIM-SIDE format in src/lib/shims.ts (PROJECT_SLUG derivation):
91
+ * slug = PWD with `/` → `_` and ` ` → `_`
92
+ *
93
+ * Cache leak note: this dir accumulates one zero-byte file per
94
+ * (agent, version, project) tuple ever launched. Disk impact is negligible
95
+ * (inodes only). A periodic GC belongs in `agents prune` — follow-up.
96
+ */
97
+ function launchSentinelPath(agent, version, cwd) {
98
+ const slug = cwd.replace(/\//g, '_').replace(/ /g, '_');
99
+ // Prefer $HOME (respects test overrides + matches bash's $HOME expansion in
100
+ // the shim), fall back to os.homedir() so the lookup never resolves to '/'
101
+ // if HOME is somehow unset.
102
+ const home = process.env.HOME || os.homedir();
103
+ return path.join(home, '.agents', '.cache', 'launch-sync', `${agent}@${version}@${slug}`);
104
+ }
105
+ function touchLaunchSentinel(agent, version, cwd) {
106
+ try {
107
+ const sentinel = launchSentinelPath(agent, version, cwd);
108
+ fs.mkdirSync(path.dirname(sentinel), { recursive: true });
109
+ // Empty content — purely an mtime carrier for the shim's `[ -nt ]` compare.
110
+ fs.writeFileSync(sentinel, '');
111
+ }
112
+ catch {
113
+ // best-effort
114
+ }
115
+ }
79
116
  const CLAUDE_MIRROR_PLANS = [
80
117
  { srcSubdir: 'subagents', destSubdir: 'agents', entriesAreDirs: false },
81
118
  { srcSubdir: 'commands', destSubdir: 'commands', entriesAreDirs: false },
@@ -155,14 +155,17 @@ export async function runPtyServer() {
155
155
  let nodePty;
156
156
  let XtermTerminal;
157
157
  try {
158
- nodePty = await import('node-pty');
158
+ // The Homebridge multiarch fork of node-pty: API-identical (same 1.x N-API
159
+ // codebase) but ships prebuilt binaries for Linux glibc + musl, x64 + arm64
160
+ // (plus macOS/Windows), so no compiler is needed on Linux/Alpine/arm64.
161
+ nodePty = await import('@homebridge/node-pty-prebuilt-multiarch');
159
162
  // Handle ESM default export
160
163
  if (nodePty.default?.spawn)
161
164
  nodePty = nodePty.default;
162
165
  // Ensure spawn-helper is executable (bun install doesn't set +x on prebuilds)
163
166
  try {
164
167
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
165
- const ptyBase = path.resolve(__dirname, '..', '..', 'node_modules', 'node-pty');
168
+ const ptyBase = path.resolve(__dirname, '..', '..', 'node_modules', '@homebridge', 'node-pty-prebuilt-multiarch');
166
169
  const helpers = [
167
170
  path.join(ptyBase, 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper'),
168
171
  path.join(ptyBase, 'build', 'Release', 'spawn-helper'),
@@ -176,8 +179,8 @@ export async function runPtyServer() {
176
179
  catch { }
177
180
  }
178
181
  catch (err) {
179
- console.error('node-pty is required for PTY support.');
180
- console.error('Install: cd ' + '~/agents-cli && bun add node-pty');
182
+ console.error('node-pty (@homebridge/node-pty-prebuilt-multiarch) is required for PTY support.');
183
+ console.error('Install: bun add @homebridge/node-pty-prebuilt-multiarch');
181
184
  process.exit(1);
182
185
  }
183
186
  try {
@@ -9,9 +9,9 @@
9
9
  * - All unique subrules across layers are unioned
10
10
  */
11
11
  import * as fs from 'fs';
12
+ import { agentConfigDirName } from '../agents.js';
12
13
  import * as path from 'path';
13
14
  import { getSystemRulesDir, getUserRulesDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
14
- import { agentConfigDirName } from '../agents.js';
15
15
  const SUBRULES_DIR = 'subrules';
16
16
  const SUBRULES_README = 'README.md';
17
17
  /**
@@ -5,10 +5,10 @@
5
5
  * Format is the same for all agents. Resolution order: project > user > system.
6
6
  */
7
7
  import * as fs from 'fs';
8
+ import { agentConfigDirName } from '../agents.js';
8
9
  import * as path from 'path';
9
10
  import * as yaml from 'yaml';
10
11
  import { getSystemSkillsDir, getUserSkillsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../state.js';
11
- import { agentConfigDirName } from '../agents.js';
12
12
  /** Default provider uses the real state module. */
13
13
  const defaultProvider = {
14
14
  getSystemSkillsDir,
@@ -36,6 +36,8 @@ export interface ResourceEntry {
36
36
  name: string;
37
37
  path: string;
38
38
  scope: 'user' | 'project';
39
+ /** One-line description pulled from frontmatter; not all resource kinds have one. */
40
+ description?: string;
39
41
  }
40
42
  /** A skill resource entry with optional rule count. */
41
43
  export interface SkillResourceEntry extends ResourceEntry {
@@ -106,7 +106,7 @@ export function getAgentResources(agentId, options = {}) {
106
106
  const commands = [];
107
107
  for (const cmd of listInstalledCommandsWithScope(agentId, cwd, { home })) {
108
108
  if (shouldInclude(cmd.scope)) {
109
- commands.push({ name: cmd.name, path: cmd.path, scope: cmd.scope });
109
+ commands.push({ name: cmd.name, path: cmd.path, scope: cmd.scope, description: cmd.description });
110
110
  }
111
111
  }
112
112
  // Skills
@@ -119,6 +119,7 @@ export function getAgentResources(agentId, options = {}) {
119
119
  path: skill.path,
120
120
  scope: skill.scope,
121
121
  ruleCount: skill.ruleCount,
122
+ description: skill.metadata.description || undefined,
122
123
  });
123
124
  }
124
125
  }
@@ -6,10 +6,10 @@
6
6
  */
7
7
  import * as fs from 'fs';
8
8
  import * as path from 'path';
9
- import * as yaml from 'yaml';
10
9
  import { getAccountInfo } from './agents.js';
11
- import { readMeta, writeMeta, getHelpersDir, getUserAgentsDir } from './state.js';
10
+ import { readMeta, writeMeta, getHelpersDir } from './state.js';
12
11
  import { listInstalledVersions, getVersionHomePath, resolveVersion } from './versions.js';
12
+ import { getProjectRunConfigs } from './run-config.js';
13
13
  import { getUsageInfoByIdentity, getUsageLookupKey, } from './usage.js';
14
14
  function getRotateDir() {
15
15
  const dir = path.join(getHelpersDir(), 'rotate');
@@ -33,22 +33,10 @@ export function normalizeRunStrategy(value) {
33
33
  }
34
34
  /** Read project-local run strategy from the nearest agents.yaml, if present. */
35
35
  export function getProjectRunStrategy(agent, startPath) {
36
- let dir = path.resolve(startPath);
37
- const userAgentsYaml = path.join(getUserAgentsDir(), 'agents.yaml');
38
- while (dir !== path.dirname(dir)) {
39
- const manifestPath = path.join(dir, 'agents.yaml');
40
- if (manifestPath !== userAgentsYaml && fs.existsSync(manifestPath)) {
41
- try {
42
- const parsed = yaml.parse(fs.readFileSync(manifestPath, 'utf-8'));
43
- const strategy = normalizeRunStrategy(parsed?.run?.[agent]?.strategy);
44
- if (strategy)
45
- return strategy;
46
- }
47
- catch {
48
- // Ignore malformed project config and keep walking, matching version resolution.
49
- }
50
- }
51
- dir = path.dirname(dir);
36
+ for (const runConfig of getProjectRunConfigs(startPath)) {
37
+ const strategy = normalizeRunStrategy(runConfig[agent]?.strategy);
38
+ if (strategy)
39
+ return strategy;
52
40
  }
53
41
  return null;
54
42
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Project-local `run:` config discovery.
3
+ *
4
+ * The user/system `agents.yaml` is read through state.ts. Project-local
5
+ * agents.yaml files are discovered from the current working directory upward.
6
+ */
7
+ import type { RunConfig } from './types.js';
8
+ /** Return project-local run configs from nearest directory upward. */
9
+ export declare function getProjectRunConfigs(startPath?: string): RunConfig[];
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Project-local `run:` config discovery.
3
+ *
4
+ * The user/system `agents.yaml` is read through state.ts. Project-local
5
+ * agents.yaml files are discovered from the current working directory upward.
6
+ */
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as yaml from 'yaml';
10
+ import { getUserAgentsDir } from './state.js';
11
+ function isRecord(value) {
12
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
13
+ }
14
+ /** Return project-local run configs from nearest directory upward. */
15
+ export function getProjectRunConfigs(startPath = process.cwd()) {
16
+ const configs = [];
17
+ let dir = path.resolve(startPath);
18
+ const userAgentsYaml = path.join(getUserAgentsDir(), 'agents.yaml');
19
+ while (dir !== path.dirname(dir)) {
20
+ const manifestPath = path.join(dir, 'agents.yaml');
21
+ if (manifestPath !== userAgentsYaml && fs.existsSync(manifestPath)) {
22
+ try {
23
+ const parsed = yaml.parse(fs.readFileSync(manifestPath, 'utf-8'));
24
+ if (isRecord(parsed?.run)) {
25
+ configs.push(parsed.run);
26
+ }
27
+ }
28
+ catch {
29
+ // Ignore malformed project config and keep walking.
30
+ }
31
+ }
32
+ dir = path.dirname(dir);
33
+ }
34
+ return configs;
35
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Selector-based defaults for `agents run`.
3
+ *
4
+ * Stored under agents.yaml:
5
+ *
6
+ * run:
7
+ * defaults:
8
+ * "claude:*":
9
+ * mode: auto
10
+ * model: opus
11
+ * "claude:2.1.45":
12
+ * mode: plan
13
+ */
14
+ import type { AgentId, Mode, RunConfig, RunDefaults } from './types.js';
15
+ export interface ParsedRunDefaultSelector {
16
+ agent: AgentId;
17
+ version: string;
18
+ selector: string;
19
+ }
20
+ export interface ResolvedRunDefaults extends RunDefaults {
21
+ sources: {
22
+ mode?: string;
23
+ model?: string;
24
+ };
25
+ }
26
+ export interface RunDefaultEntry {
27
+ selector: string;
28
+ defaults: RunDefaults;
29
+ }
30
+ type RunDefaultsInput = {
31
+ mode?: unknown;
32
+ model?: unknown;
33
+ };
34
+ export declare function normalizeRunDefaultMode(input: string): Mode;
35
+ export declare function parseRunDefaultSelector(input: string): ParsedRunDefaultSelector;
36
+ export declare function resolveRunDefaultsFromConfig(runConfig: RunConfig | undefined, agent: AgentId, version?: string | null): ResolvedRunDefaults;
37
+ export declare function resolveRunDefaultsFromConfigs(runConfigs: Array<RunConfig | undefined>, agent: AgentId, version?: string | null): ResolvedRunDefaults;
38
+ export declare function resolveRunDefaults(agent: AgentId, version?: string | null, startPath?: string): ResolvedRunDefaults;
39
+ export declare function listRunDefaults(): RunDefaultEntry[];
40
+ export declare function setRunDefault(selectorInput: string, defaultsInput: RunDefaultsInput): RunDefaultEntry;
41
+ export declare function unsetRunDefault(selectorInput: string): boolean;
42
+ export {};