@kolisachint/hoocode-agent 0.4.132 → 0.4.134

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 (50) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/core/extensions/plugins/authoring.d.ts +67 -0
  3. package/dist/core/extensions/plugins/authoring.d.ts.map +1 -1
  4. package/dist/core/extensions/plugins/authoring.js +0 -0
  5. package/dist/core/extensions/plugins/authoring.js.map +1 -1
  6. package/dist/core/model-categories.d.ts +51 -12
  7. package/dist/core/model-categories.d.ts.map +1 -1
  8. package/dist/core/model-categories.js +89 -14
  9. package/dist/core/model-categories.js.map +1 -1
  10. package/dist/core/settings-types.d.ts +14 -2
  11. package/dist/core/settings-types.d.ts.map +1 -1
  12. package/dist/core/settings-types.js.map +1 -1
  13. package/dist/core/subagent-pool-instance.d.ts +10 -2
  14. package/dist/core/subagent-pool-instance.d.ts.map +1 -1
  15. package/dist/core/subagent-pool-instance.js +10 -2
  16. package/dist/core/subagent-pool-instance.js.map +1 -1
  17. package/dist/core/subagent-pool.d.ts +9 -0
  18. package/dist/core/subagent-pool.d.ts.map +1 -1
  19. package/dist/core/subagent-pool.js +6 -3
  20. package/dist/core/subagent-pool.js.map +1 -1
  21. package/dist/core/tools/plugin-tool-names.d.ts +2 -1
  22. package/dist/core/tools/plugin-tool-names.d.ts.map +1 -1
  23. package/dist/core/tools/plugin-tool-names.js +11 -3
  24. package/dist/core/tools/plugin-tool-names.js.map +1 -1
  25. package/dist/core/tools/plugins.d.ts.map +1 -1
  26. package/dist/core/tools/plugins.js +10 -6
  27. package/dist/core/tools/plugins.js.map +1 -1
  28. package/dist/core/tools/propose-plugin.d.ts +26 -16
  29. package/dist/core/tools/propose-plugin.d.ts.map +1 -1
  30. package/dist/core/tools/propose-plugin.js +276 -143
  31. package/dist/core/tools/propose-plugin.js.map +1 -1
  32. package/dist/core/tools/subagent.d.ts.map +1 -1
  33. package/dist/core/tools/subagent.js +10 -4
  34. package/dist/core/tools/subagent.js.map +1 -1
  35. package/dist/core/warm-subagent-pool-instance.d.ts +9 -2
  36. package/dist/core/warm-subagent-pool-instance.d.ts.map +1 -1
  37. package/dist/core/warm-subagent-pool-instance.js +9 -3
  38. package/dist/core/warm-subagent-pool-instance.js.map +1 -1
  39. package/dist/core/warm-subagent-pool.d.ts +13 -2
  40. package/dist/core/warm-subagent-pool.d.ts.map +1 -1
  41. package/dist/core/warm-subagent-pool.js +16 -7
  42. package/dist/core/warm-subagent-pool.js.map +1 -1
  43. package/dist/modes/interactive/command-executor.d.ts.map +1 -1
  44. package/dist/modes/interactive/command-executor.js +1 -1
  45. package/dist/modes/interactive/command-executor.js.map +1 -1
  46. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  47. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  48. package/examples/extensions/sandbox/package.json +1 -1
  49. package/examples/extensions/with-deps/package.json +1 -1
  50. package/package.json +4 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.134] - 2026-07-16
4
+
5
+ ## [0.4.133] - 2026-07-14
6
+
7
+ ### Changed
8
+
9
+ - Plugin authoring is now a single risk-gated `ProposePlugin` tool, replacing
10
+ the `ProposePlugin` / `ProposeExecutablePlugin` split. The human-confirmation
11
+ gate is computed from the draft's content — hooks, MCP servers, or a
12
+ mutating-subagent allowlist trigger it; passive skills/commands/read-only
13
+ subagents author autonomously — instead of being pre-declared by tool choice,
14
+ so a mixed passive+executable plugin authors in one call and executable
15
+ content can never ride in through a "passive" path. Authored plugins now
16
+ carry a `.authored.json` provenance marker at their root.
17
+
18
+ ### Added
19
+
20
+ - `UpdatePlugin` tool: merge inline-authored capabilities into an existing
21
+ locally authored plugin — skills/commands/subagents are added or replaced by
22
+ name, hooks and MCP servers are unioned with what's on disk. Additive-only
23
+ and no remote fetch; executable additions require the same human confirmation
24
+ as authoring, and marketplace-installed plugins are refused (they don't carry
25
+ the authored provenance marker and don't round-trip losslessly through the
26
+ authoring emitters).
27
+ - `RemovePluginCapability` tool: remove named capabilities from a locally
28
+ authored plugin — skills/commands/subagents/MCP servers by name, hooks by
29
+ event (narrowed by matcher/command). The subtractive half of `UpdatePlugin`;
30
+ runs autonomously since removal is the low-risk direction (deleting
31
+ capabilities cannot execute code). Also the supported way to *change* a hook
32
+ (hooks have no name to replace by): remove the old one, then add the new one
33
+ via `UpdatePlugin`.
34
+ - `ListPlugins` accepts an optional `id` parameter to look up a single
35
+ installed plugin.
36
+
3
37
  ## [0.4.132] - 2026-07-14
4
38
 
5
39
  ## [0.4.131] - 2026-07-14
@@ -39,6 +39,8 @@ export interface WriteResult {
39
39
  /** Re-parsed plugin (confirms the scaffold round-trips). */
40
40
  plugin: NormalizedPlugin | null;
41
41
  }
42
+ /** Whether the plugin at `id` was authored here (carries the provenance marker), vs. installed from a marketplace. */
43
+ export declare function isAuthoredPlugin(cwd: string, id: string): boolean;
42
44
  /**
43
45
  * Render `draft` into the requested platform layouts and write it under
44
46
  * `.agents/plugins/<id>/`. Returns the destination, the emitted files, and the
@@ -47,4 +49,69 @@ export interface WriteResult {
47
49
  export declare function writePluginDraft(cwd: string, draft: PluginDraft, platforms?: MarketplacePlatform[]): WriteResult;
48
50
  /** Whether a plugin id already exists on disk (so authoring never silently clobbers). */
49
51
  export declare function pluginExists(cwd: string, id: string): boolean;
52
+ /** Load an installed/authored plugin by id, or null if it isn't on disk / doesn't parse. */
53
+ export declare function getPlugin(cwd: string, id: string): NormalizedPlugin | null;
54
+ /**
55
+ * Merge inline-authored `delta` capabilities into the existing local plugin `id`
56
+ * and re-emit. Unlike a marketplace `UpdatePlugin`, nothing is fetched from a
57
+ * remote source — the new content comes from the caller — so the supply-chain
58
+ * "benign v1 → hostile v2" vector the spec guards against is structurally absent.
59
+ *
60
+ * Merge semantics:
61
+ * - **Skills / commands / agents** are directory-scanned, so existing ones are
62
+ * left on disk untouched; a delta entry with a matching name overwrites just
63
+ * that file (an update), a new name is added.
64
+ * - **Hooks** and **MCP servers** live in single files that a re-emit rewrites,
65
+ * so they are re-emitted as the *union* of existing + delta (MCP keyed by
66
+ * server name with delta winning; hooks deduped by event/matcher/command).
67
+ * Hooks have no name, so there is deliberately no modify-in-place: a delta
68
+ * hook with the same event/matcher but a different command is a NEW hook
69
+ * added alongside the old one, never a replacement. (Keying replacement by
70
+ * event+matcher would silently drop legitimate sibling hooks that share
71
+ * them.) Changing a hook = {@link removeFromPlugin} the old one + merge the
72
+ * new one.
73
+ * - **Metadata** (version, description, author) takes the delta's value when
74
+ * provided, else keeps the existing one.
75
+ *
76
+ * Platforms default to the plugin's existing `supportPlatform` so a merge never
77
+ * silently adds or drops a vendor layout.
78
+ */
79
+ export declare function mergePluginDraft(cwd: string, id: string, delta: Partial<PluginDraft>, platforms?: MarketplacePlatform[]): WriteResult;
80
+ /** A hook to remove: `event` is required; `matcher`/`command` narrow the match when provided. */
81
+ export interface HookRemovalSpec {
82
+ event: string;
83
+ matcher?: string;
84
+ command?: string;
85
+ }
86
+ /** Named capabilities to remove from an authored plugin. */
87
+ export interface RemovalSpec {
88
+ skills?: string[];
89
+ commands?: string[];
90
+ subagents?: string[];
91
+ mcpServers?: string[];
92
+ hooks?: HookRemovalSpec[];
93
+ }
94
+ export interface RemoveResult {
95
+ dest: string;
96
+ /** Human-readable descriptions of what was removed. */
97
+ removed: string[];
98
+ /** Requested capabilities that were not found (nothing was removed for these). */
99
+ missing: string[];
100
+ }
101
+ /**
102
+ * Remove named capabilities from the authored plugin `id`. The inverse of the
103
+ * additive merge, and — like {@link mergePluginDraft} — authored-only.
104
+ *
105
+ * Removal is the low-risk direction (deleting capabilities cannot execute
106
+ * code), which is why callers may run it without a confirmation gate.
107
+ *
108
+ * - **Skills / commands / subagents** are directory-scanned, so removal is a
109
+ * surgical file delete at our emit conventions; no re-emit needed.
110
+ * - **Hooks** (matched by event, narrowed by matcher/command when given) and
111
+ * **MCP servers** (by name) live in single files, so the remaining set is
112
+ * re-emitted — and when a set empties, its file is DELETED, because the
113
+ * parser falls back to `hooks/hooks.json` / `.mcp.json` on disk and a stale
114
+ * file would resurrect the removed capability on the next parse.
115
+ */
116
+ export declare function removeFromPlugin(cwd: string, id: string, spec: RemovalSpec): RemoveResult;
50
117
  //# sourceMappingURL=authoring.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"authoring.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/authoring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAQH,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE3E,OAAO,EAAE,KAAK,gBAAgB,EAAkB,MAAM,eAAe,CAAC;AAItE,OAAO,EAAE,2BAA2B,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAKvG,MAAM,WAAW,uBAAuB;IACvC,mGAAmG;IACnG,IAAI,EAAE,WAAW,GAAG,UAAU,CAAC;IAC/B,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,4GAA0G;IAC1G,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,gCAAgC;IAChC,MAAM,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,uBAAuB,CAuCpF;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,4DAA4D;IAC5D,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,mBAAmB,EAAE,GAAG,WAAW,CAgBhH;AAED,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAE7D","sourcesContent":["/**\n * Plugin authoring engine (spec §3) — shared by the two ProposePlugin tools.\n *\n * Completes the discover → acquire → author spectrum: when no marketplace plugin\n * fits a gap, the model can scaffold one. Authoring is gated on the *content /\n * capability-grant* trust axis (what the plugin can do), not the *source* axis\n * used for install. This module carries the risk classification, the\n * privilege-amplification guardrail, and the file writer; the tools own the two\n * escalating-risk *paths* (autonomous scaffold vs. confirm-then-activate).\n *\n * Everything is written through the format registry's {@link emitForPlatforms},\n * so an authored plugin lands in the requested vendor layouts (Claude Code and\n * GitHub Copilot by default) and round-trips back through {@link parsePluginDir}.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { CLAUDE_TOOL_ALIASES } from \"../../agent-frontmatter.js\";\nimport { PLUGIN_SYSTEM_TOOL_NAMES } from \"../../tools/plugin-tool-names.js\";\nimport { emitForPlatforms } from \"./formats/index.js\";\nimport { resolveAuthoringPlatforms } from \"./formats/platform-targets.js\";\nimport type { MarketplacePlatform, PluginDraft } from \"./formats/types.js\";\nimport { installedPluginsDir, sanitizeForDir } from \"./install.js\";\nimport { type NormalizedPlugin, parsePluginDir } from \"./manifest.js\";\n\n// Re-exported so existing importers keep one vocabulary; the resolution chain\n// (explicit → session --support-platform → default) lives in platform-targets.\nexport { DEFAULT_AUTHORING_PLATFORMS, resolveAuthoringPlatforms } from \"./formats/platform-targets.js\";\n\n/** hoocode tool names that only read (no mutation, no exec). Grants limited to these are low-risk. */\nconst READONLY_TOOLS = new Set([\"read\", \"grep\", \"find\", \"ls\", \"webfetch\", \"websearch\"]);\n\nexport interface AllowlistClassification {\n\t/** read-only grants are as safe as a skill; mutating/exec/network/`*` grants need confirmation. */\n\trisk: \"read-only\" | \"mutating\";\n\t/** Human-readable explanation of what drove the classification. */\n\treason: string;\n\t/** Any plugin-system (capability-acquisition) tools found — always forbidden in an authored allowlist. */\n\tpluginTools: string[];\n\t/** The raw allowlist tokens. */\n\ttokens: string[];\n}\n\n/**\n * Classify an authored subagent `tools:` allowlist as read-only vs. mutating,\n * reusing the same Claude-alias vocabulary as the agent-frontmatter normalizer\n * (spec §3 \"compute the risk, don't guess it\"). Anything unrecognized — an MCP\n * tool, a bare `*`, an unknown name — is treated as mutating (fail-safe).\n */\nexport function classifyAllowlist(tools: string | undefined): AllowlistClassification {\n\tconst tokens = (tools ?? \"\")\n\t\t.split(/[,\\s]+/)\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean);\n\tconst pluginTools = tokens.filter((t) => PLUGIN_SYSTEM_TOOL_NAMES.some((n) => n.toLowerCase() === t.toLowerCase()));\n\n\tif (tokens.length === 0) {\n\t\treturn { risk: \"read-only\", reason: \"no tools granted\", pluginTools, tokens };\n\t}\n\n\tconst reasons: string[] = [];\n\tlet mutating = false;\n\tfor (const t of tokens) {\n\t\tconst low = t.toLowerCase();\n\t\tif (t === \"*\" || low === \"all\") {\n\t\t\tmutating = true;\n\t\t\treasons.push(\"grants all tools (*)\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (pluginTools.some((p) => p.toLowerCase() === low)) continue; // reported separately as a guardrail violation\n\t\tconst mapped = CLAUDE_TOOL_ALIASES[low];\n\t\tif (!mapped) {\n\t\t\tmutating = true;\n\t\t\treasons.push(`grants \"${t}\" (unrecognized or MCP tool — treated as mutating)`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!READONLY_TOOLS.has(mapped)) {\n\t\t\tmutating = true;\n\t\t\treasons.push(`grants \"${mapped}\" (mutating/exec)`);\n\t\t}\n\t}\n\n\treturn {\n\t\trisk: mutating ? \"mutating\" : \"read-only\",\n\t\treason: reasons.join(\"; \") || \"read-only tools only\",\n\t\tpluginTools,\n\t\ttokens,\n\t};\n}\n\nexport interface WriteResult {\n\tdest: string;\n\t/** Written file paths, relative to the plugin root. */\n\tfiles: string[];\n\t/** Re-parsed plugin (confirms the scaffold round-trips). */\n\tplugin: NormalizedPlugin | null;\n}\n\n/**\n * Render `draft` into the requested platform layouts and write it under\n * `.agents/plugins/<id>/`. Returns the destination, the emitted files, and the\n * re-parsed plugin so callers can confirm the round-trip.\n */\nexport function writePluginDraft(cwd: string, draft: PluginDraft, platforms?: MarketplacePlatform[]): WriteResult {\n\tconst targets = resolveAuthoringPlatforms(platforms ?? draft.supportPlatform);\n\tconst dest = path.join(installedPluginsDir(cwd), sanitizeForDir(draft.id));\n\tconst files = emitForPlatforms({ ...draft, supportPlatform: targets }, targets);\n\n\t// Formats share the capability tree (only marker manifests differ), so\n\t// dedupe by path — later formats overwrite with identical content.\n\tconst byPath = new Map(files.map((f) => [f.path, f]));\n\tmkdirSync(dest, { recursive: true });\n\tfor (const f of byPath.values()) {\n\t\tconst abs = path.join(dest, f.path);\n\t\tmkdirSync(path.dirname(abs), { recursive: true });\n\t\twriteFileSync(abs, f.content);\n\t}\n\n\treturn { dest, files: [...byPath.keys()], plugin: parsePluginDir(dest) };\n}\n\n/** Whether a plugin id already exists on disk (so authoring never silently clobbers). */\nexport function pluginExists(cwd: string, id: string): boolean {\n\treturn existsSync(path.join(installedPluginsDir(cwd), sanitizeForDir(id)));\n}\n"]}
1
+ {"version":3,"file":"authoring.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/authoring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AASH,OAAO,KAAK,EAAmC,mBAAmB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE5G,OAAO,EAAE,KAAK,gBAAgB,EAA0C,MAAM,eAAe,CAAC;AAI9F,OAAO,EAAE,2BAA2B,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAKvG,MAAM,WAAW,uBAAuB;IACvC,mGAAmG;IACnG,IAAI,EAAE,WAAW,GAAG,UAAU,CAAC;IAC/B,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,4GAA0G;IAC1G,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,gCAAgC;IAChC,MAAM,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,uBAAuB,CAuCpF;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,4DAA4D;IAC5D,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAChC;AAWD,sHAAsH;AACtH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAEjE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,mBAAmB,EAAE,GAAG,WAAW,CAiBhH;AAED,yFAAyF;AACzF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAE7D;AAED,4FAA4F;AAC5F,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAG1E;AA6DD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,gBAAgB,CAC/B,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,OAAO,CAAC,WAAW,CAAC,EAC3B,SAAS,CAAC,EAAE,mBAAmB,EAAE,GAC/B,WAAW,CAuCb;AAED,iGAAiG;AACjG,MAAM,WAAW,eAAe;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,4DAA4D;AAC5D,MAAM,WAAW,WAAW;IAC3B,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,eAAe,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,kFAAkF;IAClF,OAAO,EAAE,MAAM,EAAE,CAAC;CAClB;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,YAAY,CA8FzF","sourcesContent":["/**\n * Plugin authoring engine (spec §3) — shared by the two ProposePlugin tools.\n *\n * Completes the discover → acquire → author spectrum: when no marketplace plugin\n * fits a gap, the model can scaffold one. Authoring is gated on the *content /\n * capability-grant* trust axis (what the plugin can do), not the *source* axis\n * used for install. This module carries the risk classification, the\n * privilege-amplification guardrail, and the file writer; the tools own the two\n * escalating-risk *paths* (autonomous scaffold vs. confirm-then-activate).\n *\n * Everything is written through the format registry's {@link emitForPlatforms},\n * so an authored plugin lands in the requested vendor layouts (Claude Code and\n * GitHub Copilot by default) and round-trips back through {@link parsePluginDir}.\n */\n\nimport { existsSync, mkdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { CLAUDE_TOOL_ALIASES } from \"../../agent-frontmatter.js\";\nimport { PLUGIN_SYSTEM_TOOL_NAMES } from \"../../tools/plugin-tool-names.js\";\nimport { emitForPlatforms } from \"./formats/index.js\";\nimport { resolveAuthoringPlatforms } from \"./formats/platform-targets.js\";\nimport { slug } from \"./formats/shared.js\";\nimport type { AuthoredHook, AuthoredMcpServer, MarketplacePlatform, PluginDraft } from \"./formats/types.js\";\nimport { installedPluginsDir, sanitizeForDir } from \"./install.js\";\nimport { type NormalizedPlugin, type PluginHooksConfig, parsePluginDir } from \"./manifest.js\";\n\n// Re-exported so existing importers keep one vocabulary; the resolution chain\n// (explicit → session --support-platform → default) lives in platform-targets.\nexport { DEFAULT_AUTHORING_PLATFORMS, resolveAuthoringPlatforms } from \"./formats/platform-targets.js\";\n\n/** hoocode tool names that only read (no mutation, no exec). Grants limited to these are low-risk. */\nconst READONLY_TOOLS = new Set([\"read\", \"grep\", \"find\", \"ls\", \"webfetch\", \"websearch\"]);\n\nexport interface AllowlistClassification {\n\t/** read-only grants are as safe as a skill; mutating/exec/network/`*` grants need confirmation. */\n\trisk: \"read-only\" | \"mutating\";\n\t/** Human-readable explanation of what drove the classification. */\n\treason: string;\n\t/** Any plugin-system (capability-acquisition) tools found — always forbidden in an authored allowlist. */\n\tpluginTools: string[];\n\t/** The raw allowlist tokens. */\n\ttokens: string[];\n}\n\n/**\n * Classify an authored subagent `tools:` allowlist as read-only vs. mutating,\n * reusing the same Claude-alias vocabulary as the agent-frontmatter normalizer\n * (spec §3 \"compute the risk, don't guess it\"). Anything unrecognized — an MCP\n * tool, a bare `*`, an unknown name — is treated as mutating (fail-safe).\n */\nexport function classifyAllowlist(tools: string | undefined): AllowlistClassification {\n\tconst tokens = (tools ?? \"\")\n\t\t.split(/[,\\s]+/)\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean);\n\tconst pluginTools = tokens.filter((t) => PLUGIN_SYSTEM_TOOL_NAMES.some((n) => n.toLowerCase() === t.toLowerCase()));\n\n\tif (tokens.length === 0) {\n\t\treturn { risk: \"read-only\", reason: \"no tools granted\", pluginTools, tokens };\n\t}\n\n\tconst reasons: string[] = [];\n\tlet mutating = false;\n\tfor (const t of tokens) {\n\t\tconst low = t.toLowerCase();\n\t\tif (t === \"*\" || low === \"all\") {\n\t\t\tmutating = true;\n\t\t\treasons.push(\"grants all tools (*)\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (pluginTools.some((p) => p.toLowerCase() === low)) continue; // reported separately as a guardrail violation\n\t\tconst mapped = CLAUDE_TOOL_ALIASES[low];\n\t\tif (!mapped) {\n\t\t\tmutating = true;\n\t\t\treasons.push(`grants \"${t}\" (unrecognized or MCP tool — treated as mutating)`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!READONLY_TOOLS.has(mapped)) {\n\t\t\tmutating = true;\n\t\t\treasons.push(`grants \"${mapped}\" (mutating/exec)`);\n\t\t}\n\t}\n\n\treturn {\n\t\trisk: mutating ? \"mutating\" : \"read-only\",\n\t\treason: reasons.join(\"; \") || \"read-only tools only\",\n\t\tpluginTools,\n\t\ttokens,\n\t};\n}\n\nexport interface WriteResult {\n\tdest: string;\n\t/** Written file paths, relative to the plugin root. */\n\tfiles: string[];\n\t/** Re-parsed plugin (confirms the scaffold round-trips). */\n\tplugin: NormalizedPlugin | null;\n}\n\n/**\n * Provenance marker written at the root of every authored plugin. Authored and\n * marketplace-installed plugins land in the same `.agents/plugins/` directory,\n * and only authored ones round-trip losslessly through our emitters — so\n * UpdatePlugin (which re-emits manifests and hook/MCP files) is gated on this\n * marker's presence. Existence is the signal; the content is informational.\n */\nconst AUTHORED_MARKER_FILE = \".authored.json\";\n\n/** Whether the plugin at `id` was authored here (carries the provenance marker), vs. installed from a marketplace. */\nexport function isAuthoredPlugin(cwd: string, id: string): boolean {\n\treturn existsSync(path.join(installedPluginsDir(cwd), sanitizeForDir(id), AUTHORED_MARKER_FILE));\n}\n\n/**\n * Render `draft` into the requested platform layouts and write it under\n * `.agents/plugins/<id>/`. Returns the destination, the emitted files, and the\n * re-parsed plugin so callers can confirm the round-trip.\n */\nexport function writePluginDraft(cwd: string, draft: PluginDraft, platforms?: MarketplacePlatform[]): WriteResult {\n\tconst targets = resolveAuthoringPlatforms(platforms ?? draft.supportPlatform);\n\tconst dest = path.join(installedPluginsDir(cwd), sanitizeForDir(draft.id));\n\tconst files = emitForPlatforms({ ...draft, supportPlatform: targets }, targets);\n\n\t// Formats share the capability tree (only marker manifests differ), so\n\t// dedupe by path — later formats overwrite with identical content.\n\tconst byPath = new Map(files.map((f) => [f.path, f]));\n\tmkdirSync(dest, { recursive: true });\n\tfor (const f of byPath.values()) {\n\t\tconst abs = path.join(dest, f.path);\n\t\tmkdirSync(path.dirname(abs), { recursive: true });\n\t\twriteFileSync(abs, f.content);\n\t}\n\twriteFileSync(path.join(dest, AUTHORED_MARKER_FILE), `${JSON.stringify({ authored: true }, null, 2)}\\n`);\n\n\treturn { dest, files: [...byPath.keys(), AUTHORED_MARKER_FILE], plugin: parsePluginDir(dest) };\n}\n\n/** Whether a plugin id already exists on disk (so authoring never silently clobbers). */\nexport function pluginExists(cwd: string, id: string): boolean {\n\treturn existsSync(path.join(installedPluginsDir(cwd), sanitizeForDir(id)));\n}\n\n/** Load an installed/authored plugin by id, or null if it isn't on disk / doesn't parse. */\nexport function getPlugin(cwd: string, id: string): NormalizedPlugin | null {\n\tconst dir = path.join(installedPluginsDir(cwd), sanitizeForDir(id));\n\treturn existsSync(dir) ? parsePluginDir(dir) : null;\n}\n\n/** Reverse of {@link authoredHooksToConfig}: flatten a parsed hook event-map back to authored hooks. */\nfunction hooksConfigToAuthored(config: PluginHooksConfig): AuthoredHook[] {\n\tconst out: AuthoredHook[] = [];\n\tfor (const [event, groups] of Object.entries(config)) {\n\t\tfor (const group of groups) {\n\t\t\tfor (const cmd of group.hooks) {\n\t\t\t\tif (typeof cmd.command !== \"string\" || !cmd.command) continue;\n\t\t\t\tout.push({\n\t\t\t\t\tevent,\n\t\t\t\t\t...(group.matcher ? { matcher: group.matcher } : {}),\n\t\t\t\t\tcommand: cmd.command,\n\t\t\t\t\t...(cmd.timeout ? { timeout: cmd.timeout } : {}),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\treturn out;\n}\n\n/**\n * Convert a parsed `mcpServers` record back to authored form. Throws on a\n * non-stdio (url/http-type) server rather than silently dropping it from the\n * re-emit — a merge must never quietly lose a capability (only reachable via a\n * hand-edited authored plugin; our own schema always writes `command` servers).\n */\nfunction mcpRecordToAuthored(record: Record<string, unknown>): AuthoredMcpServer[] {\n\tconst out: AuthoredMcpServer[] = [];\n\tfor (const [name, value] of Object.entries(record)) {\n\t\tif (!value || typeof value !== \"object\") continue;\n\t\tconst server = value as { command?: unknown; args?: unknown; env?: unknown };\n\t\tif (typeof server.command !== \"string\") {\n\t\t\tthrow new Error(\n\t\t\t\t`Cannot merge: MCP server \"${name}\" has no command (url/http-type servers don't round-trip through authoring). ` +\n\t\t\t\t\t\"Edit the plugin's .mcp.json directly instead.\",\n\t\t\t);\n\t\t}\n\t\tout.push({\n\t\t\tname,\n\t\t\tcommand: server.command,\n\t\t\t...(Array.isArray(server.args) ? { args: server.args.map(String) } : {}),\n\t\t\t...(server.env && typeof server.env === \"object\" ? { env: server.env as Record<string, string> } : {}),\n\t\t});\n\t}\n\treturn out;\n}\n\n/** Dedupe authored hooks by (event, matcher, command) so a re-supplied hook doesn't stack. */\nfunction dedupeHooks(hooks: AuthoredHook[]): AuthoredHook[] {\n\tconst seen = new Set<string>();\n\tconst out: AuthoredHook[] = [];\n\tfor (const h of hooks) {\n\t\tconst key = `${h.event}\u0000${h.matcher ?? \"\"}\u0000${h.command}`;\n\t\tif (seen.has(key)) continue;\n\t\tseen.add(key);\n\t\tout.push(h);\n\t}\n\treturn out;\n}\n\n/**\n * Merge inline-authored `delta` capabilities into the existing local plugin `id`\n * and re-emit. Unlike a marketplace `UpdatePlugin`, nothing is fetched from a\n * remote source — the new content comes from the caller — so the supply-chain\n * \"benign v1 → hostile v2\" vector the spec guards against is structurally absent.\n *\n * Merge semantics:\n * - **Skills / commands / agents** are directory-scanned, so existing ones are\n * left on disk untouched; a delta entry with a matching name overwrites just\n * that file (an update), a new name is added.\n * - **Hooks** and **MCP servers** live in single files that a re-emit rewrites,\n * so they are re-emitted as the *union* of existing + delta (MCP keyed by\n * server name with delta winning; hooks deduped by event/matcher/command).\n * Hooks have no name, so there is deliberately no modify-in-place: a delta\n * hook with the same event/matcher but a different command is a NEW hook\n * added alongside the old one, never a replacement. (Keying replacement by\n * event+matcher would silently drop legitimate sibling hooks that share\n * them.) Changing a hook = {@link removeFromPlugin} the old one + merge the\n * new one.\n * - **Metadata** (version, description, author) takes the delta's value when\n * provided, else keeps the existing one.\n *\n * Platforms default to the plugin's existing `supportPlatform` so a merge never\n * silently adds or drops a vendor layout.\n */\nexport function mergePluginDraft(\n\tcwd: string,\n\tid: string,\n\tdelta: Partial<PluginDraft>,\n\tplatforms?: MarketplacePlatform[],\n): WriteResult {\n\tconst existing = getPlugin(cwd, id);\n\tif (!existing) {\n\t\tthrow new Error(`Cannot update plugin \"${id}\": it does not exist. Use ProposePlugin to create it first.`);\n\t}\n\t// Authored-only: merging re-emits manifests and hook/MCP files through our\n\t// writer, which only round-trips what PluginDraft can represent. Running that\n\t// over a marketplace install could silently drop fields it carries (capability\n\t// -dir overrides, url-type MCP servers, extra manifest keys).\n\tif (!isAuthoredPlugin(cwd, id)) {\n\t\tthrow new Error(\n\t\t\t`Cannot update plugin \"${id}\": it was not authored here (no ${AUTHORED_MARKER_FILE} marker). ` +\n\t\t\t\t\"Only locally authored plugins can be merged.\",\n\t\t);\n\t}\n\n\tconst existingHooks = existing.hooks ? hooksConfigToAuthored(existing.hooks) : [];\n\tconst mergedHooks = dedupeHooks([...existingHooks, ...(delta.hooks ?? [])]);\n\n\tconst mcpByName = new Map<string, AuthoredMcpServer>();\n\tfor (const s of existing.mcpServers ? mcpRecordToAuthored(existing.mcpServers) : []) mcpByName.set(s.name, s);\n\tfor (const s of delta.mcpServers ?? []) mcpByName.set(s.name, s);\n\n\tconst targets = resolveAuthoringPlatforms(platforms ?? existing.supportPlatform);\n\tconst merged: PluginDraft = {\n\t\tid,\n\t\tversion: delta.version ?? existing.version,\n\t\tdescription: delta.description ?? existing.description,\n\t\tauthor: delta.author ?? existing.author,\n\t\tsupportPlatform: targets,\n\t\t// Directory-scanned capabilities: delta-only; existing files stay on disk.\n\t\tskills: delta.skills,\n\t\tcommands: delta.commands,\n\t\tagents: delta.agents,\n\t\t// Single-file capabilities: re-emit the union so a merge never drops them.\n\t\thooks: mergedHooks.length ? mergedHooks : undefined,\n\t\tmcpServers: mcpByName.size ? [...mcpByName.values()] : undefined,\n\t};\n\treturn writePluginDraft(cwd, merged, targets);\n}\n\n/** A hook to remove: `event` is required; `matcher`/`command` narrow the match when provided. */\nexport interface HookRemovalSpec {\n\tevent: string;\n\tmatcher?: string;\n\tcommand?: string;\n}\n\n/** Named capabilities to remove from an authored plugin. */\nexport interface RemovalSpec {\n\tskills?: string[];\n\tcommands?: string[];\n\tsubagents?: string[];\n\tmcpServers?: string[];\n\thooks?: HookRemovalSpec[];\n}\n\nexport interface RemoveResult {\n\tdest: string;\n\t/** Human-readable descriptions of what was removed. */\n\tremoved: string[];\n\t/** Requested capabilities that were not found (nothing was removed for these). */\n\tmissing: string[];\n}\n\nfunction describeHookSpec(h: HookRemovalSpec): string {\n\treturn `hook [${h.event}${h.matcher !== undefined ? ` matcher=${h.matcher}` : \"\"}${h.command !== undefined ? ` command=${h.command}` : \"\"}]`;\n}\n\n/**\n * Remove named capabilities from the authored plugin `id`. The inverse of the\n * additive merge, and — like {@link mergePluginDraft} — authored-only.\n *\n * Removal is the low-risk direction (deleting capabilities cannot execute\n * code), which is why callers may run it without a confirmation gate.\n *\n * - **Skills / commands / subagents** are directory-scanned, so removal is a\n * surgical file delete at our emit conventions; no re-emit needed.\n * - **Hooks** (matched by event, narrowed by matcher/command when given) and\n * **MCP servers** (by name) live in single files, so the remaining set is\n * re-emitted — and when a set empties, its file is DELETED, because the\n * parser falls back to `hooks/hooks.json` / `.mcp.json` on disk and a stale\n * file would resurrect the removed capability on the next parse.\n */\nexport function removeFromPlugin(cwd: string, id: string, spec: RemovalSpec): RemoveResult {\n\tconst existing = getPlugin(cwd, id);\n\tif (!existing) {\n\t\tthrow new Error(`Cannot remove from plugin \"${id}\": it does not exist.`);\n\t}\n\tif (!isAuthoredPlugin(cwd, id)) {\n\t\tthrow new Error(\n\t\t\t`Cannot remove from plugin \"${id}\": it was not authored here (no ${AUTHORED_MARKER_FILE} marker). ` +\n\t\t\t\t\"Only locally authored plugins can be edited.\",\n\t\t);\n\t}\n\tconst dest = path.join(installedPluginsDir(cwd), sanitizeForDir(id));\n\tconst removed: string[] = [];\n\tconst missing: string[] = [];\n\n\t// Directory-scanned capabilities: surgical deletes at our emit conventions.\n\tconst fileTargets: Array<[kind: string, name: string, relPath: string]> = [\n\t\t...(spec.skills ?? []).map((n): [string, string, string] => [\"skill\", n, path.join(\"skills\", slug(n))]),\n\t\t...(spec.commands ?? []).map((n): [string, string, string] => [\n\t\t\t\"command\",\n\t\t\tn,\n\t\t\tpath.join(\"commands\", `${slug(n)}.md`),\n\t\t]),\n\t\t...(spec.subagents ?? []).map((n): [string, string, string] => [\n\t\t\t\"subagent\",\n\t\t\tn,\n\t\t\tpath.join(\"agents\", `${slug(n)}.md`),\n\t\t]),\n\t];\n\tfor (const [kind, name, rel] of fileTargets) {\n\t\tconst abs = path.join(dest, rel);\n\t\tif (existsSync(abs)) {\n\t\t\trmSync(abs, { recursive: true, force: true });\n\t\t\tremoved.push(`${kind} \"${name}\"`);\n\t\t} else {\n\t\t\tmissing.push(`${kind} \"${name}\"`);\n\t\t}\n\t}\n\n\t// Single-file capabilities: filter the reconstructed sets, then re-emit.\n\tlet singleFileChanged = false;\n\tlet remainingHooks = existing.hooks ? hooksConfigToAuthored(existing.hooks) : [];\n\tfor (const h of spec.hooks ?? []) {\n\t\tconst before = remainingHooks.length;\n\t\tremainingHooks = remainingHooks.filter(\n\t\t\t(x) =>\n\t\t\t\t!(\n\t\t\t\t\tx.event === h.event &&\n\t\t\t\t\t(h.matcher === undefined || (x.matcher ?? \"\") === h.matcher) &&\n\t\t\t\t\t(h.command === undefined || x.command === h.command)\n\t\t\t\t),\n\t\t);\n\t\tconst n = before - remainingHooks.length;\n\t\tif (n > 0) {\n\t\t\tremoved.push(`${n} ${describeHookSpec(h)}`);\n\t\t\tsingleFileChanged = true;\n\t\t} else {\n\t\t\tmissing.push(describeHookSpec(h));\n\t\t}\n\t}\n\tlet remainingMcp = existing.mcpServers ? mcpRecordToAuthored(existing.mcpServers) : [];\n\tfor (const name of spec.mcpServers ?? []) {\n\t\tconst before = remainingMcp.length;\n\t\tremainingMcp = remainingMcp.filter((s) => s.name !== name);\n\t\tif (remainingMcp.length < before) {\n\t\t\tremoved.push(`mcp server \"${name}\"`);\n\t\t\tsingleFileChanged = true;\n\t\t} else {\n\t\t\tmissing.push(`mcp server \"${name}\"`);\n\t\t}\n\t}\n\n\tif (singleFileChanged) {\n\t\tconst targets = resolveAuthoringPlatforms(existing.supportPlatform);\n\t\twritePluginDraft(\n\t\t\tcwd,\n\t\t\t{\n\t\t\t\tid,\n\t\t\t\tversion: existing.version,\n\t\t\t\tdescription: existing.description,\n\t\t\t\tauthor: existing.author,\n\t\t\t\tsupportPlatform: targets,\n\t\t\t\thooks: remainingHooks.length ? remainingHooks : undefined,\n\t\t\t\tmcpServers: remainingMcp.length ? remainingMcp : undefined,\n\t\t\t},\n\t\t\ttargets,\n\t\t);\n\t\t// Emit skips empty sets, so a stale file from the previous emit survives\n\t\t// and the parser's on-disk fallback would resurrect it — delete explicitly.\n\t\tif (remainingHooks.length === 0) rmSync(path.join(dest, \"hooks\", \"hooks.json\"), { force: true });\n\t\tif (remainingMcp.length === 0) rmSync(path.join(dest, \".mcp.json\"), { force: true });\n\t}\n\n\treturn { dest, removed, missing };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"authoring.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/authoring.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plugin authoring engine (spec §3) — shared by the two ProposePlugin tools.\n *\n * Completes the discover → acquire → author spectrum: when no marketplace plugin\n * fits a gap, the model can scaffold one. Authoring is gated on the *content /\n * capability-grant* trust axis (what the plugin can do), not the *source* axis\n * used for install. This module carries the risk classification, the\n * privilege-amplification guardrail, and the file writer; the tools own the two\n * escalating-risk *paths* (autonomous scaffold vs. confirm-then-activate).\n *\n * Everything is written through the format registry's {@link emitForPlatforms},\n * so an authored plugin lands in the requested vendor layouts (Claude Code and\n * GitHub Copilot by default) and round-trips back through {@link parsePluginDir}.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { CLAUDE_TOOL_ALIASES } from \"../../agent-frontmatter.js\";\nimport { PLUGIN_SYSTEM_TOOL_NAMES } from \"../../tools/plugin-tool-names.js\";\nimport { emitForPlatforms } from \"./formats/index.js\";\nimport { resolveAuthoringPlatforms } from \"./formats/platform-targets.js\";\nimport type { MarketplacePlatform, PluginDraft } from \"./formats/types.js\";\nimport { installedPluginsDir, sanitizeForDir } from \"./install.js\";\nimport { type NormalizedPlugin, parsePluginDir } from \"./manifest.js\";\n\n// Re-exported so existing importers keep one vocabulary; the resolution chain\n// (explicit → session --support-platform → default) lives in platform-targets.\nexport { DEFAULT_AUTHORING_PLATFORMS, resolveAuthoringPlatforms } from \"./formats/platform-targets.js\";\n\n/** hoocode tool names that only read (no mutation, no exec). Grants limited to these are low-risk. */\nconst READONLY_TOOLS = new Set([\"read\", \"grep\", \"find\", \"ls\", \"webfetch\", \"websearch\"]);\n\nexport interface AllowlistClassification {\n\t/** read-only grants are as safe as a skill; mutating/exec/network/`*` grants need confirmation. */\n\trisk: \"read-only\" | \"mutating\";\n\t/** Human-readable explanation of what drove the classification. */\n\treason: string;\n\t/** Any plugin-system (capability-acquisition) tools found — always forbidden in an authored allowlist. */\n\tpluginTools: string[];\n\t/** The raw allowlist tokens. */\n\ttokens: string[];\n}\n\n/**\n * Classify an authored subagent `tools:` allowlist as read-only vs. mutating,\n * reusing the same Claude-alias vocabulary as the agent-frontmatter normalizer\n * (spec §3 \"compute the risk, don't guess it\"). Anything unrecognized — an MCP\n * tool, a bare `*`, an unknown name — is treated as mutating (fail-safe).\n */\nexport function classifyAllowlist(tools: string | undefined): AllowlistClassification {\n\tconst tokens = (tools ?? \"\")\n\t\t.split(/[,\\s]+/)\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean);\n\tconst pluginTools = tokens.filter((t) => PLUGIN_SYSTEM_TOOL_NAMES.some((n) => n.toLowerCase() === t.toLowerCase()));\n\n\tif (tokens.length === 0) {\n\t\treturn { risk: \"read-only\", reason: \"no tools granted\", pluginTools, tokens };\n\t}\n\n\tconst reasons: string[] = [];\n\tlet mutating = false;\n\tfor (const t of tokens) {\n\t\tconst low = t.toLowerCase();\n\t\tif (t === \"*\" || low === \"all\") {\n\t\t\tmutating = true;\n\t\t\treasons.push(\"grants all tools (*)\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (pluginTools.some((p) => p.toLowerCase() === low)) continue; // reported separately as a guardrail violation\n\t\tconst mapped = CLAUDE_TOOL_ALIASES[low];\n\t\tif (!mapped) {\n\t\t\tmutating = true;\n\t\t\treasons.push(`grants \"${t}\" (unrecognized or MCP tool — treated as mutating)`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!READONLY_TOOLS.has(mapped)) {\n\t\t\tmutating = true;\n\t\t\treasons.push(`grants \"${mapped}\" (mutating/exec)`);\n\t\t}\n\t}\n\n\treturn {\n\t\trisk: mutating ? \"mutating\" : \"read-only\",\n\t\treason: reasons.join(\"; \") || \"read-only tools only\",\n\t\tpluginTools,\n\t\ttokens,\n\t};\n}\n\nexport interface WriteResult {\n\tdest: string;\n\t/** Written file paths, relative to the plugin root. */\n\tfiles: string[];\n\t/** Re-parsed plugin (confirms the scaffold round-trips). */\n\tplugin: NormalizedPlugin | null;\n}\n\n/**\n * Render `draft` into the requested platform layouts and write it under\n * `.agents/plugins/<id>/`. Returns the destination, the emitted files, and the\n * re-parsed plugin so callers can confirm the round-trip.\n */\nexport function writePluginDraft(cwd: string, draft: PluginDraft, platforms?: MarketplacePlatform[]): WriteResult {\n\tconst targets = resolveAuthoringPlatforms(platforms ?? draft.supportPlatform);\n\tconst dest = path.join(installedPluginsDir(cwd), sanitizeForDir(draft.id));\n\tconst files = emitForPlatforms({ ...draft, supportPlatform: targets }, targets);\n\n\t// Formats share the capability tree (only marker manifests differ), so\n\t// dedupe by path — later formats overwrite with identical content.\n\tconst byPath = new Map(files.map((f) => [f.path, f]));\n\tmkdirSync(dest, { recursive: true });\n\tfor (const f of byPath.values()) {\n\t\tconst abs = path.join(dest, f.path);\n\t\tmkdirSync(path.dirname(abs), { recursive: true });\n\t\twriteFileSync(abs, f.content);\n\t}\n\n\treturn { dest, files: [...byPath.keys()], plugin: parsePluginDir(dest) };\n}\n\n/** Whether a plugin id already exists on disk (so authoring never silently clobbers). */\nexport function pluginExists(cwd: string, id: string): boolean {\n\treturn existsSync(path.join(installedPluginsDir(cwd), sanitizeForDir(id)));\n}\n"]}
1
+ {"version":3,"file":"authoring.js","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/authoring.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Plugin authoring engine (spec §3) — shared by the two ProposePlugin tools.\n *\n * Completes the discover → acquire → author spectrum: when no marketplace plugin\n * fits a gap, the model can scaffold one. Authoring is gated on the *content /\n * capability-grant* trust axis (what the plugin can do), not the *source* axis\n * used for install. This module carries the risk classification, the\n * privilege-amplification guardrail, and the file writer; the tools own the two\n * escalating-risk *paths* (autonomous scaffold vs. confirm-then-activate).\n *\n * Everything is written through the format registry's {@link emitForPlatforms},\n * so an authored plugin lands in the requested vendor layouts (Claude Code and\n * GitHub Copilot by default) and round-trips back through {@link parsePluginDir}.\n */\n\nimport { existsSync, mkdirSync, rmSync, writeFileSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { CLAUDE_TOOL_ALIASES } from \"../../agent-frontmatter.js\";\nimport { PLUGIN_SYSTEM_TOOL_NAMES } from \"../../tools/plugin-tool-names.js\";\nimport { emitForPlatforms } from \"./formats/index.js\";\nimport { resolveAuthoringPlatforms } from \"./formats/platform-targets.js\";\nimport { slug } from \"./formats/shared.js\";\nimport type { AuthoredHook, AuthoredMcpServer, MarketplacePlatform, PluginDraft } from \"./formats/types.js\";\nimport { installedPluginsDir, sanitizeForDir } from \"./install.js\";\nimport { type NormalizedPlugin, type PluginHooksConfig, parsePluginDir } from \"./manifest.js\";\n\n// Re-exported so existing importers keep one vocabulary; the resolution chain\n// (explicit → session --support-platform → default) lives in platform-targets.\nexport { DEFAULT_AUTHORING_PLATFORMS, resolveAuthoringPlatforms } from \"./formats/platform-targets.js\";\n\n/** hoocode tool names that only read (no mutation, no exec). Grants limited to these are low-risk. */\nconst READONLY_TOOLS = new Set([\"read\", \"grep\", \"find\", \"ls\", \"webfetch\", \"websearch\"]);\n\nexport interface AllowlistClassification {\n\t/** read-only grants are as safe as a skill; mutating/exec/network/`*` grants need confirmation. */\n\trisk: \"read-only\" | \"mutating\";\n\t/** Human-readable explanation of what drove the classification. */\n\treason: string;\n\t/** Any plugin-system (capability-acquisition) tools found — always forbidden in an authored allowlist. */\n\tpluginTools: string[];\n\t/** The raw allowlist tokens. */\n\ttokens: string[];\n}\n\n/**\n * Classify an authored subagent `tools:` allowlist as read-only vs. mutating,\n * reusing the same Claude-alias vocabulary as the agent-frontmatter normalizer\n * (spec §3 \"compute the risk, don't guess it\"). Anything unrecognized — an MCP\n * tool, a bare `*`, an unknown name — is treated as mutating (fail-safe).\n */\nexport function classifyAllowlist(tools: string | undefined): AllowlistClassification {\n\tconst tokens = (tools ?? \"\")\n\t\t.split(/[,\\s]+/)\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean);\n\tconst pluginTools = tokens.filter((t) => PLUGIN_SYSTEM_TOOL_NAMES.some((n) => n.toLowerCase() === t.toLowerCase()));\n\n\tif (tokens.length === 0) {\n\t\treturn { risk: \"read-only\", reason: \"no tools granted\", pluginTools, tokens };\n\t}\n\n\tconst reasons: string[] = [];\n\tlet mutating = false;\n\tfor (const t of tokens) {\n\t\tconst low = t.toLowerCase();\n\t\tif (t === \"*\" || low === \"all\") {\n\t\t\tmutating = true;\n\t\t\treasons.push(\"grants all tools (*)\");\n\t\t\tcontinue;\n\t\t}\n\t\tif (pluginTools.some((p) => p.toLowerCase() === low)) continue; // reported separately as a guardrail violation\n\t\tconst mapped = CLAUDE_TOOL_ALIASES[low];\n\t\tif (!mapped) {\n\t\t\tmutating = true;\n\t\t\treasons.push(`grants \"${t}\" (unrecognized or MCP tool — treated as mutating)`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!READONLY_TOOLS.has(mapped)) {\n\t\t\tmutating = true;\n\t\t\treasons.push(`grants \"${mapped}\" (mutating/exec)`);\n\t\t}\n\t}\n\n\treturn {\n\t\trisk: mutating ? \"mutating\" : \"read-only\",\n\t\treason: reasons.join(\"; \") || \"read-only tools only\",\n\t\tpluginTools,\n\t\ttokens,\n\t};\n}\n\nexport interface WriteResult {\n\tdest: string;\n\t/** Written file paths, relative to the plugin root. */\n\tfiles: string[];\n\t/** Re-parsed plugin (confirms the scaffold round-trips). */\n\tplugin: NormalizedPlugin | null;\n}\n\n/**\n * Provenance marker written at the root of every authored plugin. Authored and\n * marketplace-installed plugins land in the same `.agents/plugins/` directory,\n * and only authored ones round-trip losslessly through our emitters — so\n * UpdatePlugin (which re-emits manifests and hook/MCP files) is gated on this\n * marker's presence. Existence is the signal; the content is informational.\n */\nconst AUTHORED_MARKER_FILE = \".authored.json\";\n\n/** Whether the plugin at `id` was authored here (carries the provenance marker), vs. installed from a marketplace. */\nexport function isAuthoredPlugin(cwd: string, id: string): boolean {\n\treturn existsSync(path.join(installedPluginsDir(cwd), sanitizeForDir(id), AUTHORED_MARKER_FILE));\n}\n\n/**\n * Render `draft` into the requested platform layouts and write it under\n * `.agents/plugins/<id>/`. Returns the destination, the emitted files, and the\n * re-parsed plugin so callers can confirm the round-trip.\n */\nexport function writePluginDraft(cwd: string, draft: PluginDraft, platforms?: MarketplacePlatform[]): WriteResult {\n\tconst targets = resolveAuthoringPlatforms(platforms ?? draft.supportPlatform);\n\tconst dest = path.join(installedPluginsDir(cwd), sanitizeForDir(draft.id));\n\tconst files = emitForPlatforms({ ...draft, supportPlatform: targets }, targets);\n\n\t// Formats share the capability tree (only marker manifests differ), so\n\t// dedupe by path — later formats overwrite with identical content.\n\tconst byPath = new Map(files.map((f) => [f.path, f]));\n\tmkdirSync(dest, { recursive: true });\n\tfor (const f of byPath.values()) {\n\t\tconst abs = path.join(dest, f.path);\n\t\tmkdirSync(path.dirname(abs), { recursive: true });\n\t\twriteFileSync(abs, f.content);\n\t}\n\twriteFileSync(path.join(dest, AUTHORED_MARKER_FILE), `${JSON.stringify({ authored: true }, null, 2)}\\n`);\n\n\treturn { dest, files: [...byPath.keys(), AUTHORED_MARKER_FILE], plugin: parsePluginDir(dest) };\n}\n\n/** Whether a plugin id already exists on disk (so authoring never silently clobbers). */\nexport function pluginExists(cwd: string, id: string): boolean {\n\treturn existsSync(path.join(installedPluginsDir(cwd), sanitizeForDir(id)));\n}\n\n/** Load an installed/authored plugin by id, or null if it isn't on disk / doesn't parse. */\nexport function getPlugin(cwd: string, id: string): NormalizedPlugin | null {\n\tconst dir = path.join(installedPluginsDir(cwd), sanitizeForDir(id));\n\treturn existsSync(dir) ? parsePluginDir(dir) : null;\n}\n\n/** Reverse of {@link authoredHooksToConfig}: flatten a parsed hook event-map back to authored hooks. */\nfunction hooksConfigToAuthored(config: PluginHooksConfig): AuthoredHook[] {\n\tconst out: AuthoredHook[] = [];\n\tfor (const [event, groups] of Object.entries(config)) {\n\t\tfor (const group of groups) {\n\t\t\tfor (const cmd of group.hooks) {\n\t\t\t\tif (typeof cmd.command !== \"string\" || !cmd.command) continue;\n\t\t\t\tout.push({\n\t\t\t\t\tevent,\n\t\t\t\t\t...(group.matcher ? { matcher: group.matcher } : {}),\n\t\t\t\t\tcommand: cmd.command,\n\t\t\t\t\t...(cmd.timeout ? { timeout: cmd.timeout } : {}),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\treturn out;\n}\n\n/**\n * Convert a parsed `mcpServers` record back to authored form. Throws on a\n * non-stdio (url/http-type) server rather than silently dropping it from the\n * re-emit — a merge must never quietly lose a capability (only reachable via a\n * hand-edited authored plugin; our own schema always writes `command` servers).\n */\nfunction mcpRecordToAuthored(record: Record<string, unknown>): AuthoredMcpServer[] {\n\tconst out: AuthoredMcpServer[] = [];\n\tfor (const [name, value] of Object.entries(record)) {\n\t\tif (!value || typeof value !== \"object\") continue;\n\t\tconst server = value as { command?: unknown; args?: unknown; env?: unknown };\n\t\tif (typeof server.command !== \"string\") {\n\t\t\tthrow new Error(\n\t\t\t\t`Cannot merge: MCP server \"${name}\" has no command (url/http-type servers don't round-trip through authoring). ` +\n\t\t\t\t\t\"Edit the plugin's .mcp.json directly instead.\",\n\t\t\t);\n\t\t}\n\t\tout.push({\n\t\t\tname,\n\t\t\tcommand: server.command,\n\t\t\t...(Array.isArray(server.args) ? { args: server.args.map(String) } : {}),\n\t\t\t...(server.env && typeof server.env === \"object\" ? { env: server.env as Record<string, string> } : {}),\n\t\t});\n\t}\n\treturn out;\n}\n\n/** Dedupe authored hooks by (event, matcher, command) so a re-supplied hook doesn't stack. */\nfunction dedupeHooks(hooks: AuthoredHook[]): AuthoredHook[] {\n\tconst seen = new Set<string>();\n\tconst out: AuthoredHook[] = [];\n\tfor (const h of hooks) {\n\t\tconst key = `${h.event}\u0000${h.matcher ?? \"\"}\u0000${h.command}`;\n\t\tif (seen.has(key)) continue;\n\t\tseen.add(key);\n\t\tout.push(h);\n\t}\n\treturn out;\n}\n\n/**\n * Merge inline-authored `delta` capabilities into the existing local plugin `id`\n * and re-emit. Unlike a marketplace `UpdatePlugin`, nothing is fetched from a\n * remote source — the new content comes from the caller — so the supply-chain\n * \"benign v1 → hostile v2\" vector the spec guards against is structurally absent.\n *\n * Merge semantics:\n * - **Skills / commands / agents** are directory-scanned, so existing ones are\n * left on disk untouched; a delta entry with a matching name overwrites just\n * that file (an update), a new name is added.\n * - **Hooks** and **MCP servers** live in single files that a re-emit rewrites,\n * so they are re-emitted as the *union* of existing + delta (MCP keyed by\n * server name with delta winning; hooks deduped by event/matcher/command).\n * Hooks have no name, so there is deliberately no modify-in-place: a delta\n * hook with the same event/matcher but a different command is a NEW hook\n * added alongside the old one, never a replacement. (Keying replacement by\n * event+matcher would silently drop legitimate sibling hooks that share\n * them.) Changing a hook = {@link removeFromPlugin} the old one + merge the\n * new one.\n * - **Metadata** (version, description, author) takes the delta's value when\n * provided, else keeps the existing one.\n *\n * Platforms default to the plugin's existing `supportPlatform` so a merge never\n * silently adds or drops a vendor layout.\n */\nexport function mergePluginDraft(\n\tcwd: string,\n\tid: string,\n\tdelta: Partial<PluginDraft>,\n\tplatforms?: MarketplacePlatform[],\n): WriteResult {\n\tconst existing = getPlugin(cwd, id);\n\tif (!existing) {\n\t\tthrow new Error(`Cannot update plugin \"${id}\": it does not exist. Use ProposePlugin to create it first.`);\n\t}\n\t// Authored-only: merging re-emits manifests and hook/MCP files through our\n\t// writer, which only round-trips what PluginDraft can represent. Running that\n\t// over a marketplace install could silently drop fields it carries (capability\n\t// -dir overrides, url-type MCP servers, extra manifest keys).\n\tif (!isAuthoredPlugin(cwd, id)) {\n\t\tthrow new Error(\n\t\t\t`Cannot update plugin \"${id}\": it was not authored here (no ${AUTHORED_MARKER_FILE} marker). ` +\n\t\t\t\t\"Only locally authored plugins can be merged.\",\n\t\t);\n\t}\n\n\tconst existingHooks = existing.hooks ? hooksConfigToAuthored(existing.hooks) : [];\n\tconst mergedHooks = dedupeHooks([...existingHooks, ...(delta.hooks ?? [])]);\n\n\tconst mcpByName = new Map<string, AuthoredMcpServer>();\n\tfor (const s of existing.mcpServers ? mcpRecordToAuthored(existing.mcpServers) : []) mcpByName.set(s.name, s);\n\tfor (const s of delta.mcpServers ?? []) mcpByName.set(s.name, s);\n\n\tconst targets = resolveAuthoringPlatforms(platforms ?? existing.supportPlatform);\n\tconst merged: PluginDraft = {\n\t\tid,\n\t\tversion: delta.version ?? existing.version,\n\t\tdescription: delta.description ?? existing.description,\n\t\tauthor: delta.author ?? existing.author,\n\t\tsupportPlatform: targets,\n\t\t// Directory-scanned capabilities: delta-only; existing files stay on disk.\n\t\tskills: delta.skills,\n\t\tcommands: delta.commands,\n\t\tagents: delta.agents,\n\t\t// Single-file capabilities: re-emit the union so a merge never drops them.\n\t\thooks: mergedHooks.length ? mergedHooks : undefined,\n\t\tmcpServers: mcpByName.size ? [...mcpByName.values()] : undefined,\n\t};\n\treturn writePluginDraft(cwd, merged, targets);\n}\n\n/** A hook to remove: `event` is required; `matcher`/`command` narrow the match when provided. */\nexport interface HookRemovalSpec {\n\tevent: string;\n\tmatcher?: string;\n\tcommand?: string;\n}\n\n/** Named capabilities to remove from an authored plugin. */\nexport interface RemovalSpec {\n\tskills?: string[];\n\tcommands?: string[];\n\tsubagents?: string[];\n\tmcpServers?: string[];\n\thooks?: HookRemovalSpec[];\n}\n\nexport interface RemoveResult {\n\tdest: string;\n\t/** Human-readable descriptions of what was removed. */\n\tremoved: string[];\n\t/** Requested capabilities that were not found (nothing was removed for these). */\n\tmissing: string[];\n}\n\nfunction describeHookSpec(h: HookRemovalSpec): string {\n\treturn `hook [${h.event}${h.matcher !== undefined ? ` matcher=${h.matcher}` : \"\"}${h.command !== undefined ? ` command=${h.command}` : \"\"}]`;\n}\n\n/**\n * Remove named capabilities from the authored plugin `id`. The inverse of the\n * additive merge, and — like {@link mergePluginDraft} — authored-only.\n *\n * Removal is the low-risk direction (deleting capabilities cannot execute\n * code), which is why callers may run it without a confirmation gate.\n *\n * - **Skills / commands / subagents** are directory-scanned, so removal is a\n * surgical file delete at our emit conventions; no re-emit needed.\n * - **Hooks** (matched by event, narrowed by matcher/command when given) and\n * **MCP servers** (by name) live in single files, so the remaining set is\n * re-emitted — and when a set empties, its file is DELETED, because the\n * parser falls back to `hooks/hooks.json` / `.mcp.json` on disk and a stale\n * file would resurrect the removed capability on the next parse.\n */\nexport function removeFromPlugin(cwd: string, id: string, spec: RemovalSpec): RemoveResult {\n\tconst existing = getPlugin(cwd, id);\n\tif (!existing) {\n\t\tthrow new Error(`Cannot remove from plugin \"${id}\": it does not exist.`);\n\t}\n\tif (!isAuthoredPlugin(cwd, id)) {\n\t\tthrow new Error(\n\t\t\t`Cannot remove from plugin \"${id}\": it was not authored here (no ${AUTHORED_MARKER_FILE} marker). ` +\n\t\t\t\t\"Only locally authored plugins can be edited.\",\n\t\t);\n\t}\n\tconst dest = path.join(installedPluginsDir(cwd), sanitizeForDir(id));\n\tconst removed: string[] = [];\n\tconst missing: string[] = [];\n\n\t// Directory-scanned capabilities: surgical deletes at our emit conventions.\n\tconst fileTargets: Array<[kind: string, name: string, relPath: string]> = [\n\t\t...(spec.skills ?? []).map((n): [string, string, string] => [\"skill\", n, path.join(\"skills\", slug(n))]),\n\t\t...(spec.commands ?? []).map((n): [string, string, string] => [\n\t\t\t\"command\",\n\t\t\tn,\n\t\t\tpath.join(\"commands\", `${slug(n)}.md`),\n\t\t]),\n\t\t...(spec.subagents ?? []).map((n): [string, string, string] => [\n\t\t\t\"subagent\",\n\t\t\tn,\n\t\t\tpath.join(\"agents\", `${slug(n)}.md`),\n\t\t]),\n\t];\n\tfor (const [kind, name, rel] of fileTargets) {\n\t\tconst abs = path.join(dest, rel);\n\t\tif (existsSync(abs)) {\n\t\t\trmSync(abs, { recursive: true, force: true });\n\t\t\tremoved.push(`${kind} \"${name}\"`);\n\t\t} else {\n\t\t\tmissing.push(`${kind} \"${name}\"`);\n\t\t}\n\t}\n\n\t// Single-file capabilities: filter the reconstructed sets, then re-emit.\n\tlet singleFileChanged = false;\n\tlet remainingHooks = existing.hooks ? hooksConfigToAuthored(existing.hooks) : [];\n\tfor (const h of spec.hooks ?? []) {\n\t\tconst before = remainingHooks.length;\n\t\tremainingHooks = remainingHooks.filter(\n\t\t\t(x) =>\n\t\t\t\t!(\n\t\t\t\t\tx.event === h.event &&\n\t\t\t\t\t(h.matcher === undefined || (x.matcher ?? \"\") === h.matcher) &&\n\t\t\t\t\t(h.command === undefined || x.command === h.command)\n\t\t\t\t),\n\t\t);\n\t\tconst n = before - remainingHooks.length;\n\t\tif (n > 0) {\n\t\t\tremoved.push(`${n} ${describeHookSpec(h)}`);\n\t\t\tsingleFileChanged = true;\n\t\t} else {\n\t\t\tmissing.push(describeHookSpec(h));\n\t\t}\n\t}\n\tlet remainingMcp = existing.mcpServers ? mcpRecordToAuthored(existing.mcpServers) : [];\n\tfor (const name of spec.mcpServers ?? []) {\n\t\tconst before = remainingMcp.length;\n\t\tremainingMcp = remainingMcp.filter((s) => s.name !== name);\n\t\tif (remainingMcp.length < before) {\n\t\t\tremoved.push(`mcp server \"${name}\"`);\n\t\t\tsingleFileChanged = true;\n\t\t} else {\n\t\t\tmissing.push(`mcp server \"${name}\"`);\n\t\t}\n\t}\n\n\tif (singleFileChanged) {\n\t\tconst targets = resolveAuthoringPlatforms(existing.supportPlatform);\n\t\twritePluginDraft(\n\t\t\tcwd,\n\t\t\t{\n\t\t\t\tid,\n\t\t\t\tversion: existing.version,\n\t\t\t\tdescription: existing.description,\n\t\t\t\tauthor: existing.author,\n\t\t\t\tsupportPlatform: targets,\n\t\t\t\thooks: remainingHooks.length ? remainingHooks : undefined,\n\t\t\t\tmcpServers: remainingMcp.length ? remainingMcp : undefined,\n\t\t\t},\n\t\t\ttargets,\n\t\t);\n\t\t// Emit skips empty sets, so a stale file from the previous emit survives\n\t\t// and the parser's on-disk fallback would resurrect it — delete explicitly.\n\t\tif (remainingHooks.length === 0) rmSync(path.join(dest, \"hooks\", \"hooks.json\"), { force: true });\n\t\tif (remainingMcp.length === 0) rmSync(path.join(dest, \".mcp.json\"), { force: true });\n\t}\n\n\treturn { dest, removed, missing };\n}\n"]}
@@ -2,33 +2,72 @@
2
2
  * Model categories for subagent model selection.
3
3
  *
4
4
  * A category (`fast` | `standard` | `capable`) is a provider-neutral indirection
5
- * that maps to an explicit model id via `settings.modelCategories`. No concrete
6
- * model names are baked in here, so the feature never assumes a particular
7
- * provider. When a category is not configured, it resolves to `undefined` —
8
- * callers treat that as "no override" and fall back to the agent's or parent's
9
- * default model.
5
+ * that maps to an explicit model id. Precedence:
6
+ *
7
+ * 1. An explicit `settings.modelCategories[category]` always wins.
8
+ * 2. Otherwise, when a set of available models is supplied, the category
9
+ * resolves to a default *derived* from those models (see
10
+ * `deriveDefaultModelCategories`) — never a hardcoded provider/model id.
11
+ * 3. Otherwise it resolves to `undefined`, which callers treat as "no override"
12
+ * and fall back to the agent's or parent's default model.
13
+ *
14
+ * No concrete model names are baked in here, so the feature never assumes a
15
+ * particular provider.
10
16
  */
17
+ import type { Api, Model } from "@kolisachint/hoocode-ai";
11
18
  import type { Settings } from "./settings-manager.js";
12
19
  /** Valid model category names */
13
20
  export type ModelCategory = "fast" | "standard" | "capable";
14
21
  /** Check if a string is a valid model category */
15
22
  export declare function isModelCategory(value: string): value is ModelCategory;
16
23
  /**
17
- * Resolve a model category to the model id configured for it in settings, or
18
- * `undefined` when the category is not configured. There is no built-in default:
19
- * an unconfigured category is a no-op so the caller keeps its existing model.
24
+ * Derive a default model for each tier from the user's available models, used
25
+ * only when a tier is not explicitly configured in `settings.modelCategories`.
26
+ *
27
+ * The rule is deliberately transparent (config, not magic) and provider-neutral:
28
+ * nothing is hardcoded, everything is derived from what the user actually has.
29
+ *
30
+ * 1. `capable` = the user's PRIMARY model: the configured default
31
+ * (`settings.defaultProvider`/`defaultModel`) when it is in the available
32
+ * set, otherwise the most capable available model, using combined token
33
+ * price (input + output cost) as a stand-in for capability.
34
+ * 2. `fast` and `standard` are the cheapest and the upper-median of every
35
+ * available model priced at or below `capable`, ordered cheapest-first.
36
+ * Clamping to `capable`'s price keeps the tiers monotonic
37
+ * (`fast` <= `standard` <= `capable`), and drawing from the whole available
38
+ * set — not just `capable`'s own provider — still yields a genuinely cheap
39
+ * `fast` when the primary model's provider has nothing cheaper (a strict
40
+ * same-provider rule collapses every tier onto a single-model provider).
41
+ *
42
+ * Every ordering breaks ties on a fixed key (context window, then id) so the same
43
+ * available set always yields the same mapping. An empty available set yields an
44
+ * empty map (every tier resolves to `undefined`, i.e. inherit the parent model).
45
+ */
46
+ export declare function deriveDefaultModelCategories(availableModels: readonly Model<Api>[], settings?: Settings): {
47
+ fast?: string;
48
+ standard?: string;
49
+ capable?: string;
50
+ };
51
+ /**
52
+ * Resolve a model category to a model id. An explicit
53
+ * `settings.modelCategories[category]` wins; otherwise a default is derived from
54
+ * `availableModels` (provider-neutral, see `deriveDefaultModelCategories`); when
55
+ * neither applies the category resolves to `undefined` (a no-op, so the caller
56
+ * keeps its existing model).
20
57
  *
21
58
  * @param category - The model category (fast, standard, capable)
22
59
  * @param settings - The current settings (may contain modelCategories config)
60
+ * @param availableModels - The user's available/configured models to derive from
23
61
  */
24
- export declare function resolveModelCategory(category: ModelCategory, settings?: Settings): string | undefined;
62
+ export declare function resolveModelCategory(category: ModelCategory, settings?: Settings, availableModels?: readonly Model<Api>[]): string | undefined;
25
63
  /**
26
64
  * Resolve a model string that might be a category reference. A category resolves
27
- * to its configured model id (or `undefined` when unconfigured); any other string
28
- * is already a concrete model id or alias and is returned as-is.
65
+ * to its configured or derived model id (or `undefined` when neither applies);
66
+ * any other string is already a concrete model id or alias and is returned as-is.
29
67
  *
30
68
  * @param model - The model string (could be a category, alias, or full model ID)
31
69
  * @param settings - The current settings (may contain modelCategories config)
70
+ * @param availableModels - The user's available/configured models to derive from
32
71
  */
33
- export declare function resolveModelReference(model: string, settings?: Settings): string | undefined;
72
+ export declare function resolveModelReference(model: string, settings?: Settings, availableModels?: readonly Model<Api>[]): string | undefined;
34
73
  //# sourceMappingURL=model-categories.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-categories.d.ts","sourceRoot":"","sources":["../../src/core/model-categories.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEtD,iCAAiC;AACjC,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;AAE5D,kDAAkD;AAClD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,aAAa,CAErE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,SAAS,CAErG;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,MAAM,GAAG,SAAS,CAK5F","sourcesContent":["/**\n * Model categories for subagent model selection.\n *\n * A category (`fast` | `standard` | `capable`) is a provider-neutral indirection\n * that maps to an explicit model id via `settings.modelCategories`. No concrete\n * model names are baked in here, so the feature never assumes a particular\n * provider. When a category is not configured, it resolves to `undefined` —\n * callers treat that as \"no override\" and fall back to the agent's or parent's\n * default model.\n */\n\nimport type { Settings } from \"./settings-manager.js\";\n\n/** Valid model category names */\nexport type ModelCategory = \"fast\" | \"standard\" | \"capable\";\n\n/** Check if a string is a valid model category */\nexport function isModelCategory(value: string): value is ModelCategory {\n\treturn value === \"fast\" || value === \"standard\" || value === \"capable\";\n}\n\n/**\n * Resolve a model category to the model id configured for it in settings, or\n * `undefined` when the category is not configured. There is no built-in default:\n * an unconfigured category is a no-op so the caller keeps its existing model.\n *\n * @param category - The model category (fast, standard, capable)\n * @param settings - The current settings (may contain modelCategories config)\n */\nexport function resolveModelCategory(category: ModelCategory, settings?: Settings): string | undefined {\n\treturn settings?.modelCategories?.[category];\n}\n\n/**\n * Resolve a model string that might be a category reference. A category resolves\n * to its configured model id (or `undefined` when unconfigured); any other string\n * is already a concrete model id or alias and is returned as-is.\n *\n * @param model - The model string (could be a category, alias, or full model ID)\n * @param settings - The current settings (may contain modelCategories config)\n */\nexport function resolveModelReference(model: string, settings?: Settings): string | undefined {\n\tif (isModelCategory(model)) {\n\t\treturn resolveModelCategory(model, settings);\n\t}\n\treturn model;\n}\n"]}
1
+ {"version":3,"file":"model-categories.d.ts","sourceRoot":"","sources":["../../src/core/model-categories.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEtD,iCAAiC;AACjC,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;AAE5D,kDAAkD;AAClD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,aAAa,CAErE;AAiBD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,4BAA4B,CAC3C,eAAe,EAAE,SAAS,KAAK,CAAC,GAAG,CAAC,EAAE,EACtC,QAAQ,CAAC,EAAE,QAAQ,GACjB;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CA8BxD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CACnC,QAAQ,EAAE,aAAa,EACvB,QAAQ,CAAC,EAAE,QAAQ,EACnB,eAAe,CAAC,EAAE,SAAS,KAAK,CAAC,GAAG,CAAC,EAAE,GACrC,MAAM,GAAG,SAAS,CAOpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACpC,KAAK,EAAE,MAAM,EACb,QAAQ,CAAC,EAAE,QAAQ,EACnB,eAAe,CAAC,EAAE,SAAS,KAAK,CAAC,GAAG,CAAC,EAAE,GACrC,MAAM,GAAG,SAAS,CAKpB","sourcesContent":["/**\n * Model categories for subagent model selection.\n *\n * A category (`fast` | `standard` | `capable`) is a provider-neutral indirection\n * that maps to an explicit model id. Precedence:\n *\n * 1. An explicit `settings.modelCategories[category]` always wins.\n * 2. Otherwise, when a set of available models is supplied, the category\n * resolves to a default *derived* from those models (see\n * `deriveDefaultModelCategories`) never a hardcoded provider/model id.\n * 3. Otherwise it resolves to `undefined`, which callers treat as \"no override\"\n * and fall back to the agent's or parent's default model.\n *\n * No concrete model names are baked in here, so the feature never assumes a\n * particular provider.\n */\n\nimport type { Api, Model } from \"@kolisachint/hoocode-ai\";\nimport type { Settings } from \"./settings-manager.js\";\n\n/** Valid model category names */\nexport type ModelCategory = \"fast\" | \"standard\" | \"capable\";\n\n/** Check if a string is a valid model category */\nexport function isModelCategory(value: string): value is ModelCategory {\n\treturn value === \"fast\" || value === \"standard\" || value === \"capable\";\n}\n\n/** A category maps to a concrete model reference in `<provider>/<id>` form. */\nfunction modelRef(model: Model<Api>): string {\n\treturn `${model.provider}/${model.id}`;\n}\n\n/** Combined per-token price (input + output), used as a capability/cost proxy. */\nfunction combinedPrice(model: Model<Api>): number {\n\treturn model.cost.input + model.cost.output;\n}\n\n/** Deterministic tie-break so identical available sets always yield the same pick. */\nfunction compareById(a: Model<Api>, b: Model<Api>): number {\n\treturn a.id < b.id ? -1 : a.id > b.id ? 1 : 0;\n}\n\n/**\n * Derive a default model for each tier from the user's available models, used\n * only when a tier is not explicitly configured in `settings.modelCategories`.\n *\n * The rule is deliberately transparent (config, not magic) and provider-neutral:\n * nothing is hardcoded, everything is derived from what the user actually has.\n *\n * 1. `capable` = the user's PRIMARY model: the configured default\n * (`settings.defaultProvider`/`defaultModel`) when it is in the available\n * set, otherwise the most capable available model, using combined token\n * price (input + output cost) as a stand-in for capability.\n * 2. `fast` and `standard` are the cheapest and the upper-median of every\n * available model priced at or below `capable`, ordered cheapest-first.\n * Clamping to `capable`'s price keeps the tiers monotonic\n * (`fast` <= `standard` <= `capable`), and drawing from the whole available\n * set — not just `capable`'s own provider — still yields a genuinely cheap\n * `fast` when the primary model's provider has nothing cheaper (a strict\n * same-provider rule collapses every tier onto a single-model provider).\n *\n * Every ordering breaks ties on a fixed key (context window, then id) so the same\n * available set always yields the same mapping. An empty available set yields an\n * empty map (every tier resolves to `undefined`, i.e. inherit the parent model).\n */\nexport function deriveDefaultModelCategories(\n\tavailableModels: readonly Model<Api>[],\n\tsettings?: Settings,\n): { fast?: string; standard?: string; capable?: string } {\n\tif (availableModels.length === 0) return {};\n\n\t// capable = primary model.\n\tconst configuredDefault =\n\t\tsettings?.defaultProvider && settings?.defaultModel\n\t\t\t? availableModels.find((m) => m.provider === settings.defaultProvider && m.id === settings.defaultModel)\n\t\t\t: undefined;\n\t// Most capable = highest combined price; ties -> larger context window, then id.\n\tconst capable =\n\t\tconfiguredDefault ??\n\t\t[...availableModels].sort(\n\t\t\t(a, b) => combinedPrice(b) - combinedPrice(a) || b.contextWindow - a.contextWindow || compareById(a, b),\n\t\t)[0];\n\n\t// fast/standard: every model priced at or below capable, cheapest-first.\n\t// `capable` is always in this set (its price <= its own price), so it never empties.\n\tconst capablePrice = combinedPrice(capable);\n\tconst candidates = availableModels\n\t\t.filter((m) => combinedPrice(m) <= capablePrice)\n\t\t.sort((a, b) => combinedPrice(a) - combinedPrice(b) || a.contextWindow - b.contextWindow || compareById(a, b));\n\n\tconst fast = candidates[0] ?? capable;\n\tconst standard = candidates[Math.floor(candidates.length / 2)] ?? capable;\n\n\treturn {\n\t\tfast: modelRef(fast),\n\t\tstandard: modelRef(standard),\n\t\tcapable: modelRef(capable),\n\t};\n}\n\n/**\n * Resolve a model category to a model id. An explicit\n * `settings.modelCategories[category]` wins; otherwise a default is derived from\n * `availableModels` (provider-neutral, see `deriveDefaultModelCategories`); when\n * neither applies the category resolves to `undefined` (a no-op, so the caller\n * keeps its existing model).\n *\n * @param category - The model category (fast, standard, capable)\n * @param settings - The current settings (may contain modelCategories config)\n * @param availableModels - The user's available/configured models to derive from\n */\nexport function resolveModelCategory(\n\tcategory: ModelCategory,\n\tsettings?: Settings,\n\tavailableModels?: readonly Model<Api>[],\n): string | undefined {\n\tconst explicit = settings?.modelCategories?.[category];\n\tif (explicit) return explicit;\n\tif (availableModels && availableModels.length > 0) {\n\t\treturn deriveDefaultModelCategories(availableModels, settings)[category];\n\t}\n\treturn undefined;\n}\n\n/**\n * Resolve a model string that might be a category reference. A category resolves\n * to its configured or derived model id (or `undefined` when neither applies);\n * any other string is already a concrete model id or alias and is returned as-is.\n *\n * @param model - The model string (could be a category, alias, or full model ID)\n * @param settings - The current settings (may contain modelCategories config)\n * @param availableModels - The user's available/configured models to derive from\n */\nexport function resolveModelReference(\n\tmodel: string,\n\tsettings?: Settings,\n\tavailableModels?: readonly Model<Api>[],\n): string | undefined {\n\tif (isModelCategory(model)) {\n\t\treturn resolveModelCategory(model, settings, availableModels);\n\t}\n\treturn model;\n}\n"]}
@@ -2,38 +2,113 @@
2
2
  * Model categories for subagent model selection.
3
3
  *
4
4
  * A category (`fast` | `standard` | `capable`) is a provider-neutral indirection
5
- * that maps to an explicit model id via `settings.modelCategories`. No concrete
6
- * model names are baked in here, so the feature never assumes a particular
7
- * provider. When a category is not configured, it resolves to `undefined` —
8
- * callers treat that as "no override" and fall back to the agent's or parent's
9
- * default model.
5
+ * that maps to an explicit model id. Precedence:
6
+ *
7
+ * 1. An explicit `settings.modelCategories[category]` always wins.
8
+ * 2. Otherwise, when a set of available models is supplied, the category
9
+ * resolves to a default *derived* from those models (see
10
+ * `deriveDefaultModelCategories`) — never a hardcoded provider/model id.
11
+ * 3. Otherwise it resolves to `undefined`, which callers treat as "no override"
12
+ * and fall back to the agent's or parent's default model.
13
+ *
14
+ * No concrete model names are baked in here, so the feature never assumes a
15
+ * particular provider.
10
16
  */
11
17
  /** Check if a string is a valid model category */
12
18
  export function isModelCategory(value) {
13
19
  return value === "fast" || value === "standard" || value === "capable";
14
20
  }
21
+ /** A category maps to a concrete model reference in `<provider>/<id>` form. */
22
+ function modelRef(model) {
23
+ return `${model.provider}/${model.id}`;
24
+ }
25
+ /** Combined per-token price (input + output), used as a capability/cost proxy. */
26
+ function combinedPrice(model) {
27
+ return model.cost.input + model.cost.output;
28
+ }
29
+ /** Deterministic tie-break so identical available sets always yield the same pick. */
30
+ function compareById(a, b) {
31
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
32
+ }
33
+ /**
34
+ * Derive a default model for each tier from the user's available models, used
35
+ * only when a tier is not explicitly configured in `settings.modelCategories`.
36
+ *
37
+ * The rule is deliberately transparent (config, not magic) and provider-neutral:
38
+ * nothing is hardcoded, everything is derived from what the user actually has.
39
+ *
40
+ * 1. `capable` = the user's PRIMARY model: the configured default
41
+ * (`settings.defaultProvider`/`defaultModel`) when it is in the available
42
+ * set, otherwise the most capable available model, using combined token
43
+ * price (input + output cost) as a stand-in for capability.
44
+ * 2. `fast` and `standard` are the cheapest and the upper-median of every
45
+ * available model priced at or below `capable`, ordered cheapest-first.
46
+ * Clamping to `capable`'s price keeps the tiers monotonic
47
+ * (`fast` <= `standard` <= `capable`), and drawing from the whole available
48
+ * set — not just `capable`'s own provider — still yields a genuinely cheap
49
+ * `fast` when the primary model's provider has nothing cheaper (a strict
50
+ * same-provider rule collapses every tier onto a single-model provider).
51
+ *
52
+ * Every ordering breaks ties on a fixed key (context window, then id) so the same
53
+ * available set always yields the same mapping. An empty available set yields an
54
+ * empty map (every tier resolves to `undefined`, i.e. inherit the parent model).
55
+ */
56
+ export function deriveDefaultModelCategories(availableModels, settings) {
57
+ if (availableModels.length === 0)
58
+ return {};
59
+ // capable = primary model.
60
+ const configuredDefault = settings?.defaultProvider && settings?.defaultModel
61
+ ? availableModels.find((m) => m.provider === settings.defaultProvider && m.id === settings.defaultModel)
62
+ : undefined;
63
+ // Most capable = highest combined price; ties -> larger context window, then id.
64
+ const capable = configuredDefault ??
65
+ [...availableModels].sort((a, b) => combinedPrice(b) - combinedPrice(a) || b.contextWindow - a.contextWindow || compareById(a, b))[0];
66
+ // fast/standard: every model priced at or below capable, cheapest-first.
67
+ // `capable` is always in this set (its price <= its own price), so it never empties.
68
+ const capablePrice = combinedPrice(capable);
69
+ const candidates = availableModels
70
+ .filter((m) => combinedPrice(m) <= capablePrice)
71
+ .sort((a, b) => combinedPrice(a) - combinedPrice(b) || a.contextWindow - b.contextWindow || compareById(a, b));
72
+ const fast = candidates[0] ?? capable;
73
+ const standard = candidates[Math.floor(candidates.length / 2)] ?? capable;
74
+ return {
75
+ fast: modelRef(fast),
76
+ standard: modelRef(standard),
77
+ capable: modelRef(capable),
78
+ };
79
+ }
15
80
  /**
16
- * Resolve a model category to the model id configured for it in settings, or
17
- * `undefined` when the category is not configured. There is no built-in default:
18
- * an unconfigured category is a no-op so the caller keeps its existing model.
81
+ * Resolve a model category to a model id. An explicit
82
+ * `settings.modelCategories[category]` wins; otherwise a default is derived from
83
+ * `availableModels` (provider-neutral, see `deriveDefaultModelCategories`); when
84
+ * neither applies the category resolves to `undefined` (a no-op, so the caller
85
+ * keeps its existing model).
19
86
  *
20
87
  * @param category - The model category (fast, standard, capable)
21
88
  * @param settings - The current settings (may contain modelCategories config)
89
+ * @param availableModels - The user's available/configured models to derive from
22
90
  */
23
- export function resolveModelCategory(category, settings) {
24
- return settings?.modelCategories?.[category];
91
+ export function resolveModelCategory(category, settings, availableModels) {
92
+ const explicit = settings?.modelCategories?.[category];
93
+ if (explicit)
94
+ return explicit;
95
+ if (availableModels && availableModels.length > 0) {
96
+ return deriveDefaultModelCategories(availableModels, settings)[category];
97
+ }
98
+ return undefined;
25
99
  }
26
100
  /**
27
101
  * Resolve a model string that might be a category reference. A category resolves
28
- * to its configured model id (or `undefined` when unconfigured); any other string
29
- * is already a concrete model id or alias and is returned as-is.
102
+ * to its configured or derived model id (or `undefined` when neither applies);
103
+ * any other string is already a concrete model id or alias and is returned as-is.
30
104
  *
31
105
  * @param model - The model string (could be a category, alias, or full model ID)
32
106
  * @param settings - The current settings (may contain modelCategories config)
107
+ * @param availableModels - The user's available/configured models to derive from
33
108
  */
34
- export function resolveModelReference(model, settings) {
109
+ export function resolveModelReference(model, settings, availableModels) {
35
110
  if (isModelCategory(model)) {
36
- return resolveModelCategory(model, settings);
111
+ return resolveModelCategory(model, settings, availableModels);
37
112
  }
38
113
  return model;
39
114
  }
@@ -1 +1 @@
1
- {"version":3,"file":"model-categories.js","sourceRoot":"","sources":["../../src/core/model-categories.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAOH,kDAAkD;AAClD,MAAM,UAAU,eAAe,CAAC,KAAa,EAA0B;IACtE,OAAO,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,UAAU,IAAI,KAAK,KAAK,SAAS,CAAC;AAAA,CACvE;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAuB,EAAE,QAAmB,EAAsB;IACtG,OAAO,QAAQ,EAAE,eAAe,EAAE,CAAC,QAAQ,CAAC,CAAC;AAAA,CAC7C;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAa,EAAE,QAAmB,EAAsB;IAC7F,IAAI,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO,oBAAoB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IAC9C,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb","sourcesContent":["/**\n * Model categories for subagent model selection.\n *\n * A category (`fast` | `standard` | `capable`) is a provider-neutral indirection\n * that maps to an explicit model id via `settings.modelCategories`. No concrete\n * model names are baked in here, so the feature never assumes a particular\n * provider. When a category is not configured, it resolves to `undefined` —\n * callers treat that as \"no override\" and fall back to the agent's or parent's\n * default model.\n */\n\nimport type { Settings } from \"./settings-manager.js\";\n\n/** Valid model category names */\nexport type ModelCategory = \"fast\" | \"standard\" | \"capable\";\n\n/** Check if a string is a valid model category */\nexport function isModelCategory(value: string): value is ModelCategory {\n\treturn value === \"fast\" || value === \"standard\" || value === \"capable\";\n}\n\n/**\n * Resolve a model category to the model id configured for it in settings, or\n * `undefined` when the category is not configured. There is no built-in default:\n * an unconfigured category is a no-op so the caller keeps its existing model.\n *\n * @param category - The model category (fast, standard, capable)\n * @param settings - The current settings (may contain modelCategories config)\n */\nexport function resolveModelCategory(category: ModelCategory, settings?: Settings): string | undefined {\n\treturn settings?.modelCategories?.[category];\n}\n\n/**\n * Resolve a model string that might be a category reference. A category resolves\n * to its configured model id (or `undefined` when unconfigured); any other string\n * is already a concrete model id or alias and is returned as-is.\n *\n * @param model - The model string (could be a category, alias, or full model ID)\n * @param settings - The current settings (may contain modelCategories config)\n */\nexport function resolveModelReference(model: string, settings?: Settings): string | undefined {\n\tif (isModelCategory(model)) {\n\t\treturn resolveModelCategory(model, settings);\n\t}\n\treturn model;\n}\n"]}
1
+ {"version":3,"file":"model-categories.js","sourceRoot":"","sources":["../../src/core/model-categories.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Model categories for subagent model selection.\n *\n * A category (`fast` | `standard` | `capable`) is a provider-neutral indirection\n * that maps to an explicit model id. Precedence:\n *\n * 1. An explicit `settings.modelCategories[category]` always wins.\n * 2. Otherwise, when a set of available models is supplied, the category\n * resolves to a default *derived* from those models (see\n * `deriveDefaultModelCategories`) never a hardcoded provider/model id.\n * 3. Otherwise it resolves to `undefined`, which callers treat as \"no override\"\n * and fall back to the agent's or parent's default model.\n *\n * No concrete model names are baked in here, so the feature never assumes a\n * particular provider.\n */\n\nimport type { Api, Model } from \"@kolisachint/hoocode-ai\";\nimport type { Settings } from \"./settings-manager.js\";\n\n/** Valid model category names */\nexport type ModelCategory = \"fast\" | \"standard\" | \"capable\";\n\n/** Check if a string is a valid model category */\nexport function isModelCategory(value: string): value is ModelCategory {\n\treturn value === \"fast\" || value === \"standard\" || value === \"capable\";\n}\n\n/** A category maps to a concrete model reference in `<provider>/<id>` form. */\nfunction modelRef(model: Model<Api>): string {\n\treturn `${model.provider}/${model.id}`;\n}\n\n/** Combined per-token price (input + output), used as a capability/cost proxy. */\nfunction combinedPrice(model: Model<Api>): number {\n\treturn model.cost.input + model.cost.output;\n}\n\n/** Deterministic tie-break so identical available sets always yield the same pick. */\nfunction compareById(a: Model<Api>, b: Model<Api>): number {\n\treturn a.id < b.id ? -1 : a.id > b.id ? 1 : 0;\n}\n\n/**\n * Derive a default model for each tier from the user's available models, used\n * only when a tier is not explicitly configured in `settings.modelCategories`.\n *\n * The rule is deliberately transparent (config, not magic) and provider-neutral:\n * nothing is hardcoded, everything is derived from what the user actually has.\n *\n * 1. `capable` = the user's PRIMARY model: the configured default\n * (`settings.defaultProvider`/`defaultModel`) when it is in the available\n * set, otherwise the most capable available model, using combined token\n * price (input + output cost) as a stand-in for capability.\n * 2. `fast` and `standard` are the cheapest and the upper-median of every\n * available model priced at or below `capable`, ordered cheapest-first.\n * Clamping to `capable`'s price keeps the tiers monotonic\n * (`fast` <= `standard` <= `capable`), and drawing from the whole available\n * set — not just `capable`'s own provider — still yields a genuinely cheap\n * `fast` when the primary model's provider has nothing cheaper (a strict\n * same-provider rule collapses every tier onto a single-model provider).\n *\n * Every ordering breaks ties on a fixed key (context window, then id) so the same\n * available set always yields the same mapping. An empty available set yields an\n * empty map (every tier resolves to `undefined`, i.e. inherit the parent model).\n */\nexport function deriveDefaultModelCategories(\n\tavailableModels: readonly Model<Api>[],\n\tsettings?: Settings,\n): { fast?: string; standard?: string; capable?: string } {\n\tif (availableModels.length === 0) return {};\n\n\t// capable = primary model.\n\tconst configuredDefault =\n\t\tsettings?.defaultProvider && settings?.defaultModel\n\t\t\t? availableModels.find((m) => m.provider === settings.defaultProvider && m.id === settings.defaultModel)\n\t\t\t: undefined;\n\t// Most capable = highest combined price; ties -> larger context window, then id.\n\tconst capable =\n\t\tconfiguredDefault ??\n\t\t[...availableModels].sort(\n\t\t\t(a, b) => combinedPrice(b) - combinedPrice(a) || b.contextWindow - a.contextWindow || compareById(a, b),\n\t\t)[0];\n\n\t// fast/standard: every model priced at or below capable, cheapest-first.\n\t// `capable` is always in this set (its price <= its own price), so it never empties.\n\tconst capablePrice = combinedPrice(capable);\n\tconst candidates = availableModels\n\t\t.filter((m) => combinedPrice(m) <= capablePrice)\n\t\t.sort((a, b) => combinedPrice(a) - combinedPrice(b) || a.contextWindow - b.contextWindow || compareById(a, b));\n\n\tconst fast = candidates[0] ?? capable;\n\tconst standard = candidates[Math.floor(candidates.length / 2)] ?? capable;\n\n\treturn {\n\t\tfast: modelRef(fast),\n\t\tstandard: modelRef(standard),\n\t\tcapable: modelRef(capable),\n\t};\n}\n\n/**\n * Resolve a model category to a model id. An explicit\n * `settings.modelCategories[category]` wins; otherwise a default is derived from\n * `availableModels` (provider-neutral, see `deriveDefaultModelCategories`); when\n * neither applies the category resolves to `undefined` (a no-op, so the caller\n * keeps its existing model).\n *\n * @param category - The model category (fast, standard, capable)\n * @param settings - The current settings (may contain modelCategories config)\n * @param availableModels - The user's available/configured models to derive from\n */\nexport function resolveModelCategory(\n\tcategory: ModelCategory,\n\tsettings?: Settings,\n\tavailableModels?: readonly Model<Api>[],\n): string | undefined {\n\tconst explicit = settings?.modelCategories?.[category];\n\tif (explicit) return explicit;\n\tif (availableModels && availableModels.length > 0) {\n\t\treturn deriveDefaultModelCategories(availableModels, settings)[category];\n\t}\n\treturn undefined;\n}\n\n/**\n * Resolve a model string that might be a category reference. A category resolves\n * to its configured or derived model id (or `undefined` when neither applies);\n * any other string is already a concrete model id or alias and is returned as-is.\n *\n * @param model - The model string (could be a category, alias, or full model ID)\n * @param settings - The current settings (may contain modelCategories config)\n * @param availableModels - The user's available/configured models to derive from\n */\nexport function resolveModelReference(\n\tmodel: string,\n\tsettings?: Settings,\n\tavailableModels?: readonly Model<Api>[],\n): string | undefined {\n\tif (isModelCategory(model)) {\n\t\treturn resolveModelCategory(model, settings, availableModels);\n\t}\n\treturn model;\n}\n"]}
@@ -59,8 +59,20 @@ export interface WarningSettings {
59
59
  /**
60
60
  * Model categories for subagent model selection.
61
61
  * Categories map to explicit model IDs (e.g., "<provider>/<model-id>").
62
- * When a category is not configured, no override is applied and the agent's or
63
- * parent's default model is used.
62
+ *
63
+ * A field set here always wins. When a tier is left unset, it does NOT become a
64
+ * no-op: it resolves to a default *derived* from the user's available models
65
+ * (nothing is hardcoded, so the feature stays provider-neutral). The derivation,
66
+ * implemented in `deriveDefaultModelCategories` (core/model-categories.ts), is:
67
+ * - `capable` = the user's primary model — the configured default
68
+ * (`defaultProvider`/`defaultModel`) when available, else the most capable
69
+ * available model (highest combined input+output token price as a proxy).
70
+ * - `fast` / `standard` = the cheapest / upper-median of every available model
71
+ * priced at or below `capable`, ordered cheapest-first. Clamping to
72
+ * `capable`'s price keeps tiers monotonic (`fast` <= `standard` <= `capable`).
73
+ * Ties break on a fixed key (context window, then id), so identical inputs always
74
+ * yield the same mapping. When no models are available, tiers stay unresolved and
75
+ * the agent's or parent's default model is used.
64
76
  */
65
77
  export interface ModelCategories {
66
78
  /** Quick, cheap models for read-only exploration (grep, find, file discovery) */