@arnilo/prism 0.0.1 → 0.0.2

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 (120) hide show
  1. package/CHANGELOG.md +4 -2
  2. package/README.md +17 -7
  3. package/dist/agent-definitions.d.ts +12 -0
  4. package/dist/agent-definitions.js +131 -0
  5. package/dist/agent-loops.d.ts +14 -0
  6. package/dist/agent-loops.js +161 -0
  7. package/dist/agents.js +263 -76
  8. package/dist/cache-helpers.d.ts +28 -0
  9. package/dist/cache-helpers.js +73 -0
  10. package/dist/cli-runner.d.ts +38 -2
  11. package/dist/cli-runner.js +167 -5
  12. package/dist/compaction.js +2 -0
  13. package/dist/config.js +47 -12
  14. package/dist/contracts.d.ts +581 -6
  15. package/dist/contracts.js +41 -1
  16. package/dist/contribution-parsing.d.ts +19 -0
  17. package/dist/contribution-parsing.js +124 -0
  18. package/dist/contributions.d.ts +13 -3
  19. package/dist/contributions.js +96 -20
  20. package/dist/extensions.js +3 -0
  21. package/dist/index.d.ts +19 -9
  22. package/dist/index.js +10 -4
  23. package/dist/input.d.ts +7 -1
  24. package/dist/input.js +52 -11
  25. package/dist/instruction-injection.d.ts +28 -0
  26. package/dist/instruction-injection.js +55 -0
  27. package/dist/manifests.d.ts +1 -1
  28. package/dist/manifests.js +3 -3
  29. package/dist/models.d.ts +4 -1
  30. package/dist/models.js +5 -2
  31. package/dist/node/agent-definitions.d.ts +98 -0
  32. package/dist/node/agent-definitions.js +389 -0
  33. package/dist/node/contribution-discovery.d.ts +17 -0
  34. package/dist/node/contribution-discovery.js +163 -0
  35. package/dist/node/instruction-injectors.d.ts +32 -0
  36. package/dist/node/instruction-injectors.js +72 -0
  37. package/dist/node/session-store-jsonl.d.ts +1 -1
  38. package/dist/node/session-store-jsonl.js +42 -4
  39. package/dist/node/system-project-prompts.d.ts +30 -0
  40. package/dist/node/system-project-prompts.js +53 -0
  41. package/dist/provider-events.d.ts +3 -1
  42. package/dist/provider-events.js +34 -0
  43. package/dist/provider-request-policy.js +15 -1
  44. package/dist/providers/openai-compatible.js +1 -1
  45. package/dist/providers.d.ts +6 -2
  46. package/dist/providers.js +15 -1
  47. package/dist/redaction.d.ts +2 -1
  48. package/dist/redaction.js +3 -0
  49. package/dist/registry-options.d.ts +5 -0
  50. package/dist/registry-options.js +5 -0
  51. package/dist/rpc.d.ts +6 -2
  52. package/dist/rpc.js +71 -13
  53. package/dist/session-stores.d.ts +3 -1
  54. package/dist/session-stores.js +67 -6
  55. package/dist/skills.d.ts +4 -1
  56. package/dist/skills.js +3 -1
  57. package/dist/system-prompts.js +6 -2
  58. package/dist/testing/compaction-conformance.d.ts +17 -0
  59. package/dist/testing/compaction-conformance.js +61 -0
  60. package/dist/testing/extension-conformance.d.ts +26 -0
  61. package/dist/testing/extension-conformance.js +55 -0
  62. package/dist/testing/provider-conformance.d.ts +7 -0
  63. package/dist/testing/provider-conformance.js +18 -31
  64. package/dist/testing/session-store-conformance.d.ts +20 -0
  65. package/dist/testing/session-store-conformance.js +92 -0
  66. package/dist/testing/tool-conformance.d.ts +39 -0
  67. package/dist/testing/tool-conformance.js +79 -0
  68. package/dist/tools.d.ts +7 -2
  69. package/dist/tools.js +50 -13
  70. package/docs/agent-definitions.md +251 -0
  71. package/docs/agent-events.md +199 -0
  72. package/docs/agent-loops.md +217 -0
  73. package/docs/agent-session-runtime.md +20 -8
  74. package/docs/cli-rpc.md +39 -4
  75. package/docs/compaction-and-retry.md +2 -2
  76. package/docs/compaction-conformance.md +76 -0
  77. package/docs/compaction-llm.md +6 -3
  78. package/docs/compaction-observational-memory.md +4 -4
  79. package/docs/configuration-and-manifests.md +6 -1
  80. package/docs/context-and-skills.md +79 -6
  81. package/docs/contribution-discovery.md +149 -0
  82. package/docs/contribution-registries.md +9 -6
  83. package/docs/credentials-and-redaction.md +2 -0
  84. package/docs/customization.md +191 -0
  85. package/docs/database-persistence.md +407 -0
  86. package/docs/extension-authoring.md +193 -0
  87. package/docs/extension-conformance.md +80 -0
  88. package/docs/extensions.md +6 -0
  89. package/docs/host-security.md +141 -0
  90. package/docs/index.md +40 -19
  91. package/docs/input-and-prompt-assembly.md +19 -3
  92. package/docs/instruction-injection.md +183 -0
  93. package/docs/migration.md +201 -0
  94. package/docs/model-registry.md +122 -0
  95. package/docs/node-jsonl-session-store.md +5 -4
  96. package/docs/performance.md +127 -0
  97. package/docs/provider-caching.md +206 -0
  98. package/docs/provider-conformance.md +32 -5
  99. package/docs/provider-layer.md +51 -11
  100. package/docs/provider-packages.md +65 -5
  101. package/docs/provider-request-policies.md +113 -0
  102. package/docs/providers/kimi.md +22 -0
  103. package/docs/providers/neuralwatt.md +388 -0
  104. package/docs/providers/openai-compatible.md +1 -0
  105. package/docs/providers/openai.md +21 -0
  106. package/docs/providers/opencode-go.md +31 -3
  107. package/docs/providers/openrouter.md +29 -0
  108. package/docs/providers/zai.md +17 -0
  109. package/docs/public-contracts.md +87 -12
  110. package/docs/release-and-install.md +76 -26
  111. package/docs/runs-and-usage.md +236 -0
  112. package/docs/session-store-conformance.md +78 -0
  113. package/docs/session-stores-and-branching.md +10 -6
  114. package/docs/session-stores.md +126 -0
  115. package/docs/settings-auth-trust-security.md +18 -4
  116. package/docs/structured-output.md +247 -0
  117. package/docs/system-prompts.md +104 -2
  118. package/docs/tool-conformance.md +87 -0
  119. package/docs/tools.md +64 -8
  120. package/package.json +35 -2
@@ -1,5 +1,6 @@
1
1
  import type { Readable, Writable } from "node:stream";
2
- import type { AgentSession } from "./contracts.js";
2
+ import type { AgentSession, ContributionFileKind, InstructionInjector, Skill, SystemPromptContribution } from "./contracts.js";
3
+ import type { AgentBundle } from "./node/agent-definitions.js";
3
4
  import { type RpcSessionFactory } from "./rpc.js";
4
5
  export type CliMode = "print" | "json" | "rpc";
5
6
  export interface CliOptions {
@@ -17,6 +18,36 @@ export interface CliOptions {
17
18
  readonly compact?: number;
18
19
  readonly maxToolRounds?: number;
19
20
  readonly help: boolean;
21
+ readonly discover: boolean;
22
+ readonly discoverKinds: readonly ContributionFileKind[];
23
+ readonly noDiscovery: boolean;
24
+ /** Parsed flag: `--agents-config <path>` points at the app config root holding
25
+ * `agents/<name>/AGENT.md` bundles. Opt-in; default runs never scan it. */
26
+ readonly agentsConfig?: string;
27
+ /** Runtime-populated (not a parsed flag): agent bundles discovered by `--agents-config`
28
+ * via {@link discoverAgentBundles}. Host-owned resolution. */
29
+ readonly discoveredAgents: readonly AgentBundle[];
30
+ /** Runtime-populated (not a parsed flag): skills discovered by `--discover`,
31
+ * threaded into `createSession` so the bootstrap can build a SkillRegistry. */
32
+ readonly discoveredSkills: readonly Skill[];
33
+ /** Runtime-populated (not a parsed flag): instruction injectors discovered by
34
+ * `--discover`, selectable by name via `--instruction` (Task 8). */
35
+ readonly discoveredInjectors: readonly InstructionInjector[];
36
+ /** Parsed flag: `--instruction <name>` (repeatable). `false` disables injectors. */
37
+ readonly instructions: readonly string[];
38
+ /** Parsed flag: `--injector-file <path>` (repeatable); markdown → static injector. */
39
+ readonly injectorFiles: readonly string[];
40
+ /** Runtime-populated: `--instruction`/`--injector-file` resolved to live injectors. */
41
+ readonly resolvedInstructionInjectors: readonly InstructionInjector[];
42
+ /** Parsed flag: `--no-agents-md` / `--no-system-md` skip the corresponding auto-load. */
43
+ readonly noAgentsMd: boolean;
44
+ readonly noSystemMd: boolean;
45
+ /** Parsed flag: `--agents-md-file <path>` / `--system-md-file <path>` override read paths. */
46
+ readonly agentsMdFile?: string;
47
+ readonly systemMdFile?: string;
48
+ /** Runtime-populated: AGENTS.md/SYSTEM.md layers loaded from disk (print/json modes only;
49
+ * RPC is host-owned). Threaded into `AgentConfig.systemPrompt` by `defaultCreateSession`. */
50
+ readonly systemPromptLayers: readonly SystemPromptContribution[];
20
51
  }
21
52
  export interface CliRuntime {
22
53
  readonly stdin: Readable;
@@ -24,8 +55,13 @@ export interface CliRuntime {
24
55
  readonly stderr: Writable;
25
56
  readonly createSession?: (options: CliOptions) => AgentSession;
26
57
  readonly commands?: RpcSessionFactory["commands"];
58
+ /** Workspace root for `--discover`. Defaults to `process.cwd()`. */
59
+ readonly workspaceRoot?: string;
60
+ /** Optional global root for SYSTEM.md auto-load (host-controlled). No default;
61
+ * the CLI does not auto-touch the user's home directory. */
62
+ readonly globalRoot?: string;
27
63
  }
28
- export declare const usage = "Usage: prism [--mode print|json|rpc] [-p prompt] [options]\n\nOptions:\n -p, --prompt <text> Prompt to run in print/json mode\n --provider <name> Explicit provider id (mock is built in for smoke tests)\n --model <name> Explicit model name\n --session <id> Session id\n --config <path> Explicit config path (recorded, not auto-loaded)\n --resource <uri> Explicit resource URI (recorded, not auto-loaded)\n --extension <name> Explicit extension name (recorded, not auto-loaded)\n --tool <name> Explicit tool name (recorded, not auto-enabled)\n --system <text> System instructions\n --context <text> Context text\n --compact <entries> Auto-compaction threshold\n --max-tool-rounds <n> Maximum tool rounds\n -h, --help Show this help\n";
64
+ export declare const usage = "Usage: prism [--mode print|json|rpc] [-p prompt] [options]\n\nOptions:\n -p, --prompt <text> Prompt to run in print/json mode\n --provider <name> Explicit provider id (mock is built in for smoke tests)\n --model <name> Explicit model name\n --session <id> Session id\n --config <path> Explicit config path (recorded, not auto-loaded)\n --resource <uri> Explicit resource URI (recorded, not auto-loaded)\n --extension <name> Explicit extension name (recorded, not auto-loaded)\n --tool <name> Explicit tool name (recorded, not auto-enabled)\n --system <text> System instructions\n --context <text> Context text\n --compact <entries> Auto-compaction threshold\n --max-tool-rounds <n> Maximum tool rounds\n --discover Enable workspace contribution discovery (opt-in)\n --discover-kinds <csv> Kinds to discover (default: skill; skill,tool,context,instructions)\n --no-discovery Disable discovery even if --discover is set\n --agents-config <path> App config root holding agents/<name>/AGENT.md bundles (opt-in)\n --no-agents-md Skip auto-loading <workspaceRoot>/AGENTS.md\n --no-system-md Skip auto-loading the global SYSTEM.md layer\n --agents-md-file <path> Read AGENTS.md from <path> instead (trust-gated, source: app)\n --system-md-file <path> Read SYSTEM.md from <path> instead (source: user)\n -h, --help Show this help\n";
29
65
  export declare function parseCliArgs(argv: readonly string[]): CliOptions;
30
66
  export declare function runCli(argv: readonly string[], runtime: CliRuntime): Promise<number>;
31
67
  export declare function runPromptMode(session: AgentSession, options: CliOptions, stdout: Writable, mode: "print" | "json"): Promise<void>;
@@ -1,4 +1,14 @@
1
- import { createAgent, createMockProvider, providerDone, providerTextDelta } from "./index.js";
1
+ import process from "node:process";
2
+ import { createAgent, createMockProvider, providerDone, providerTextDelta, resolveInstructionInjectors, createContributionRegistry } from "./index.js";
3
+ import { readFile } from "node:fs/promises";
4
+ import { basename, dirname } from "node:path";
5
+ import { createContributionRegistries, registerDiscoveredContributions } from "./contributions.js";
6
+ import { createSkillRegistry } from "./skills.js";
7
+ import { discoverContributions } from "./node/contribution-discovery.js";
8
+ import { discoverAgentBundles } from "./node/agent-definitions.js";
9
+ import { registerDiscoveredInstructionInjectors } from "./node/instruction-injectors.js";
10
+ import { loadSystemPromptFiles } from "./node/system-project-prompts.js";
11
+ import { createPathTrustPolicy } from "./node/trust.js";
2
12
  import { runRpcServer } from "./rpc.js";
3
13
  export const usage = `Usage: prism [--mode print|json|rpc] [-p prompt] [options]
4
14
 
@@ -15,9 +25,19 @@ Options:
15
25
  --context <text> Context text
16
26
  --compact <entries> Auto-compaction threshold
17
27
  --max-tool-rounds <n> Maximum tool rounds
28
+ --discover Enable workspace contribution discovery (opt-in)
29
+ --discover-kinds <csv> Kinds to discover (default: skill; skill,tool,context,instructions)
30
+ --no-discovery Disable discovery even if --discover is set
31
+ --agents-config <path> App config root holding agents/<name>/AGENT.md bundles (opt-in)
32
+ --no-agents-md Skip auto-loading <workspaceRoot>/AGENTS.md
33
+ --no-system-md Skip auto-loading the global SYSTEM.md layer
34
+ --agents-md-file <path> Read AGENTS.md from <path> instead (trust-gated, source: app)
35
+ --system-md-file <path> Read SYSTEM.md from <path> instead (source: user)
18
36
  -h, --help Show this help
19
37
  `;
20
- const valueFlags = new Set(["--prompt", "--mode", "--provider", "--model", "--session", "--config", "--resource", "--extension", "--tool", "--system", "--context", "--compact", "--max-tool-rounds"]);
38
+ const valueFlags = new Set(["--prompt", "--mode", "--provider", "--model", "--session", "--config", "--resource", "--extension", "--tool", "--system", "--context", "--compact", "--max-tool-rounds", "--discover-kinds", "--instruction", "--injector-file", "--agents-md-file", "--system-md-file", "--agents-config"]);
39
+ const boolFlags = new Set(["--discover", "--no-discovery", "--no-agents-md", "--no-system-md"]);
40
+ const ALL_KINDS = ["skill", "tool", "context", "instructions"];
21
41
  export function parseCliArgs(argv) {
22
42
  let mode = "print";
23
43
  let prompt;
@@ -28,11 +48,21 @@ export function parseCliArgs(argv) {
28
48
  let compact;
29
49
  let maxToolRounds;
30
50
  let help = false;
51
+ let discover = false;
52
+ let discoverKinds = ["skill"];
53
+ let noDiscovery = false;
54
+ let noAgentsMd = false;
55
+ let noSystemMd = false;
56
+ let agentsMdFile;
57
+ let systemMdFile;
58
+ let agentsConfig;
31
59
  const config = [];
32
60
  const resources = [];
33
61
  const extensions = [];
34
62
  const tools = [];
35
63
  const context = [];
64
+ const instructions = [];
65
+ const injectorFiles = [];
36
66
  for (let i = 0; i < argv.length; i += 1) {
37
67
  const flag = argv[i];
38
68
  if (flag === "-h" || flag === "--help") {
@@ -40,6 +70,23 @@ export function parseCliArgs(argv) {
40
70
  continue;
41
71
  }
42
72
  const name = flag === "-p" ? "--prompt" : flag;
73
+ if (boolFlags.has(name)) {
74
+ switch (name) {
75
+ case "--discover":
76
+ discover = true;
77
+ break;
78
+ case "--no-discovery":
79
+ noDiscovery = true;
80
+ break;
81
+ case "--no-agents-md":
82
+ noAgentsMd = true;
83
+ break;
84
+ case "--no-system-md":
85
+ noSystemMd = true;
86
+ break;
87
+ }
88
+ continue;
89
+ }
43
90
  if (!valueFlags.has(name))
44
91
  throw new CliUsageError(`Unknown flag: ${flag}`);
45
92
  const value = argv[i + 1];
@@ -88,9 +135,36 @@ export function parseCliArgs(argv) {
88
135
  case "--max-tool-rounds":
89
136
  maxToolRounds = positiveInt(value, flag);
90
137
  break;
138
+ case "--discover-kinds":
139
+ discoverKinds = parseKinds(value, flag);
140
+ break;
141
+ case "--instruction":
142
+ instructions.push(value);
143
+ break;
144
+ case "--injector-file":
145
+ injectorFiles.push(value);
146
+ break;
147
+ case "--agents-md-file":
148
+ agentsMdFile = value;
149
+ break;
150
+ case "--system-md-file":
151
+ systemMdFile = value;
152
+ break;
153
+ case "--agents-config":
154
+ agentsConfig = value;
155
+ break;
91
156
  }
92
157
  }
93
- return { mode, prompt, provider, model, session, config, resources, extensions, tools, system, context, compact, maxToolRounds, help };
158
+ return { mode, prompt, provider, model, session, config, resources, extensions, tools, system, context, compact, maxToolRounds, help, discover, discoverKinds, noDiscovery, agentsConfig, discoveredSkills: [], discoveredInjectors: [], discoveredAgents: [], instructions, injectorFiles, resolvedInstructionInjectors: [], noAgentsMd, noSystemMd, agentsMdFile, systemMdFile, systemPromptLayers: [] };
159
+ }
160
+ function parseKinds(csv, flag) {
161
+ const kinds = csv.split(",").map((k) => k.trim()).filter(Boolean);
162
+ if (kinds.length === 0)
163
+ throw new CliUsageError(`Invalid value for ${flag}: ${csv}`);
164
+ const invalid = kinds.filter((k) => !ALL_KINDS.includes(k));
165
+ if (invalid.length > 0)
166
+ throw new CliUsageError(`Invalid kinds for ${flag}: ${invalid.join(", ")}`);
167
+ return kinds;
94
168
  }
95
169
  export async function runCli(argv, runtime) {
96
170
  let options;
@@ -112,8 +186,62 @@ export async function runCli(argv, runtime) {
112
186
  }
113
187
  if (!options.prompt)
114
188
  throw new CliUsageError("Missing prompt: use -p or --prompt");
189
+ const mode = options.mode;
190
+ // ponytail: discovery is opt-in. --no-discovery hard-disables; default runs never touch the FS.
191
+ // Registering all kinds honors the plan (tool/context/agent descriptors registered),
192
+ // but only skills are wired into the mock agent here — other kinds are host-owned execution.
193
+ let discoveredSkills = options.discoveredSkills;
194
+ let discoveredInjectors = options.discoveredInjectors;
195
+ let discoveredAgents = options.discoveredAgents;
196
+ const workspaceRoot = runtime.workspaceRoot ?? process.cwd();
197
+ // ponytail: trust policy shared by the repo AGENTS.md loader and the app-config bundle scanner.
198
+ // --agents-config is an explicit opt-in → its configRoot is a trusted root by that act.
199
+ const trustedRoots = [workspaceRoot];
200
+ if (options.agentsConfig)
201
+ trustedRoots.push(options.agentsConfig);
202
+ if (options.agentsMdFile)
203
+ trustedRoots.push(dirname(options.agentsMdFile));
204
+ const trust = createPathTrustPolicy({ trustedRoots });
205
+ if (options.discover && !options.noDiscovery) {
206
+ const discovered = await discoverContributions({
207
+ kinds: options.discoverKinds,
208
+ workspaceRoot,
209
+ });
210
+ const registries = createContributionRegistries();
211
+ registerDiscoveredContributions(registries, discovered);
212
+ discoveredSkills = registries.skills.list();
213
+ // ponytail: Phase 30 — discovered instruction injectors register host-owned (no core auto-import).
214
+ await registerDiscoveredInstructionInjectors(registries, discovered);
215
+ discoveredInjectors = registries.instructionInjectors.list();
216
+ options = { ...options, discoveredSkills, discoveredInjectors };
217
+ }
218
+ // ponytail: Phase 34 — --agents-config discovers app-config agent bundles (envelopes only,
219
+ // no import()/resolve). Host owns turning a bundle into a live agent via resolveAgentBundle.
220
+ if (options.agentsConfig) {
221
+ discoveredAgents = await discoverAgentBundles({ configRoot: options.agentsConfig, trust });
222
+ options = { ...options, discoveredAgents };
223
+ }
224
+ // ponytail: Phase 30 — resolve --instruction names against discovered injectors (fail-closed)
225
+ // and load --injector-file markdown as static injectors. `--instruction false` disables all.
226
+ const resolvedInstructionInjectors = options.instructions.includes("false")
227
+ ? []
228
+ : await resolveCliInjectors(options.instructions, options.injectorFiles, discoveredInjectors);
229
+ options = { ...options, resolvedInstructionInjectors };
230
+ // ponytail: Phase 31/34 — auto-load AGENTS.md (trust-gated, source: app) and the global
231
+ // SYSTEM.md layer (source: user) as systemPrompt layers composed on top of `--system`.
232
+ // The CLI no longer defaults globalRoot to the user's home directory; hosts pass globalRoot
233
+ // explicitly or use --agents-config / --system-md-file. RPC mode skips auto-load (host-owned).
234
+ if (options.mode !== "rpc") {
235
+ // ponytail: explicit --agents-md-file opt-in → trust its parent dir too so the named file passes
236
+ // the containment check (the user named it, so it's trusted by that act).
237
+ const layers = await loadSystemPromptFiles({
238
+ ...(options.noAgentsMd ? {} : { workspaceRoot, trust, ...(options.agentsMdFile ? { agentsMdPath: options.agentsMdFile } : {}) }),
239
+ ...(options.noSystemMd ? {} : { ...(runtime.globalRoot !== undefined ? { globalRoot: runtime.globalRoot } : {}), ...(options.systemMdFile ? { systemMdPath: options.systemMdFile } : {}) }),
240
+ });
241
+ options = { ...options, systemPromptLayers: layers };
242
+ }
115
243
  const session = (runtime.createSession ?? defaultCreateSession)(options);
116
- await runPromptMode(session, options, runtime.stdout, options.mode);
244
+ await runPromptMode(session, options, runtime.stdout, mode);
117
245
  return 0;
118
246
  }
119
247
  catch (error) {
@@ -147,14 +275,48 @@ function defaultCreateSession(options) {
147
275
  if (options.provider !== "mock")
148
276
  throw new CliUsageError("No provider configured. Pass --provider mock for a smoke test or embed Prism with an explicit provider.");
149
277
  const model = { provider: "mock", model: options.model ?? "mock" };
150
- return createAgent({ model, provider: createMockProvider([providerTextDelta("Hello"), providerDone()]), instructions: options.system }).createSession({ id: options.session });
278
+ return createAgent({
279
+ model,
280
+ provider: createMockProvider([providerTextDelta("Hello"), providerDone()]),
281
+ instructions: options.system,
282
+ // ponytail: Phase 31 — file layers compose with `instructions` (base) via the existing
283
+ // composeSystemPrompt pipeline; rank order (user<package<app<run) is enforced inside.
284
+ ...(options.systemPromptLayers.length > 0 ? { systemPrompt: options.systemPromptLayers } : {}),
285
+ // ponytail: discovered skills become selectable via RunOptions.activeSkills (set by runOptions below).
286
+ ...(options.discoveredSkills.length > 0 ? { skills: createSkillRegistry(options.discoveredSkills) } : {}),
287
+ }).createSession({ id: options.session });
151
288
  }
152
289
  function runOptions(options) {
153
290
  return {
154
291
  maxToolRounds: options.maxToolRounds,
155
292
  compaction: options.compact ? { thresholdEntries: options.compact } : undefined,
293
+ // ponytail: --discover is the explicit opt-in that activates discovered skills.
294
+ ...(options.discover && !options.noDiscovery && options.discoveredSkills.length > 0
295
+ ? { activeSkills: options.discoveredSkills.map((s) => s.name) }
296
+ : {}),
297
+ ...(options.resolvedInstructionInjectors.length > 0
298
+ ? { instructionInjectors: options.resolvedInstructionInjectors }
299
+ : {}),
156
300
  };
157
301
  }
302
+ // ponytail: resolve --instruction names against discovered injectors (fail-closed) and load
303
+ // --injector-file markdown files as static every_turn injectors. Names route through
304
+ // resolveInstructionInjectors so an unknown name throws (caught by runCli's error handler).
305
+ async function resolveCliInjectors(names, files, discovered) {
306
+ const registry = createContributionRegistry({ label: "instruction injector" });
307
+ for (const inj of discovered)
308
+ registry.register(inj.name, inj);
309
+ const byName = names.length ? resolveInstructionInjectors({ registry, names }) : [];
310
+ const fromFiles = [];
311
+ for (const path of files) {
312
+ const text = await readFile(path, "utf8");
313
+ fromFiles.push({
314
+ name: basename(path).replace(/\.[^.]+$/, ""),
315
+ apply: () => ({ instructions: text, when: "every_turn" }),
316
+ });
317
+ }
318
+ return [...byName, ...fromFiles];
319
+ }
158
320
  function positiveInt(value, flag) {
159
321
  const parsed = Number(value);
160
322
  if (!Number.isInteger(parsed) || parsed < 0)
@@ -55,6 +55,8 @@ function blockText(block) {
55
55
  return block.text;
56
56
  if (block.type === "tool_call")
57
57
  return `[tool_call ${block.name}]`;
58
+ if (block.type === "tool_call_delta")
59
+ return `[tool_call_delta ${block.name ?? block.index}]`;
58
60
  if (block.type === "tool_result")
59
61
  return `[tool_result ${block.name}]`;
60
62
  return "[image]";
package/dist/config.js CHANGED
@@ -1,9 +1,10 @@
1
1
  export function isJsonObject(value) {
2
- return isPlainObject(value) && isJsonValue(value);
2
+ return isPlainObject(value) && isJsonValue(value) && isSafeJsonValue(value);
3
3
  }
4
4
  export function assertJsonObject(value, label = "value") {
5
- if (!isJsonObject(value))
5
+ if (!isPlainObject(value) || !isJsonValue(value))
6
6
  throw new Error(`${label} must be a JSON object`);
7
+ assertSafeJsonValue(value, label);
7
8
  }
8
9
  export async function loadConfigLayers(providers, context) {
9
10
  const layers = [];
@@ -24,27 +25,32 @@ export function mergeConfigLayers(layers) {
24
25
  }
25
26
  return merged;
26
27
  }
27
- function mergeObjects(base, override) {
28
- const result = { ...cloneJsonObject(base) };
28
+ function mergeObjects(base, override, path = "config") {
29
+ const result = { ...cloneJsonObject(base, path) };
29
30
  for (const [key, value] of Object.entries(override)) {
31
+ const childPath = `${path}.${key}`;
32
+ assertSafeJsonKey(key, childPath);
30
33
  const current = result[key];
31
34
  result[key] = isPlainObject(current) && isPlainObject(value)
32
- ? mergeObjects(current, value)
33
- : cloneJsonValue(value);
35
+ ? mergeObjects(current, value, childPath)
36
+ : cloneJsonValue(value, childPath);
34
37
  }
35
38
  return result;
36
39
  }
37
- function cloneJsonObject(value) {
40
+ function cloneJsonObject(value, path = "value") {
38
41
  const result = {};
39
- for (const [key, entry] of Object.entries(value))
40
- result[key] = cloneJsonValue(entry);
42
+ for (const [key, entry] of Object.entries(value)) {
43
+ const childPath = `${path}.${key}`;
44
+ assertSafeJsonKey(key, childPath);
45
+ result[key] = cloneJsonValue(entry, childPath);
46
+ }
41
47
  return result;
42
48
  }
43
- function cloneJsonValue(value) {
49
+ function cloneJsonValue(value, path = "value") {
44
50
  if (Array.isArray(value))
45
- return value.map(cloneJsonValue);
51
+ return value.map((entry, index) => cloneJsonValue(entry, `${path}[${index}]`));
46
52
  if (isPlainObject(value))
47
- return cloneJsonObject(value);
53
+ return cloneJsonObject(value, path);
48
54
  return value;
49
55
  }
50
56
  function isJsonValue(value) {
@@ -60,6 +66,35 @@ function isJsonValue(value) {
60
66
  return Object.values(value).every(isJsonValue);
61
67
  return false;
62
68
  }
69
+ function isSafeJsonValue(value) {
70
+ try {
71
+ assertSafeJsonValue(value, "value");
72
+ return true;
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+ function assertSafeJsonValue(value, path) {
79
+ if (Array.isArray(value)) {
80
+ value.forEach((entry, index) => assertSafeJsonValue(entry, `${path}[${index}]`));
81
+ return;
82
+ }
83
+ if (!isPlainObject(value))
84
+ return;
85
+ for (const [key, entry] of Object.entries(value)) {
86
+ const childPath = `${path}.${key}`;
87
+ assertSafeJsonKey(key, childPath);
88
+ assertSafeJsonValue(entry, childPath);
89
+ }
90
+ }
91
+ function assertSafeJsonKey(key, path) {
92
+ if (!isSafeJsonKey(key))
93
+ throw new Error(`${path} uses forbidden JSON key: ${key}`);
94
+ }
95
+ function isSafeJsonKey(key) {
96
+ return key !== "__proto__" && key !== "prototype" && key !== "constructor";
97
+ }
63
98
  function isPlainObject(value) {
64
99
  if (typeof value !== "object" || value === null || Array.isArray(value))
65
100
  return false;