@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.
- package/CHANGELOG.md +34 -0
- package/dist/core/extensions/plugins/authoring.d.ts +67 -0
- package/dist/core/extensions/plugins/authoring.d.ts.map +1 -1
- package/dist/core/extensions/plugins/authoring.js +0 -0
- package/dist/core/extensions/plugins/authoring.js.map +1 -1
- package/dist/core/model-categories.d.ts +51 -12
- package/dist/core/model-categories.d.ts.map +1 -1
- package/dist/core/model-categories.js +89 -14
- package/dist/core/model-categories.js.map +1 -1
- package/dist/core/settings-types.d.ts +14 -2
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/subagent-pool-instance.d.ts +10 -2
- package/dist/core/subagent-pool-instance.d.ts.map +1 -1
- package/dist/core/subagent-pool-instance.js +10 -2
- package/dist/core/subagent-pool-instance.js.map +1 -1
- package/dist/core/subagent-pool.d.ts +9 -0
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +6 -3
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/tools/plugin-tool-names.d.ts +2 -1
- package/dist/core/tools/plugin-tool-names.d.ts.map +1 -1
- package/dist/core/tools/plugin-tool-names.js +11 -3
- package/dist/core/tools/plugin-tool-names.js.map +1 -1
- package/dist/core/tools/plugins.d.ts.map +1 -1
- package/dist/core/tools/plugins.js +10 -6
- package/dist/core/tools/plugins.js.map +1 -1
- package/dist/core/tools/propose-plugin.d.ts +26 -16
- package/dist/core/tools/propose-plugin.d.ts.map +1 -1
- package/dist/core/tools/propose-plugin.js +276 -143
- package/dist/core/tools/propose-plugin.js.map +1 -1
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +10 -4
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/core/warm-subagent-pool-instance.d.ts +9 -2
- package/dist/core/warm-subagent-pool-instance.d.ts.map +1 -1
- package/dist/core/warm-subagent-pool-instance.js +9 -3
- package/dist/core/warm-subagent-pool-instance.js.map +1 -1
- package/dist/core/warm-subagent-pool.d.ts +13 -2
- package/dist/core/warm-subagent-pool.d.ts.map +1 -1
- package/dist/core/warm-subagent-pool.js +16 -7
- package/dist/core/warm-subagent-pool.js.map +1 -1
- package/dist/modes/interactive/command-executor.d.ts.map +1 -1
- package/dist/modes/interactive/command-executor.js +1 -1
- package/dist/modes/interactive/command-executor.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- 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"]}
|
|
Binary file
|
|
@@ -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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* default
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* default
|
|
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
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
63
|
-
*
|
|
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) */
|