@kolisachint/hoocode-agent 0.4.129 → 0.4.131

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 (70) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/cli/args.d.ts +7 -0
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +18 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/context-gc.d.ts +14 -2
  7. package/dist/core/context-gc.d.ts.map +1 -1
  8. package/dist/core/context-gc.js +73 -22
  9. package/dist/core/context-gc.js.map +1 -1
  10. package/dist/core/extensions/plugins/authoring.d.ts +1 -2
  11. package/dist/core/extensions/plugins/authoring.d.ts.map +1 -1
  12. package/dist/core/extensions/plugins/authoring.js +5 -3
  13. package/dist/core/extensions/plugins/authoring.js.map +1 -1
  14. package/dist/core/extensions/plugins/formats/agents.d.ts.map +1 -1
  15. package/dist/core/extensions/plugins/formats/agents.js +1 -0
  16. package/dist/core/extensions/plugins/formats/agents.js.map +1 -1
  17. package/dist/core/extensions/plugins/formats/claude.d.ts.map +1 -1
  18. package/dist/core/extensions/plugins/formats/claude.js +1 -0
  19. package/dist/core/extensions/plugins/formats/claude.js.map +1 -1
  20. package/dist/core/extensions/plugins/formats/copilot.d.ts +24 -8
  21. package/dist/core/extensions/plugins/formats/copilot.d.ts.map +1 -1
  22. package/dist/core/extensions/plugins/formats/copilot.js +85 -25
  23. package/dist/core/extensions/plugins/formats/copilot.js.map +1 -1
  24. package/dist/core/extensions/plugins/formats/index.d.ts +2 -1
  25. package/dist/core/extensions/plugins/formats/index.d.ts.map +1 -1
  26. package/dist/core/extensions/plugins/formats/index.js +1 -0
  27. package/dist/core/extensions/plugins/formats/index.js.map +1 -1
  28. package/dist/core/extensions/plugins/formats/jsonManifest.d.ts +2 -0
  29. package/dist/core/extensions/plugins/formats/jsonManifest.d.ts.map +1 -1
  30. package/dist/core/extensions/plugins/formats/jsonManifest.js +4 -2
  31. package/dist/core/extensions/plugins/formats/jsonManifest.js.map +1 -1
  32. package/dist/core/extensions/plugins/formats/platform-targets.d.ts +43 -0
  33. package/dist/core/extensions/plugins/formats/platform-targets.d.ts.map +1 -0
  34. package/dist/core/extensions/plugins/formats/platform-targets.js +74 -0
  35. package/dist/core/extensions/plugins/formats/platform-targets.js.map +1 -0
  36. package/dist/core/extensions/plugins/formats/shared.d.ts +13 -1
  37. package/dist/core/extensions/plugins/formats/shared.d.ts.map +1 -1
  38. package/dist/core/extensions/plugins/formats/shared.js +36 -0
  39. package/dist/core/extensions/plugins/formats/shared.js.map +1 -1
  40. package/dist/core/extensions/plugins/formats/types.d.ts +17 -0
  41. package/dist/core/extensions/plugins/formats/types.d.ts.map +1 -1
  42. package/dist/core/extensions/plugins/formats/types.js.map +1 -1
  43. package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -1
  44. package/dist/core/extensions/plugins/marketplace.js +26 -9
  45. package/dist/core/extensions/plugins/marketplace.js.map +1 -1
  46. package/dist/core/sdk.d.ts.map +1 -1
  47. package/dist/core/sdk.js +24 -2
  48. package/dist/core/sdk.js.map +1 -1
  49. package/dist/core/settings-manager.d.ts +2 -0
  50. package/dist/core/settings-manager.d.ts.map +1 -1
  51. package/dist/core/settings-manager.js +7 -0
  52. package/dist/core/settings-manager.js.map +1 -1
  53. package/dist/core/settings-types.d.ts +1 -0
  54. package/dist/core/settings-types.d.ts.map +1 -1
  55. package/dist/core/settings-types.js.map +1 -1
  56. package/dist/core/tools/propose-plugin.d.ts.map +1 -1
  57. package/dist/core/tools/propose-plugin.js +9 -4
  58. package/dist/core/tools/propose-plugin.js.map +1 -1
  59. package/dist/extensions/core/scaffold.d.ts +10 -2
  60. package/dist/extensions/core/scaffold.d.ts.map +1 -1
  61. package/dist/extensions/core/scaffold.js +111 -16
  62. package/dist/extensions/core/scaffold.js.map +1 -1
  63. package/dist/main.d.ts.map +1 -1
  64. package/dist/main.js +25 -0
  65. package/dist/main.js.map +1 -1
  66. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  67. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  68. package/examples/extensions/sandbox/package.json +1 -1
  69. package/examples/extensions/with-deps/package.json +1 -1
  70. package/package.json +4 -4
@@ -20,14 +20,31 @@
20
20
  *
21
21
  * Deliberately conservative: paths are matched after `path.resolve`, so two
22
22
  * different files never collide, and any ambiguity results in NOT evicting.
23
- * Bash-output eviction is intentionally left out for now — that output is not
24
- * always recoverable, so it needs its own safeguards.
23
+ *
24
+ * Bash-output eviction is additionally gated on token-budget pressure
25
+ * (`options.budgetPressure`, the fraction of the model's context window in
26
+ * use). At 0 pressure — the default — behaviour is identical to read-only GC.
27
+ * Bash output is not always recoverable, so it carries its own safeguards: a
28
+ * command that looks side-effecting is never elided (re-running it, as the
29
+ * stub invites, could repeat a destructive action), and below 80% pressure
30
+ * only large outputs are elided.
25
31
  */
26
32
  import { resolve } from "node:path";
27
33
  /** Tool names whose result injects file contents into the transcript. */
28
34
  const READ_TOOLS = new Set(["read"]);
29
35
  /** Tool names that change a file, making a prior read of that path stale. */
30
36
  const MUTATE_TOOLS = new Set(["edit", "write"]);
37
+ /**
38
+ * Commands whose bash output must never be elided, because the stub invites
39
+ * the model to re-run the command and re-running could repeat a destructive or
40
+ * state-changing action. Tested against the whole command string, and
41
+ * deliberately over-broad: a false positive merely keeps an output (safe),
42
+ * while the common verbose *read* commands (cat/grep/ls/find/git log/diff,
43
+ * test runners) intentionally do NOT match, so they stay evictable.
44
+ */
45
+ const BASH_SIDE_EFFECT_PATTERN = /(\b(write|insert|delete|update|curl|wget|psql|mysql|sqlite3|migrate|drop|truncate|rm|rmdir|mv|cp|dd|kill|tee|chmod|chown|ln|mkdir|touch)\b|\bgit\s+(commit|push|reset|checkout|rebase|clean|apply|merge)\b|\bsed\b[^|]*-i|>>?)/i;
46
+ /** Below 80% pressure, only bash outputs larger than this (chars) are elided. */
47
+ const BASH_EVICTION_CHAR_THRESHOLD = 2000;
31
48
  function isAssistant(m) {
32
49
  return m.role === "assistant" && Array.isArray(m.content);
33
50
  }
@@ -44,12 +61,20 @@ export function evictSupersededReads(messages, options) {
44
61
  // display path (the original argument) for the stub text.
45
62
  const resolvedPathByCallId = new Map();
46
63
  const displayPathByResolved = new Map();
64
+ // Bash calls carry a `command`, not a `path`; capture it so the eviction
65
+ // pass can consult the side-effect guard by tool call id.
66
+ const bashCommandByCallId = new Map();
47
67
  for (const m of messages) {
48
68
  if (!isAssistant(m))
49
69
  continue;
50
70
  for (const block of m.content) {
51
71
  if (block.type !== "toolCall" || !block.id)
52
72
  continue;
73
+ if (block.name === "bash") {
74
+ const cmd = block.arguments?.command;
75
+ if (typeof cmd === "string" && cmd.length > 0)
76
+ bashCommandByCallId.set(block.id, cmd);
77
+ }
53
78
  const rawPath = block.arguments?.path;
54
79
  if (typeof rawPath !== "string" || rawPath.length === 0)
55
80
  continue;
@@ -80,29 +105,55 @@ export function evictSupersededReads(messages, options) {
80
105
  // Second pass: build the output, stubbing evicted reads. Keep every other
81
106
  // message by reference; clone only the ones we rewrite so the persisted
82
107
  // history (which may share these objects) is never mutated.
108
+ const pressure = options.budgetPressure ?? 0;
83
109
  let changed = false;
84
110
  const out = messages.map((m, i) => {
85
- if (!isToolResult(m) || m.isError || !READ_TOOLS.has(m.toolName))
86
- return m;
87
- const path = resolvedPathByCallId.get(m.toolCallId);
88
- if (!path)
89
- return m;
90
- const laterRead = (lastReadIndex.get(path) ?? -1) > i;
91
- const laterMutate = (lastMutateIndex.get(path) ?? -1) > i;
92
- if (!laterRead && !laterMutate)
111
+ if (!isToolResult(m) || m.isError)
93
112
  return m;
94
- changed = true;
95
- const display = displayPathByResolved.get(path) ?? path;
96
- const reason = laterMutate ? "the file was modified after this read" : "the file was read again later";
97
- return {
98
- ...m,
99
- content: [
100
- {
101
- type: "text",
102
- text: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,
103
- },
104
- ],
105
- };
113
+ // Superseded-read eviction — always on, pressure-independent.
114
+ if (READ_TOOLS.has(m.toolName)) {
115
+ const path = resolvedPathByCallId.get(m.toolCallId);
116
+ if (!path)
117
+ return m;
118
+ const laterRead = (lastReadIndex.get(path) ?? -1) > i;
119
+ const laterMutate = (lastMutateIndex.get(path) ?? -1) > i;
120
+ if (!laterRead && !laterMutate)
121
+ return m;
122
+ changed = true;
123
+ const display = displayPathByResolved.get(path) ?? path;
124
+ const reason = laterMutate ? "the file was modified after this read" : "the file was read again later";
125
+ return {
126
+ ...m,
127
+ content: [
128
+ {
129
+ type: "text",
130
+ text: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,
131
+ },
132
+ ],
133
+ };
134
+ }
135
+ // Bash-output eviction — only under token-budget pressure (>= 0.6).
136
+ if (pressure >= 0.6 && m.toolName === "bash") {
137
+ const cmd = bashCommandByCallId.get(m.toolCallId) ?? "";
138
+ if (BASH_SIDE_EFFECT_PATTERN.test(cmd))
139
+ return m;
140
+ const textLen = m.content.filter((c) => c.type === "text").reduce((sum, c) => sum + (c.text?.length ?? 0), 0);
141
+ // 60–79%: elide only large outputs. 80%+: elide unconditionally.
142
+ const shouldEvict = pressure >= 0.8 || textLen > BASH_EVICTION_CHAR_THRESHOLD;
143
+ if (shouldEvict) {
144
+ changed = true;
145
+ return {
146
+ ...m,
147
+ content: [
148
+ {
149
+ type: "text",
150
+ text: `[Bash output elided at ${Math.round(pressure * 100)}% token budget — re-run if needed.]`,
151
+ },
152
+ ],
153
+ };
154
+ }
155
+ }
156
+ return m;
106
157
  });
107
158
  return changed ? out : messages;
108
159
  }
@@ -1 +1 @@
1
- {"version":3,"file":"context-gc.js","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context garbage collection.\n *\n * The whole transcript is re-sent on every turn, so a tool result that has\n * become useless keeps costing tokens for the rest of the session. This pass\n * runs on the OUTGOING message copy (via the agent's `transformContext` hook),\n * never on the persisted session, and replaces provably-dead `read` results\n * with a short stub. Nothing is lost: the persisted history is untouched and\n * the file is re-readable on demand.\n *\n * Current rule — superseded reads only (the read-then-edit / re-read pattern,\n * which dominates coding sessions):\n *\n * A `read` result for a path P is stale once, later in the transcript, the\n * same path is edited/written (its on-disk content changed) or read again\n * (the newer read reflects newer state). The most recent read of each path is\n * always kept, and a read is only evicted when a *successful* later event\n * supersedes it — so a read whose edit failed (and which the model still needs\n * to retry) is never touched.\n *\n * Deliberately conservative: paths are matched after `path.resolve`, so two\n * different files never collide, and any ambiguity results in NOT evicting.\n * Bash-output eviction is intentionally left out for now — that output is not\n * always recoverable, so it needs its own safeguards.\n */\n\nimport { resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\n\n/** Tool names whose result injects file contents into the transcript. */\nconst READ_TOOLS = new Set([\"read\"]);\n/** Tool names that change a file, making a prior read of that path stale. */\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\nexport interface ContextGcOptions {\n\t/** Working directory used to resolve relative tool path arguments. */\n\tcwd: string;\n}\n\ninterface AssistantLike {\n\trole: \"assistant\";\n\tcontent: Array<{ type: string; id?: string; name?: string; arguments?: Record<string, unknown> }>;\n}\n\ninterface ToolResultLike {\n\trole: \"toolResult\";\n\ttoolCallId: string;\n\ttoolName: string;\n\tcontent: Array<{ type: string; text?: string }>;\n\tisError: boolean;\n}\n\nfunction isAssistant(m: AgentMessage): m is AgentMessage & AssistantLike {\n\treturn (m as { role?: string }).role === \"assistant\" && Array.isArray((m as AssistantLike).content);\n}\n\nfunction isToolResult(m: AgentMessage): m is AgentMessage & ToolResultLike {\n\treturn (m as { role?: string }).role === \"toolResult\";\n}\n\n/**\n * Return a message array with superseded `read` results stubbed out. Returns the\n * original array reference unchanged when there is nothing to evict, so a\n * no-op turn does not needlessly perturb the outgoing context.\n */\nexport function evictSupersededReads(messages: AgentMessage[], options: ContextGcOptions): AgentMessage[] {\n\t// Map each tool call id -> the resolved path it operated on, plus a friendly\n\t// display path (the original argument) for the stub text.\n\tconst resolvedPathByCallId = new Map<string, string>();\n\tconst displayPathByResolved = new Map<string, string>();\n\tfor (const m of messages) {\n\t\tif (!isAssistant(m)) continue;\n\t\tfor (const block of m.content) {\n\t\t\tif (block.type !== \"toolCall\" || !block.id) continue;\n\t\t\tconst rawPath = block.arguments?.path;\n\t\t\tif (typeof rawPath !== \"string\" || rawPath.length === 0) continue;\n\t\t\tconst resolved = resolve(options.cwd, rawPath);\n\t\t\tresolvedPathByCallId.set(block.id, resolved);\n\t\t\tif (!displayPathByResolved.has(resolved)) displayPathByResolved.set(resolved, rawPath);\n\t\t}\n\t}\n\n\t// First pass: per path, the index of the last read and the last successful\n\t// mutate. A read at index i is superseded when a later read (index > i) or a\n\t// later successful mutate (index > i) exists for the same path.\n\tconst lastReadIndex = new Map<string, number>();\n\tconst lastMutateIndex = new Map<string, number>();\n\tmessages.forEach((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return;\n\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\tif (!path) return;\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tlastReadIndex.set(path, i);\n\t\t} else if (MUTATE_TOOLS.has(m.toolName)) {\n\t\t\tlastMutateIndex.set(path, i);\n\t\t}\n\t});\n\n\t// Second pass: build the output, stubbing evicted reads. Keep every other\n\t// message by reference; clone only the ones we rewrite so the persisted\n\t// history (which may share these objects) is never mutated.\n\tlet changed = false;\n\tconst out = messages.map((m, i) => {\n\t\tif (!isToolResult(m) || m.isError || !READ_TOOLS.has(m.toolName)) return m;\n\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\tif (!path) return m;\n\t\tconst laterRead = (lastReadIndex.get(path) ?? -1) > i;\n\t\tconst laterMutate = (lastMutateIndex.get(path) ?? -1) > i;\n\t\tif (!laterRead && !laterMutate) return m;\n\t\tchanged = true;\n\t\tconst display = displayPathByResolved.get(path) ?? path;\n\t\tconst reason = laterMutate ? \"the file was modified after this read\" : \"the file was read again later\";\n\t\treturn {\n\t\t\t...m,\n\t\t\tcontent: [\n\t\t\t\t{\n\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\ttext: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t});\n\n\treturn changed ? out : messages;\n}\n"]}
1
+ {"version":3,"file":"context-gc.js","sourceRoot":"","sources":["../../src/core/context-gc.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context garbage collection.\n *\n * The whole transcript is re-sent on every turn, so a tool result that has\n * become useless keeps costing tokens for the rest of the session. This pass\n * runs on the OUTGOING message copy (via the agent's `transformContext` hook),\n * never on the persisted session, and replaces provably-dead `read` results\n * with a short stub. Nothing is lost: the persisted history is untouched and\n * the file is re-readable on demand.\n *\n * Current rule — superseded reads only (the read-then-edit / re-read pattern,\n * which dominates coding sessions):\n *\n * A `read` result for a path P is stale once, later in the transcript, the\n * same path is edited/written (its on-disk content changed) or read again\n * (the newer read reflects newer state). The most recent read of each path is\n * always kept, and a read is only evicted when a *successful* later event\n * supersedes it — so a read whose edit failed (and which the model still needs\n * to retry) is never touched.\n *\n * Deliberately conservative: paths are matched after `path.resolve`, so two\n * different files never collide, and any ambiguity results in NOT evicting.\n *\n * Bash-output eviction is additionally gated on token-budget pressure\n * (`options.budgetPressure`, the fraction of the model's context window in\n * use). At 0 pressure — the default — behaviour is identical to read-only GC.\n * Bash output is not always recoverable, so it carries its own safeguards: a\n * command that looks side-effecting is never elided (re-running it, as the\n * stub invites, could repeat a destructive action), and below 80% pressure\n * only large outputs are elided.\n */\n\nimport { resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\n\n/** Tool names whose result injects file contents into the transcript. */\nconst READ_TOOLS = new Set([\"read\"]);\n/** Tool names that change a file, making a prior read of that path stale. */\nconst MUTATE_TOOLS = new Set([\"edit\", \"write\"]);\n\n/**\n * Commands whose bash output must never be elided, because the stub invites\n * the model to re-run the command and re-running could repeat a destructive or\n * state-changing action. Tested against the whole command string, and\n * deliberately over-broad: a false positive merely keeps an output (safe),\n * while the common verbose *read* commands (cat/grep/ls/find/git log/diff,\n * test runners) intentionally do NOT match, so they stay evictable.\n */\nconst BASH_SIDE_EFFECT_PATTERN =\n\t/(\\b(write|insert|delete|update|curl|wget|psql|mysql|sqlite3|migrate|drop|truncate|rm|rmdir|mv|cp|dd|kill|tee|chmod|chown|ln|mkdir|touch)\\b|\\bgit\\s+(commit|push|reset|checkout|rebase|clean|apply|merge)\\b|\\bsed\\b[^|]*-i|>>?)/i;\n/** Below 80% pressure, only bash outputs larger than this (chars) are elided. */\nconst BASH_EVICTION_CHAR_THRESHOLD = 2000;\n\nexport interface ContextGcOptions {\n\t/** Working directory used to resolve relative tool path arguments. */\n\tcwd: string;\n\t/**\n\t * Token-budget pressure in [0, 1] — the fraction of the model's context\n\t * window currently in use. Absent or 0 reproduces read-only GC behaviour.\n\t * Bash-output eviction begins at 0.6 and becomes unconditional at 0.8.\n\t */\n\tbudgetPressure?: number;\n}\n\ninterface AssistantLike {\n\trole: \"assistant\";\n\tcontent: Array<{ type: string; id?: string; name?: string; arguments?: Record<string, unknown> }>;\n}\n\ninterface ToolResultLike {\n\trole: \"toolResult\";\n\ttoolCallId: string;\n\ttoolName: string;\n\tcontent: Array<{ type: string; text?: string }>;\n\tisError: boolean;\n}\n\nfunction isAssistant(m: AgentMessage): m is AgentMessage & AssistantLike {\n\treturn (m as { role?: string }).role === \"assistant\" && Array.isArray((m as AssistantLike).content);\n}\n\nfunction isToolResult(m: AgentMessage): m is AgentMessage & ToolResultLike {\n\treturn (m as { role?: string }).role === \"toolResult\";\n}\n\n/**\n * Return a message array with superseded `read` results stubbed out. Returns the\n * original array reference unchanged when there is nothing to evict, so a\n * no-op turn does not needlessly perturb the outgoing context.\n */\nexport function evictSupersededReads(messages: AgentMessage[], options: ContextGcOptions): AgentMessage[] {\n\t// Map each tool call id -> the resolved path it operated on, plus a friendly\n\t// display path (the original argument) for the stub text.\n\tconst resolvedPathByCallId = new Map<string, string>();\n\tconst displayPathByResolved = new Map<string, string>();\n\t// Bash calls carry a `command`, not a `path`; capture it so the eviction\n\t// pass can consult the side-effect guard by tool call id.\n\tconst bashCommandByCallId = new Map<string, string>();\n\tfor (const m of messages) {\n\t\tif (!isAssistant(m)) continue;\n\t\tfor (const block of m.content) {\n\t\t\tif (block.type !== \"toolCall\" || !block.id) continue;\n\t\t\tif (block.name === \"bash\") {\n\t\t\t\tconst cmd = block.arguments?.command;\n\t\t\t\tif (typeof cmd === \"string\" && cmd.length > 0) bashCommandByCallId.set(block.id, cmd);\n\t\t\t}\n\t\t\tconst rawPath = block.arguments?.path;\n\t\t\tif (typeof rawPath !== \"string\" || rawPath.length === 0) continue;\n\t\t\tconst resolved = resolve(options.cwd, rawPath);\n\t\t\tresolvedPathByCallId.set(block.id, resolved);\n\t\t\tif (!displayPathByResolved.has(resolved)) displayPathByResolved.set(resolved, rawPath);\n\t\t}\n\t}\n\n\t// First pass: per path, the index of the last read and the last successful\n\t// mutate. A read at index i is superseded when a later read (index > i) or a\n\t// later successful mutate (index > i) exists for the same path.\n\tconst lastReadIndex = new Map<string, number>();\n\tconst lastMutateIndex = new Map<string, number>();\n\tmessages.forEach((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return;\n\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\tif (!path) return;\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tlastReadIndex.set(path, i);\n\t\t} else if (MUTATE_TOOLS.has(m.toolName)) {\n\t\t\tlastMutateIndex.set(path, i);\n\t\t}\n\t});\n\n\t// Second pass: build the output, stubbing evicted reads. Keep every other\n\t// message by reference; clone only the ones we rewrite so the persisted\n\t// history (which may share these objects) is never mutated.\n\tconst pressure = options.budgetPressure ?? 0;\n\tlet changed = false;\n\tconst out = messages.map((m, i) => {\n\t\tif (!isToolResult(m) || m.isError) return m;\n\n\t\t// Superseded-read eviction — always on, pressure-independent.\n\t\tif (READ_TOOLS.has(m.toolName)) {\n\t\t\tconst path = resolvedPathByCallId.get(m.toolCallId);\n\t\t\tif (!path) return m;\n\t\t\tconst laterRead = (lastReadIndex.get(path) ?? -1) > i;\n\t\t\tconst laterMutate = (lastMutateIndex.get(path) ?? -1) > i;\n\t\t\tif (!laterRead && !laterMutate) return m;\n\t\t\tchanged = true;\n\t\t\tconst display = displayPathByResolved.get(path) ?? path;\n\t\t\tconst reason = laterMutate ? \"the file was modified after this read\" : \"the file was read again later\";\n\t\t\treturn {\n\t\t\t\t...m,\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\ttext: `[Superseded read of ${display} elided to save context — ${reason}. Re-read the file if you need its current contents.]`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\t\t}\n\n\t\t// Bash-output eviction — only under token-budget pressure (>= 0.6).\n\t\tif (pressure >= 0.6 && m.toolName === \"bash\") {\n\t\t\tconst cmd = bashCommandByCallId.get(m.toolCallId) ?? \"\";\n\t\t\tif (BASH_SIDE_EFFECT_PATTERN.test(cmd)) return m;\n\t\t\tconst textLen = m.content.filter((c) => c.type === \"text\").reduce((sum, c) => sum + (c.text?.length ?? 0), 0);\n\t\t\t// 60–79%: elide only large outputs. 80%+: elide unconditionally.\n\t\t\tconst shouldEvict = pressure >= 0.8 || textLen > BASH_EVICTION_CHAR_THRESHOLD;\n\t\t\tif (shouldEvict) {\n\t\t\t\tchanged = true;\n\t\t\t\treturn {\n\t\t\t\t\t...m,\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\" as const,\n\t\t\t\t\t\t\ttext: `[Bash output elided at ${Math.round(pressure * 100)}% token budget — re-run if needed.]`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\treturn m;\n\t});\n\n\treturn changed ? out : messages;\n}\n"]}
@@ -14,8 +14,7 @@
14
14
  */
15
15
  import type { MarketplacePlatform, PluginDraft } from "./formats/types.js";
16
16
  import { type NormalizedPlugin } from "./manifest.js";
17
- /** Default authoring targets: the two external vendor formats the model can author into. */
18
- export declare const DEFAULT_AUTHORING_PLATFORMS: MarketplacePlatform[];
17
+ export { DEFAULT_AUTHORING_PLATFORMS, resolveAuthoringPlatforms } from "./formats/platform-targets.js";
19
18
  export interface AllowlistClassification {
20
19
  /** read-only grants are as safe as a skill; mutating/exec/network/`*` grants need confirmation. */
21
20
  risk: "read-only" | "mutating";
@@ -1 +1 @@
1
- {"version":3,"file":"authoring.d.ts","sourceRoot":"","sources":["../../../../src/core/extensions/plugins/authoring.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAOH,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE3E,OAAO,EAAE,KAAK,gBAAgB,EAAkB,MAAM,eAAe,CAAC;AAEtE,4FAA4F;AAC5F,eAAO,MAAM,2BAA2B,EAAE,mBAAmB,EAAyB,CAAC;AAKvF,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 type { MarketplacePlatform, PluginDraft } from \"./formats/types.js\";\nimport { installedPluginsDir, sanitizeForDir } from \"./install.js\";\nimport { type NormalizedPlugin, parsePluginDir } from \"./manifest.js\";\n\n/** Default authoring targets: the two external vendor formats the model can author into. */\nexport const DEFAULT_AUTHORING_PLATFORMS: MarketplacePlatform[] = [\"claude\", \"github\"];\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 = platforms ?? draft.supportPlatform ?? DEFAULT_AUTHORING_PLATFORMS;\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;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"]}
@@ -17,10 +17,12 @@ import * as path from "node:path";
17
17
  import { CLAUDE_TOOL_ALIASES } from "../../agent-frontmatter.js";
18
18
  import { PLUGIN_SYSTEM_TOOL_NAMES } from "../../tools/plugin-tool-names.js";
19
19
  import { emitForPlatforms } from "./formats/index.js";
20
+ import { resolveAuthoringPlatforms } from "./formats/platform-targets.js";
20
21
  import { installedPluginsDir, sanitizeForDir } from "./install.js";
21
22
  import { parsePluginDir } from "./manifest.js";
22
- /** Default authoring targets: the two external vendor formats the model can author into. */
23
- export const DEFAULT_AUTHORING_PLATFORMS = ["claude", "github"];
23
+ // Re-exported so existing importers keep one vocabulary; the resolution chain
24
+ // (explicit session --support-platform → default) lives in platform-targets.
25
+ export { DEFAULT_AUTHORING_PLATFORMS, resolveAuthoringPlatforms } from "./formats/platform-targets.js";
24
26
  /** hoocode tool names that only read (no mutation, no exec). Grants limited to these are low-risk. */
25
27
  const READONLY_TOOLS = new Set(["read", "grep", "find", "ls", "webfetch", "websearch"]);
26
28
  /**
@@ -73,7 +75,7 @@ export function classifyAllowlist(tools) {
73
75
  * re-parsed plugin so callers can confirm the round-trip.
74
76
  */
75
77
  export function writePluginDraft(cwd, draft, platforms) {
76
- const targets = platforms ?? draft.supportPlatform ?? DEFAULT_AUTHORING_PLATFORMS;
78
+ const targets = resolveAuthoringPlatforms(platforms ?? draft.supportPlatform);
77
79
  const dest = path.join(installedPluginsDir(cwd), sanitizeForDir(draft.id));
78
80
  const files = emitForPlatforms({ ...draft, supportPlatform: targets }, targets);
79
81
  // Formats share the capability tree (only marker manifests differ), so
@@ -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 type { MarketplacePlatform, PluginDraft } from \"./formats/types.js\";\nimport { installedPluginsDir, sanitizeForDir } from \"./install.js\";\nimport { type NormalizedPlugin, parsePluginDir } from \"./manifest.js\";\n\n/** Default authoring targets: the two external vendor formats the model can author into. */\nexport const DEFAULT_AUTHORING_PLATFORMS: MarketplacePlatform[] = [\"claude\", \"github\"];\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 = platforms ?? draft.supportPlatform ?? DEFAULT_AUTHORING_PLATFORMS;\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, 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 +1 @@
1
- {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/agents.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,eAAO,MAAM,YAAY,0CAMvB,CAAC","sourcesContent":["/**\n * Native `.agents-plugin` format — hoocode's own layout and a strict superset of\n * the Claude Code format. Wins precedence when several formats coexist, and is\n * the only format that honors `providers`.\n */\n\nimport { createJsonManifestAdapter } from \"./jsonManifest.js\";\n\nexport const agentsFormat = createJsonManifestAdapter({\n\tid: \"agents\",\n\tmanifestDir: \".agents-plugin\",\n\tprecedence: 0,\n\tlabel: \"Native (.agents-plugin)\",\n\tsupportsProviders: true,\n});\n"]}
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/agents.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,eAAO,MAAM,YAAY,0CAOvB,CAAC","sourcesContent":["/**\n * Native `.agents-plugin` format — hoocode's own layout and a strict superset of\n * the Claude Code format. Wins precedence when several formats coexist, and is\n * the only format that honors `providers`.\n */\n\nimport { createJsonManifestAdapter } from \"./jsonManifest.js\";\n\nexport const agentsFormat = createJsonManifestAdapter({\n\tid: \"agents\",\n\tmanifestDir: \".agents-plugin\",\n\tworkspaceRoot: \".agents\",\n\tprecedence: 0,\n\tlabel: \"Native (.agents-plugin)\",\n\tsupportsProviders: true,\n});\n"]}
@@ -7,6 +7,7 @@ import { createJsonManifestAdapter } from "./jsonManifest.js";
7
7
  export const agentsFormat = createJsonManifestAdapter({
8
8
  id: "agents",
9
9
  manifestDir: ".agents-plugin",
10
+ workspaceRoot: ".agents",
10
11
  precedence: 0,
11
12
  label: "Native (.agents-plugin)",
12
13
  supportsProviders: true,
@@ -1 +1 @@
1
- {"version":3,"file":"agents.js","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/agents.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,MAAM,CAAC,MAAM,YAAY,GAAG,yBAAyB,CAAC;IACrD,EAAE,EAAE,QAAQ;IACZ,WAAW,EAAE,gBAAgB;IAC7B,UAAU,EAAE,CAAC;IACb,KAAK,EAAE,yBAAyB;IAChC,iBAAiB,EAAE,IAAI;CACvB,CAAC,CAAC","sourcesContent":["/**\n * Native `.agents-plugin` format — hoocode's own layout and a strict superset of\n * the Claude Code format. Wins precedence when several formats coexist, and is\n * the only format that honors `providers`.\n */\n\nimport { createJsonManifestAdapter } from \"./jsonManifest.js\";\n\nexport const agentsFormat = createJsonManifestAdapter({\n\tid: \"agents\",\n\tmanifestDir: \".agents-plugin\",\n\tprecedence: 0,\n\tlabel: \"Native (.agents-plugin)\",\n\tsupportsProviders: true,\n});\n"]}
1
+ {"version":3,"file":"agents.js","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/agents.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,MAAM,CAAC,MAAM,YAAY,GAAG,yBAAyB,CAAC;IACrD,EAAE,EAAE,QAAQ;IACZ,WAAW,EAAE,gBAAgB;IAC7B,aAAa,EAAE,SAAS;IACxB,UAAU,EAAE,CAAC;IACb,KAAK,EAAE,yBAAyB;IAChC,iBAAiB,EAAE,IAAI;CACvB,CAAC,CAAC","sourcesContent":["/**\n * Native `.agents-plugin` format — hoocode's own layout and a strict superset of\n * the Claude Code format. Wins precedence when several formats coexist, and is\n * the only format that honors `providers`.\n */\n\nimport { createJsonManifestAdapter } from \"./jsonManifest.js\";\n\nexport const agentsFormat = createJsonManifestAdapter({\n\tid: \"agents\",\n\tmanifestDir: \".agents-plugin\",\n\tworkspaceRoot: \".agents\",\n\tprecedence: 0,\n\tlabel: \"Native (.agents-plugin)\",\n\tsupportsProviders: true,\n});\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/claude.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,eAAO,MAAM,YAAY,0CAMvB,CAAC","sourcesContent":["/**\n * Claude Code `.claude-plugin` format. Same on-disk layout as the native format\n * (skills/, commands/, agents/, hooks/hooks.json, inline mcpServers) but under\n * the `.claude-plugin/` marker directory and without `providers` (native-only).\n *\n * This is one of the two vendor formats `ProposePlugin` can scaffold into; keep\n * anything Claude-specific here so it can track upstream changes in isolation.\n */\n\nimport { createJsonManifestAdapter } from \"./jsonManifest.js\";\n\nexport const claudeFormat = createJsonManifestAdapter({\n\tid: \"claude\",\n\tmanifestDir: \".claude-plugin\",\n\tprecedence: 1,\n\tlabel: \"Claude Code (.claude-plugin)\",\n\tsupportsProviders: false,\n});\n"]}
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/claude.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,eAAO,MAAM,YAAY,0CAOvB,CAAC","sourcesContent":["/**\n * Claude Code `.claude-plugin` format. Same on-disk layout as the native format\n * (skills/, commands/, agents/, hooks/hooks.json, inline mcpServers) but under\n * the `.claude-plugin/` marker directory and without `providers` (native-only).\n *\n * This is one of the two vendor formats `ProposePlugin` can scaffold into; keep\n * anything Claude-specific here so it can track upstream changes in isolation.\n */\n\nimport { createJsonManifestAdapter } from \"./jsonManifest.js\";\n\nexport const claudeFormat = createJsonManifestAdapter({\n\tid: \"claude\",\n\tmanifestDir: \".claude-plugin\",\n\tworkspaceRoot: \".claude\",\n\tprecedence: 1,\n\tlabel: \"Claude Code (.claude-plugin)\",\n\tsupportsProviders: false,\n});\n"]}
@@ -10,6 +10,7 @@ import { createJsonManifestAdapter } from "./jsonManifest.js";
10
10
  export const claudeFormat = createJsonManifestAdapter({
11
11
  id: "claude",
12
12
  manifestDir: ".claude-plugin",
13
+ workspaceRoot: ".claude",
13
14
  precedence: 1,
14
15
  label: "Claude Code (.claude-plugin)",
15
16
  supportsProviders: false,
@@ -1 +1 @@
1
- {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/claude.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,MAAM,CAAC,MAAM,YAAY,GAAG,yBAAyB,CAAC;IACrD,EAAE,EAAE,QAAQ;IACZ,WAAW,EAAE,gBAAgB;IAC7B,UAAU,EAAE,CAAC;IACb,KAAK,EAAE,8BAA8B;IACrC,iBAAiB,EAAE,KAAK;CACxB,CAAC,CAAC","sourcesContent":["/**\n * Claude Code `.claude-plugin` format. Same on-disk layout as the native format\n * (skills/, commands/, agents/, hooks/hooks.json, inline mcpServers) but under\n * the `.claude-plugin/` marker directory and without `providers` (native-only).\n *\n * This is one of the two vendor formats `ProposePlugin` can scaffold into; keep\n * anything Claude-specific here so it can track upstream changes in isolation.\n */\n\nimport { createJsonManifestAdapter } from \"./jsonManifest.js\";\n\nexport const claudeFormat = createJsonManifestAdapter({\n\tid: \"claude\",\n\tmanifestDir: \".claude-plugin\",\n\tprecedence: 1,\n\tlabel: \"Claude Code (.claude-plugin)\",\n\tsupportsProviders: false,\n});\n"]}
1
+ {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/claude.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAE9D,MAAM,CAAC,MAAM,YAAY,GAAG,yBAAyB,CAAC;IACrD,EAAE,EAAE,QAAQ;IACZ,WAAW,EAAE,gBAAgB;IAC7B,aAAa,EAAE,SAAS;IACxB,UAAU,EAAE,CAAC;IACb,KAAK,EAAE,8BAA8B;IACrC,iBAAiB,EAAE,KAAK;CACxB,CAAC,CAAC","sourcesContent":["/**\n * Claude Code `.claude-plugin` format. Same on-disk layout as the native format\n * (skills/, commands/, agents/, hooks/hooks.json, inline mcpServers) but under\n * the `.claude-plugin/` marker directory and without `providers` (native-only).\n *\n * This is one of the two vendor formats `ProposePlugin` can scaffold into; keep\n * anything Claude-specific here so it can track upstream changes in isolation.\n */\n\nimport { createJsonManifestAdapter } from \"./jsonManifest.js\";\n\nexport const claudeFormat = createJsonManifestAdapter({\n\tid: \"claude\",\n\tmanifestDir: \".claude-plugin\",\n\tworkspaceRoot: \".claude\",\n\tprecedence: 1,\n\tlabel: \"Claude Code (.claude-plugin)\",\n\tsupportsProviders: false,\n});\n"]}
@@ -1,18 +1,34 @@
1
1
  /**
2
- * GitHub Copilot `.github` format.
2
+ * GitHub Copilot format.
3
3
  *
4
- * The real-world convention (see github/copilot-plugins and the plugins it
5
- * indexes, e.g. microsoft/work-iq) mirrors the Claude Code bundle layout and
6
- * differs only in the marker/manifest location:
4
+ * hoocode's preferred Copilot home is the `.github/plugin/` marker directory
5
+ * the convention used by the real-world plugins indexed by
6
+ * github/copilot-plugins (e.g. microsoft/work-iq) and the documented location
7
+ * for GitHub-hosted marketplaces. The official Copilot CLI plugin reference
8
+ * (docs.github.com copilot/reference/copilot-cli-reference/cli-plugin-reference,
9
+ * "File locations"; verified 2026-07) accepts several locations for both files
10
+ * (`.plugin/`, plugin root, `.github/plugin/`, `.claude-plugin/`), so this
11
+ * adapter reads them all — `.github/plugin/` first, then root, then `.plugin/`,
12
+ * then the legacy hoocode layout — and writes `.github/plugin/`.
13
+ * (`.claude-plugin/` belongs to the Claude adapter, which wins precedence, so a
14
+ * `.claude-plugin`-only directory correctly parses as a Claude plugin; Copilot
15
+ * reads those natively anyway.) The capability tree matches the Claude layout:
7
16
  *
8
- * marker/manifest .github/plugin/plugin.json { name, version, description, skills?: "./skills/" }
9
- * marketplace .github/plugin/marketplace.json (also accepts .github/marketplace.json)
10
17
  * skills skills/<name>/SKILL.md (or the manifest's `skills` path override)
11
18
  * commands commands/<name>.md
12
- * subagents agents/<name>.md
13
- * hooks hooks/hooks.json ({ description?, hooks: { Event: [...] } })
19
+ * subagents agents/<name>.md (Copilot also emits/reads `.agent.md` names)
20
+ * hooks hooks.json or hooks/hooks.json ({ description?, hooks: { Event: [...] } })
14
21
  * MCP servers .mcp.json ({ mcpServers } | { servers })
15
22
  *
23
+ * Workspace-level (non-plugin) conventions, per the current GitHub Copilot
24
+ * customization docs (docs.github.com — agent skills, custom agents, prompt
25
+ * files; verified 2026-07):
26
+ *
27
+ * skills .github/skills/<name>/SKILL.md (agentskills.io open standard)
28
+ * custom agents .github/agents/<name>.agent.md (frontmatter: name, description,
29
+ * tools as a YAML list, model, ...)
30
+ * prompt files .github/prompts/<name>.prompt.md (VS Code / Copilot prompt files)
31
+ *
16
32
  * Earlier hoocode releases authored a different Copilot mapping
17
33
  * (`.github/copilot-plugin.json` + `.github/prompts/*.prompt.md` +
18
34
  * `.github/chatmodes/*.chatmode.md`); the reader still accepts that layout as a
@@ -1 +1 @@
1
- {"version":3,"file":"copilot.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/copilot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAkBH,OAAO,KAAK,EAA4B,mBAAmB,EAAE,MAAM,YAAY,CAAC;AA+BhF,eAAO,MAAM,aAAa,EAAE,mBAmG3B,CAAC","sourcesContent":["/**\n * GitHub Copilot `.github` format.\n *\n * The real-world convention (see github/copilot-plugins and the plugins it\n * indexes, e.g. microsoft/work-iq) mirrors the Claude Code bundle layout and\n * differs only in the marker/manifest location:\n *\n * marker/manifest .github/plugin/plugin.json { name, version, description, skills?: \"./skills/\" }\n * marketplace .github/plugin/marketplace.json (also accepts .github/marketplace.json)\n * skills skills/<name>/SKILL.md (or the manifest's `skills` path override)\n * commands commands/<name>.md\n * subagents agents/<name>.md\n * hooks hooks/hooks.json ({ description?, hooks: { Event: [...] } })\n * MCP servers .mcp.json ({ mcpServers } | { servers })\n *\n * Earlier hoocode releases authored a different Copilot mapping\n * (`.github/copilot-plugin.json` + `.github/prompts/*.prompt.md` +\n * `.github/chatmodes/*.chatmode.md`); the reader still accepts that layout as a\n * legacy fallback so previously authored plugins keep loading. Copilot\n * conventions move quickly; this file is the single place that encodes them,\n * so tracking an upstream change never reaches beyond this adapter.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { NormalizedPlugin } from \"../manifest.js\";\nimport {\n\tauthoredHooksToConfig,\n\tdirIfExists,\n\temitJson,\n\temitMarkdown,\n\tnormalizeHooks,\n\tnormalizeMcp,\n\tparseAuthor,\n\ttype RawManifest,\n\treadJson,\n\tresolveCapabilityDir,\n\tslug,\n} from \"./shared.js\";\nimport type { EmittedFile, PluginDraft, PluginFormatAdapter } from \"./types.js\";\n\nconst MARKER_DIR = \".github\";\n/** Real-world manifest location (github/copilot-plugins convention). */\nconst manifestRelPath = path.join(MARKER_DIR, \"plugin\", \"plugin.json\");\n/** Legacy hoocode-authored manifest location, still read for compatibility. */\nconst legacyManifestRelPath = path.join(MARKER_DIR, \"copilot-plugin.json\");\n\n// Legacy authored layout (read-only fallbacks).\nconst LEGACY_PROMPTS_DIR = path.join(MARKER_DIR, \"prompts\");\nconst LEGACY_CHATMODES_DIR = path.join(MARKER_DIR, \"chatmodes\");\nconst LEGACY_MCP_FILE = path.join(MARKER_DIR, \"mcp.json\");\nconst LEGACY_HOOKS_FILE = path.join(MARKER_DIR, \"hooks\", \"hooks.json\");\n\n/** Read legacy `.github/hooks/hooks.json` into the shared hooks event-map shape. */\nfunction readLegacyHooks(root: string): NormalizedPlugin[\"hooks\"] {\n\tconst file = path.join(root, LEGACY_HOOKS_FILE);\n\tif (!fs.existsSync(file)) return undefined;\n\tconst raw = readJson<{ hooks?: Record<string, unknown> } | Record<string, unknown>>(file);\n\tif (!raw || typeof raw !== \"object\") return undefined;\n\tconst config = (\"hooks\" in raw && raw.hooks ? raw.hooks : raw) as NormalizedPlugin[\"hooks\"];\n\treturn config && Object.keys(config).length > 0 ? config : undefined;\n}\n\nfunction manifestPathFor(root: string): string | undefined {\n\tfor (const rel of [manifestRelPath, legacyManifestRelPath]) {\n\t\tif (readJson(path.join(root, rel)) != null) return path.join(root, rel);\n\t}\n\treturn undefined;\n}\n\nexport const copilotFormat: PluginFormatAdapter = {\n\tid: \"copilot\",\n\tplatform: \"github\",\n\tprecedence: 2,\n\tlabel: \"GitHub Copilot (.github)\",\n\tmarketplaceFiles: [path.join(MARKER_DIR, \"plugin\", \"marketplace.json\"), path.join(MARKER_DIR, \"marketplace.json\")],\n\n\tdetectPlugin(root: string): boolean {\n\t\treturn manifestPathFor(root) !== undefined;\n\t},\n\n\tparsePlugin(root: string): NormalizedPlugin | null {\n\t\tconst manifestPath = manifestPathFor(root);\n\t\tif (!manifestPath) return null;\n\t\tconst raw = readJson<RawManifest>(manifestPath);\n\t\tif (!raw) return null;\n\n\t\tconst id = (raw.name ?? path.basename(root)).trim();\n\t\tif (!id) return null;\n\n\t\treturn {\n\t\t\tid,\n\t\t\tversion: raw.version,\n\t\t\tdescription: raw.description,\n\t\t\tauthor: parseAuthor(raw.author),\n\t\t\troot,\n\t\t\tmanifestPath,\n\t\t\tformat: \"copilot\",\n\t\t\t// Single-format view; the registry widens this to every format present.\n\t\t\tsupportPlatform: [\"github\"],\n\t\t\t// Claude-mirror layout (manifest overrides honored), with the legacy\n\t\t\t// prompts/chatmodes locations as read-only fallbacks.\n\t\t\tskillsDir: resolveCapabilityDir(root, raw.skills, \"skills\"),\n\t\t\tcommandsDir: resolveCapabilityDir(root, raw.commands, \"commands\") ?? dirIfExists(root, LEGACY_PROMPTS_DIR),\n\t\t\tagentsDir: resolveCapabilityDir(root, raw.agents, \"agents\") ?? dirIfExists(root, LEGACY_CHATMODES_DIR),\n\t\t\tthemesDir: resolveCapabilityDir(root, raw.themes, \"themes\"),\n\t\t\thooks: normalizeHooks(raw.hooks, root) ?? readLegacyHooks(root),\n\t\t\tmcpServers: normalizeMcp(raw.mcpServers, root) ?? normalizeMcp(undefined, root, LEGACY_MCP_FILE),\n\t\t\t// Providers are a native-only concept.\n\t\t\tproviders: undefined,\n\t\t};\n\t},\n\n\temit(draft: PluginDraft): EmittedFile[] {\n\t\t// Real-world convention: only the marker manifest is Copilot-specific; the\n\t\t// capability tree mirrors the Claude layout, so a plugin authored for both\n\t\t// platforms is one tree plus two marker manifests (exactly how plugins in\n\t\t// github/copilot-plugins ship).\n\t\tconst files: EmittedFile[] = [];\n\n\t\tfiles.push({\n\t\t\tpath: manifestRelPath,\n\t\t\tcontent: emitJson({\n\t\t\t\tname: draft.id,\n\t\t\t\t...(draft.version ? { version: draft.version } : {}),\n\t\t\t\t...(draft.description ? { description: draft.description } : {}),\n\t\t\t\t...(draft.author ? { author: draft.author } : {}),\n\t\t\t}),\n\t\t});\n\n\t\tfor (const s of draft.skills ?? []) {\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(\"skills\", slug(s.name), \"SKILL.md\"),\n\t\t\t\tcontent: emitMarkdown({ name: s.name, description: s.description }, s.body),\n\t\t\t});\n\t\t}\n\t\tfor (const c of draft.commands ?? []) {\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(\"commands\", `${slug(c.name)}.md`),\n\t\t\t\tcontent: emitMarkdown({ description: c.description }, c.body),\n\t\t\t});\n\t\t}\n\t\tfor (const a of draft.agents ?? []) {\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(\"agents\", `${slug(a.name)}.md`),\n\t\t\t\tcontent: emitMarkdown({ name: a.name, description: a.description, tools: a.tools, model: a.model }, a.body),\n\t\t\t});\n\t\t}\n\t\tif (draft.mcpServers?.length) {\n\t\t\tfiles.push({\n\t\t\t\tpath: \".mcp.json\",\n\t\t\t\tcontent: emitJson({\n\t\t\t\t\tmcpServers: Object.fromEntries(\n\t\t\t\t\t\tdraft.mcpServers.map((s) => [\n\t\t\t\t\t\t\ts.name,\n\t\t\t\t\t\t\t{ command: s.command, ...(s.args ? { args: s.args } : {}), ...(s.env ? { env: s.env } : {}) },\n\t\t\t\t\t\t]),\n\t\t\t\t\t),\n\t\t\t\t}),\n\t\t\t});\n\t\t}\n\t\tif (draft.hooks?.length) {\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(\"hooks\", \"hooks.json\"),\n\t\t\t\tcontent: emitJson({ hooks: authoredHooksToConfig(draft.hooks) }),\n\t\t\t});\n\t\t}\n\t\treturn files;\n\t},\n};\n"]}
1
+ {"version":3,"file":"copilot.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/plugins/formats/copilot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAmBH,OAAO,KAAK,EAA4B,mBAAmB,EAAE,MAAM,YAAY,CAAC;AA2ChF,eAAO,MAAM,aAAa,EAAE,mBAwI3B,CAAC","sourcesContent":["/**\n * GitHub Copilot format.\n *\n * hoocode's preferred Copilot home is the `.github/plugin/` marker directory —\n * the convention used by the real-world plugins indexed by\n * github/copilot-plugins (e.g. microsoft/work-iq) and the documented location\n * for GitHub-hosted marketplaces. The official Copilot CLI plugin reference\n * (docs.github.com copilot/reference/copilot-cli-reference/cli-plugin-reference,\n * \"File locations\"; verified 2026-07) accepts several locations for both files\n * (`.plugin/`, plugin root, `.github/plugin/`, `.claude-plugin/`), so this\n * adapter reads them all — `.github/plugin/` first, then root, then `.plugin/`,\n * then the legacy hoocode layout — and writes `.github/plugin/`.\n * (`.claude-plugin/` belongs to the Claude adapter, which wins precedence, so a\n * `.claude-plugin`-only directory correctly parses as a Claude plugin; Copilot\n * reads those natively anyway.) The capability tree matches the Claude layout:\n *\n * skills skills/<name>/SKILL.md (or the manifest's `skills` path override)\n * commands commands/<name>.md\n * subagents agents/<name>.md (Copilot also emits/reads `.agent.md` names)\n * hooks hooks.json or hooks/hooks.json ({ description?, hooks: { Event: [...] } })\n * MCP servers .mcp.json ({ mcpServers } | { servers })\n *\n * Workspace-level (non-plugin) conventions, per the current GitHub Copilot\n * customization docs (docs.github.com — agent skills, custom agents, prompt\n * files; verified 2026-07):\n *\n * skills .github/skills/<name>/SKILL.md (agentskills.io open standard)\n * custom agents .github/agents/<name>.agent.md (frontmatter: name, description,\n * tools as a YAML list, model, ...)\n * prompt files .github/prompts/<name>.prompt.md (VS Code / Copilot prompt files)\n *\n * Earlier hoocode releases authored a different Copilot mapping\n * (`.github/copilot-plugin.json` + `.github/prompts/*.prompt.md` +\n * `.github/chatmodes/*.chatmode.md`); the reader still accepts that layout as a\n * legacy fallback so previously authored plugins keep loading. Copilot\n * conventions move quickly; this file is the single place that encodes them,\n * so tracking an upstream change never reaches beyond this adapter.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport type { NormalizedPlugin } from \"../manifest.js\";\nimport {\n\tauthoredHooksToConfig,\n\tdirIfExists,\n\temitJson,\n\temitMarkdown,\n\tnormalizeHooks,\n\tnormalizeMcp,\n\tparseAuthor,\n\ttype RawManifest,\n\treadJson,\n\tresolveCapabilityDir,\n\tslug,\n\ttoolsYamlList,\n} from \"./shared.js\";\nimport type { EmittedFile, PluginDraft, PluginFormatAdapter } from \"./types.js\";\n\nconst MARKER_DIR = \".github\";\n/**\n * Manifest probe order: `.github/plugin/plugin.json` first — hoocode's\n * preferred Copilot home, matching the real-world plugins indexed by\n * github/copilot-plugins (e.g. microsoft/work-iq) — then the Copilot CLI\n * reference's other locations (root `plugin.json`, `.plugin/plugin.json`) and\n * hoocode's legacy `.github/copilot-plugin.json`. (`.claude-plugin/plugin.json`,\n * also probed by the CLI, belongs to the Claude adapter.)\n */\nconst MANIFEST_REL_PATHS = [\n\tpath.join(MARKER_DIR, \"plugin\", \"plugin.json\"),\n\t\"plugin.json\",\n\tpath.join(\".plugin\", \"plugin.json\"),\n\tpath.join(MARKER_DIR, \"copilot-plugin.json\"),\n] as const;\n/** Authored manifest location: the preferred `.github/plugin/` home. */\nconst emitManifestRelPath = path.join(MARKER_DIR, \"plugin\", \"plugin.json\");\n\n// Legacy authored layout (read-only fallbacks).\nconst LEGACY_PROMPTS_DIR = path.join(MARKER_DIR, \"prompts\");\nconst LEGACY_CHATMODES_DIR = path.join(MARKER_DIR, \"chatmodes\");\nconst LEGACY_MCP_FILE = path.join(MARKER_DIR, \"mcp.json\");\nconst LEGACY_HOOKS_FILE = path.join(MARKER_DIR, \"hooks\", \"hooks.json\");\n\n/** Read a hooks JSON file (either `{ hooks: {...} }` or a bare event map). */\nfunction readHooksFile(root: string, rel: string): NormalizedPlugin[\"hooks\"] {\n\tconst file = path.join(root, rel);\n\tif (!fs.existsSync(file)) return undefined;\n\tconst raw = readJson<{ hooks?: Record<string, unknown> } | Record<string, unknown>>(file);\n\tif (!raw || typeof raw !== \"object\") return undefined;\n\tconst config = (\"hooks\" in raw && raw.hooks ? raw.hooks : raw) as NormalizedPlugin[\"hooks\"];\n\treturn config && Object.keys(config).length > 0 ? config : undefined;\n}\n\nfunction manifestPathFor(root: string): string | undefined {\n\tfor (const rel of MANIFEST_REL_PATHS) {\n\t\tif (readJson(path.join(root, rel)) != null) return path.join(root, rel);\n\t}\n\treturn undefined;\n}\n\nexport const copilotFormat: PluginFormatAdapter = {\n\tid: \"copilot\",\n\tplatform: \"github\",\n\tprecedence: 2,\n\tlabel: \"GitHub Copilot (.github)\",\n\t// .github/plugin/ first (hoocode's preferred Copilot home and the documented\n\t// location for GitHub-hosted marketplaces), then the legacy .github/ spot,\n\t// then the Copilot CLI's other probe locations (root, .plugin/).\n\tmarketplaceFiles: [\n\t\tpath.join(MARKER_DIR, \"plugin\", \"marketplace.json\"),\n\t\tpath.join(MARKER_DIR, \"marketplace.json\"),\n\t\t\"marketplace.json\",\n\t\tpath.join(\".plugin\", \"marketplace.json\"),\n\t],\n\n\tworkspace: {\n\t\troot: MARKER_DIR,\n\t\t// Copilot reads repo skills from .github/skills (also .claude/skills and\n\t\t// .agents/skills, which the sibling adapters cover).\n\t\temitSkill: (s) => ({\n\t\t\tpath: path.join(MARKER_DIR, \"skills\", slug(s.name), \"SKILL.md\"),\n\t\t\tcontent: emitMarkdown({ name: s.name, description: s.description }, s.body),\n\t\t}),\n\t\t// Custom agents take `tools` as a YAML list (not the Claude comma string).\n\t\temitAgent: (a) => ({\n\t\t\tpath: path.join(MARKER_DIR, \"agents\", `${slug(a.name)}.agent.md`),\n\t\t\tcontent: emitMarkdown(\n\t\t\t\t{ name: a.name, description: a.description, tools: toolsYamlList(a.tools), model: a.model },\n\t\t\t\ta.body,\n\t\t\t),\n\t\t}),\n\t\t// The closest Copilot equivalent of a slash command is a prompt file.\n\t\temitCommand: (c) => ({\n\t\t\tpath: path.join(MARKER_DIR, \"prompts\", `${slug(c.name)}.prompt.md`),\n\t\t\tcontent: emitMarkdown({ description: c.description }, c.body),\n\t\t}),\n\t},\n\n\tdetectPlugin(root: string): boolean {\n\t\treturn manifestPathFor(root) !== undefined;\n\t},\n\n\tparsePlugin(root: string): NormalizedPlugin | null {\n\t\tconst manifestPath = manifestPathFor(root);\n\t\tif (!manifestPath) return null;\n\t\tconst raw = readJson<RawManifest>(manifestPath);\n\t\tif (!raw) return null;\n\n\t\tconst id = (raw.name ?? path.basename(root)).trim();\n\t\tif (!id) return null;\n\n\t\treturn {\n\t\t\tid,\n\t\t\tversion: raw.version,\n\t\t\tdescription: raw.description,\n\t\t\tauthor: parseAuthor(raw.author),\n\t\t\troot,\n\t\t\tmanifestPath,\n\t\t\tformat: \"copilot\",\n\t\t\t// Single-format view; the registry widens this to every format present.\n\t\t\tsupportPlatform: [\"github\"],\n\t\t\t// Claude-mirror layout (manifest overrides honored), with the legacy\n\t\t\t// prompts/chatmodes locations as read-only fallbacks.\n\t\t\tskillsDir: resolveCapabilityDir(root, raw.skills, \"skills\"),\n\t\t\tcommandsDir: resolveCapabilityDir(root, raw.commands, \"commands\") ?? dirIfExists(root, LEGACY_PROMPTS_DIR),\n\t\t\tagentsDir: resolveCapabilityDir(root, raw.agents, \"agents\") ?? dirIfExists(root, LEGACY_CHATMODES_DIR),\n\t\t\tthemesDir: resolveCapabilityDir(root, raw.themes, \"themes\"),\n\t\t\t// Copilot CLI plugins put hooks config at root `hooks.json`; normalizeHooks\n\t\t\t// covers the Claude-mirror `hooks/hooks.json`, then the legacy location.\n\t\t\thooks:\n\t\t\t\tnormalizeHooks(raw.hooks, root) ??\n\t\t\t\treadHooksFile(root, \"hooks.json\") ??\n\t\t\t\treadHooksFile(root, LEGACY_HOOKS_FILE),\n\t\t\tmcpServers: normalizeMcp(raw.mcpServers, root) ?? normalizeMcp(undefined, root, LEGACY_MCP_FILE),\n\t\t\t// Providers are a native-only concept.\n\t\t\tproviders: undefined,\n\t\t};\n\t},\n\n\temit(draft: PluginDraft): EmittedFile[] {\n\t\t// Only the manifest is Copilot-specific — the canonical location is root\n\t\t// `plugin.json` (Copilot CLI spec) — and the capability tree mirrors the\n\t\t// Claude layout, so a plugin authored for both platforms is one tree with a\n\t\t// root manifest for Copilot and a .claude-plugin/ manifest for Claude.\n\t\tconst files: EmittedFile[] = [];\n\n\t\tfiles.push({\n\t\t\tpath: emitManifestRelPath,\n\t\t\tcontent: emitJson({\n\t\t\t\tname: draft.id,\n\t\t\t\t...(draft.version ? { version: draft.version } : {}),\n\t\t\t\t...(draft.description ? { description: draft.description } : {}),\n\t\t\t\t// Spec: `author` is an object with a required `name`.\n\t\t\t\t...(draft.author ? { author: { name: draft.author } } : {}),\n\t\t\t}),\n\t\t});\n\n\t\tfor (const s of draft.skills ?? []) {\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(\"skills\", slug(s.name), \"SKILL.md\"),\n\t\t\t\tcontent: emitMarkdown({ name: s.name, description: s.description }, s.body),\n\t\t\t});\n\t\t}\n\t\tfor (const c of draft.commands ?? []) {\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(\"commands\", `${slug(c.name)}.md`),\n\t\t\t\tcontent: emitMarkdown({ description: c.description }, c.body),\n\t\t\t});\n\t\t}\n\t\tfor (const a of draft.agents ?? []) {\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(\"agents\", `${slug(a.name)}.md`),\n\t\t\t\tcontent: emitMarkdown({ name: a.name, description: a.description, tools: a.tools, model: a.model }, a.body),\n\t\t\t});\n\t\t}\n\t\tif (draft.mcpServers?.length) {\n\t\t\tfiles.push({\n\t\t\t\tpath: \".mcp.json\",\n\t\t\t\tcontent: emitJson({\n\t\t\t\t\tmcpServers: Object.fromEntries(\n\t\t\t\t\t\tdraft.mcpServers.map((s) => [\n\t\t\t\t\t\t\ts.name,\n\t\t\t\t\t\t\t{ command: s.command, ...(s.args ? { args: s.args } : {}), ...(s.env ? { env: s.env } : {}) },\n\t\t\t\t\t\t]),\n\t\t\t\t\t),\n\t\t\t\t}),\n\t\t\t});\n\t\t}\n\t\tif (draft.hooks?.length) {\n\t\t\tfiles.push({\n\t\t\t\tpath: path.join(\"hooks\", \"hooks.json\"),\n\t\t\t\tcontent: emitJson({ hooks: authoredHooksToConfig(draft.hooks) }),\n\t\t\t});\n\t\t}\n\t\treturn files;\n\t},\n};\n"]}