@arnilo/prism 0.1.5 → 0.1.7

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 (41) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/agent-run-lifecycle.d.ts +2 -0
  3. package/dist/agent-run-lifecycle.js +7 -0
  4. package/dist/agent-run-state.d.ts +2 -0
  5. package/dist/agent-run-state.js +10 -0
  6. package/dist/agent-session.d.ts +7 -0
  7. package/dist/agent-session.js +25 -2
  8. package/dist/cache-telemetry.d.ts +58 -0
  9. package/dist/cache-telemetry.js +102 -0
  10. package/dist/cli-provider-add.d.ts +37 -0
  11. package/dist/cli-provider-add.js +293 -0
  12. package/dist/cli-runner.d.ts +5 -1
  13. package/dist/cli-runner.js +13 -1
  14. package/dist/contracts-run-state.d.ts +13 -0
  15. package/dist/index.d.ts +5 -3
  16. package/dist/index.js +3 -2
  17. package/dist/skill-load.d.ts +23 -0
  18. package/dist/skill-load.js +74 -0
  19. package/docs/acp.md +2 -2
  20. package/docs/agent-session-runtime.md +1 -1
  21. package/docs/cli-rpc.md +33 -0
  22. package/docs/coding-agent-tools.md +12 -7
  23. package/docs/coding-security.md +3 -0
  24. package/docs/context-and-skills.md +2 -2
  25. package/docs/document-reader.md +85 -0
  26. package/docs/index.md +8 -8
  27. package/docs/model-routing.md +45 -0
  28. package/docs/provider-caching.md +63 -0
  29. package/docs/provider-packages.md +2 -0
  30. package/docs/release-and-install.md +54 -4
  31. package/package.json +3 -2
  32. package/templates/provider/CHANGELOG.md.tmpl +5 -0
  33. package/templates/provider/README.md.tmpl +41 -0
  34. package/templates/provider/docs/providers/NAME.md.tmpl +61 -0
  35. package/templates/provider/package.json.tmpl +49 -0
  36. package/templates/provider/src/cache.ts.tmpl +20 -0
  37. package/templates/provider/src/index.ts.tmpl +33 -0
  38. package/templates/provider/src/models.ts.tmpl +16 -0
  39. package/templates/provider/src/provider.ts.tmpl +23 -0
  40. package/templates/provider/src/tests/provider.test.ts.tmpl +104 -0
  41. package/templates/provider/tsconfig.json.tmpl +16 -0
@@ -0,0 +1,293 @@
1
+ import { accessSync, constants as fsConstants } from "node:fs";
2
+ import { access, mkdir, readdir, readFile, realpath, writeFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ export class ProviderAddUsageError extends Error {
6
+ }
7
+ const NPM_NAME_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
8
+ const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
9
+ const MAX_NAME_LENGTH = 214;
10
+ const DEFAULT_BASE_URL = "https://api.example.com/v1";
11
+ export function getProviderAddUsage() {
12
+ return `Usage: prism providers add <name> [options]
13
+
14
+ Scaffold an OpenAI-compatible provider package (manifest, provider, models, cache
15
+ helpers, conformance test, docs stub) into ./<name>.
16
+
17
+ Arguments:
18
+ <name> npm-validated package/provider name (lowercase)
19
+
20
+ Options:
21
+ --base-url <url> Default Chat Completions base URL (default: ${DEFAULT_BASE_URL})
22
+ --env-key <name> Credential environment-var identifier (default: <NAME>_API_KEY)
23
+ --model <id> Starter model id (default: <name>-large)
24
+ --force Overwrite existing generated files
25
+ -h, --help Show this help
26
+
27
+ Examples:
28
+ prism providers add acme --base-url https://api.acme.example/v1 --env-key ACME_API_KEY --model acme-large
29
+ `;
30
+ }
31
+ export const providerAddUsage = getProviderAddUsage();
32
+ export function parseProviderAddArgs(argv) {
33
+ let name;
34
+ let baseUrl = DEFAULT_BASE_URL;
35
+ let envKey;
36
+ let model;
37
+ let force = false;
38
+ let help = false;
39
+ for (let i = 0; i < argv.length; i += 1) {
40
+ const arg = argv[i];
41
+ if (arg === "-h" || arg === "--help") {
42
+ help = true;
43
+ continue;
44
+ }
45
+ if (arg === "--force") {
46
+ force = true;
47
+ continue;
48
+ }
49
+ if (arg === "--base-url" || arg === "--env-key" || arg === "--model") {
50
+ const value = argv[i + 1];
51
+ if (value === undefined || value.startsWith("-")) {
52
+ throw new ProviderAddUsageError(`Missing value for ${arg}`);
53
+ }
54
+ i += 1;
55
+ if (arg === "--base-url") {
56
+ baseUrl = value;
57
+ }
58
+ else if (arg === "--env-key") {
59
+ envKey = value;
60
+ }
61
+ else {
62
+ model = value;
63
+ }
64
+ continue;
65
+ }
66
+ if (arg.startsWith("-")) {
67
+ throw new ProviderAddUsageError(`Unknown flag: ${arg}`);
68
+ }
69
+ if (name !== undefined) {
70
+ throw new ProviderAddUsageError(`Unexpected argument: ${arg}`);
71
+ }
72
+ name = arg;
73
+ }
74
+ if (!help && name === undefined) {
75
+ throw new ProviderAddUsageError("Missing provider name");
76
+ }
77
+ return {
78
+ name: name ?? "provider",
79
+ baseUrl,
80
+ envKey: envKey ?? `${(name ?? "provider").replace(/[^a-z0-9]+/gi, "_").toUpperCase()}_API_KEY`,
81
+ model: model ?? `${name ?? "provider"}-large`,
82
+ force,
83
+ help,
84
+ };
85
+ }
86
+ export async function runProviderAddCommand(argv, runtime) {
87
+ let options;
88
+ try {
89
+ options = parseProviderAddArgs(argv);
90
+ }
91
+ catch (error) {
92
+ write(runtime.stderr, `${error instanceof Error ? error.message : String(error)}\n${getProviderAddUsage()}`);
93
+ return 2;
94
+ }
95
+ if (options.help) {
96
+ write(runtime.stdout, getProviderAddUsage());
97
+ return 0;
98
+ }
99
+ try {
100
+ validateProviderName(options.name);
101
+ validateBaseUrl(options.baseUrl);
102
+ if (!ENV_KEY_PATTERN.test(options.envKey)) {
103
+ throw new ProviderAddUsageError(`Invalid --env-key: ${options.envKey} (must be a shell-safe identifier)`);
104
+ }
105
+ const result = await createProviderProject(options, runtime);
106
+ write(runtime.stdout, [
107
+ `Scaffolded provider package in ${result.targetDir}`,
108
+ ` name: ${result.name}`,
109
+ ` files: ${result.writtenFiles.length}`,
110
+ ` bytes: ${result.totalBytes}`,
111
+ "",
112
+ "Next:",
113
+ ` cd ${result.name}`,
114
+ " npm install",
115
+ " npm test",
116
+ " Replace the starter model metadata and docs stub with docs-verified values before publishing.",
117
+ "",
118
+ ].join("\n"));
119
+ return 0;
120
+ }
121
+ catch (error) {
122
+ const message = error instanceof Error ? error.message : String(error);
123
+ if (error instanceof ProviderAddUsageError) {
124
+ write(runtime.stderr, `${message}\n${getProviderAddUsage()}`);
125
+ return 2;
126
+ }
127
+ write(runtime.stderr, `${message}\n`);
128
+ return 1;
129
+ }
130
+ }
131
+ export async function createProviderProject(options, runtime = {
132
+ stdout: process.stdout,
133
+ stderr: process.stderr,
134
+ }) {
135
+ validateProviderName(options.name);
136
+ const cwd = runtime.cwd ?? process.cwd();
137
+ const targetDir = resolve(cwd, options.name);
138
+ const templatesRoot = runtime.templatesRoot ?? defaultProviderTemplatesRoot();
139
+ const version = runtime.packageVersion ?? (await readPackageVersion());
140
+ await assertDestinationWritable(targetDir, options.force);
141
+ const tokens = buildTokens({ ...options, version });
142
+ const planned = planProviderFiles(templatesRoot, options.name);
143
+ if (!options.force) {
144
+ for (const file of planned) {
145
+ const dest = join(targetDir, file.relativePath);
146
+ if (await exists(dest)) {
147
+ throw new ProviderAddUsageError(`Refusing to overwrite existing file: ${file.relativePath} (pass --force to overwrite)`);
148
+ }
149
+ }
150
+ }
151
+ const writtenFiles = [];
152
+ let totalBytes = 0;
153
+ for (const file of planned) {
154
+ const dest = join(targetDir, file.relativePath);
155
+ assertPathInside(targetDir, dest);
156
+ const raw = await readFile(file.sourcePath, "utf8");
157
+ const content = applyTokens(raw, tokens);
158
+ await mkdir(dirname(dest), { recursive: true });
159
+ await assertNoSymlinkEscape(targetDir, dirname(dest));
160
+ await writeFile(dest, content, "utf8");
161
+ writtenFiles.push(file.relativePath);
162
+ totalBytes += Buffer.byteLength(content, "utf8");
163
+ }
164
+ return {
165
+ targetDir,
166
+ writtenFiles,
167
+ name: options.name,
168
+ totalBytes,
169
+ };
170
+ }
171
+ export function defaultProviderTemplatesRoot() {
172
+ return join(dirname(fileURLToPath(import.meta.url)), "..", "templates", "provider");
173
+ }
174
+ /** npm package-name rules plus traversal refusal. Throws `ProviderAddUsageError` on violation. */
175
+ export function validateProviderName(name) {
176
+ if (name.length === 0)
177
+ throw new ProviderAddUsageError("Missing provider name");
178
+ if (name.includes("\0"))
179
+ throw new ProviderAddUsageError("Invalid provider name");
180
+ if (name.length > MAX_NAME_LENGTH) {
181
+ throw new ProviderAddUsageError(`Invalid provider name: ${name} (max ${MAX_NAME_LENGTH} chars)`);
182
+ }
183
+ if (!NPM_NAME_PATTERN.test(name) || name.includes("..")) {
184
+ throw new ProviderAddUsageError(`Invalid provider name: ${name} (npm names are lowercase, start with a letter/digit, and contain only letters, digits, -, _, .)`);
185
+ }
186
+ }
187
+ function validateBaseUrl(baseUrl) {
188
+ let parsed;
189
+ try {
190
+ parsed = new URL(baseUrl);
191
+ }
192
+ catch {
193
+ throw new ProviderAddUsageError(`Invalid --base-url: ${baseUrl} (must be an http(s) URL)`);
194
+ }
195
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
196
+ throw new ProviderAddUsageError(`Invalid --base-url: ${baseUrl} (must be an http(s) URL)`);
197
+ }
198
+ }
199
+ function buildTokens(input) {
200
+ const pascal = input.name
201
+ .split(/[^a-z0-9]+/i)
202
+ .filter(Boolean)
203
+ .map((part) => part[0].toUpperCase() + part.slice(1))
204
+ .join("");
205
+ return {
206
+ __PROVIDER_ID__: input.name,
207
+ __PROVIDER_UPPER__: input.name.replace(/[^a-z0-9]+/gi, "_").toUpperCase(),
208
+ __PROVIDER_PASCAL__: pascal,
209
+ __PACKAGE_NAME__: input.name,
210
+ __PRISM_VERSION__: input.version,
211
+ __BASE_URL__: input.baseUrl.replace(/\/+$/, ""),
212
+ __ENV_KEY__: input.envKey,
213
+ __MODEL_ID__: input.model,
214
+ };
215
+ }
216
+ function planProviderFiles(templatesRoot, name) {
217
+ const files = [
218
+ { relativePath: "package.json", sourcePath: join(templatesRoot, "package.json.tmpl") },
219
+ { relativePath: "tsconfig.json", sourcePath: join(templatesRoot, "tsconfig.json.tmpl") },
220
+ { relativePath: "README.md", sourcePath: join(templatesRoot, "README.md.tmpl") },
221
+ { relativePath: "CHANGELOG.md", sourcePath: join(templatesRoot, "CHANGELOG.md.tmpl") },
222
+ { relativePath: "src/index.ts", sourcePath: join(templatesRoot, "src/index.ts.tmpl") },
223
+ { relativePath: "src/provider.ts", sourcePath: join(templatesRoot, "src/provider.ts.tmpl") },
224
+ { relativePath: "src/models.ts", sourcePath: join(templatesRoot, "src/models.ts.tmpl") },
225
+ { relativePath: "src/cache.ts", sourcePath: join(templatesRoot, "src/cache.ts.tmpl") },
226
+ { relativePath: "src/__tests__/provider.test.ts", sourcePath: join(templatesRoot, "src/tests/provider.test.ts.tmpl") },
227
+ { relativePath: `docs/providers/${name}.md`, sourcePath: join(templatesRoot, "docs/providers/NAME.md.tmpl") },
228
+ ];
229
+ for (const file of files) {
230
+ try {
231
+ accessSync(file.sourcePath, fsConstants.R_OK);
232
+ }
233
+ catch {
234
+ throw new Error(`Missing provider template: ${file.sourcePath}`);
235
+ }
236
+ }
237
+ return files;
238
+ }
239
+ function applyTokens(template, tokens) {
240
+ let out = template;
241
+ for (const [token, value] of Object.entries(tokens)) {
242
+ out = out.split(token).join(value);
243
+ }
244
+ if (/__[A-Z0-9_]+__/.test(out)) {
245
+ const leftover = out.match(/__[A-Z0-9_]+__/g) ?? [];
246
+ throw new Error(`Unresolved provider template tokens: ${Array.from(new Set(leftover)).join(", ")}`);
247
+ }
248
+ return out;
249
+ }
250
+ async function readPackageVersion() {
251
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
252
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
253
+ if (!pkg.version)
254
+ throw new Error(`Missing version in ${pkgPath}`);
255
+ return pkg.version;
256
+ }
257
+ async function assertDestinationWritable(targetDir, force) {
258
+ if (!(await exists(targetDir))) {
259
+ await mkdir(targetDir, { recursive: true });
260
+ return;
261
+ }
262
+ const entries = await readdir(targetDir);
263
+ if (entries.length > 0 && !force) {
264
+ throw new ProviderAddUsageError(`Destination is not empty: ${targetDir} (pass --force to overwrite generated files)`);
265
+ }
266
+ }
267
+ function assertPathInside(root, candidate) {
268
+ const rel = relative(root, candidate);
269
+ if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel)))
270
+ return;
271
+ throw new ProviderAddUsageError(`Refusing to write outside destination: ${candidate}`);
272
+ }
273
+ /** Refuse writes whose real parent directory escapes the real target (symlinked dirs). */
274
+ async function assertNoSymlinkEscape(targetDir, parentDir) {
275
+ const realTarget = await realpath(targetDir);
276
+ const realParent = await realpath(parentDir);
277
+ if (realParent !== realTarget && !realParent.startsWith(realTarget + sep)) {
278
+ throw new ProviderAddUsageError(`Refusing to write through a symlinked directory: ${parentDir}`);
279
+ }
280
+ }
281
+ async function exists(path) {
282
+ try {
283
+ await access(path, fsConstants.F_OK);
284
+ return true;
285
+ }
286
+ catch {
287
+ return false;
288
+ }
289
+ }
290
+ function write(stream, text) {
291
+ stream.write(text);
292
+ }
293
+ //# sourceMappingURL=cli-provider-add.js.map
@@ -60,10 +60,14 @@ export interface CliRuntime {
60
60
  readonly initTemplatesRoot?: string;
61
61
  /** Override version stamped by `prism init` (tests). */
62
62
  readonly initPackageVersion?: string;
63
+ /** Override provider-scaffold template root (tests). */
64
+ readonly providerTemplatesRoot?: string;
65
+ /** Override version stamped by `prism providers add` (tests). */
66
+ readonly providerPackageVersion?: string;
63
67
  /** Working directory for relative `prism init` destinations (tests). */
64
68
  readonly cwd?: string;
65
69
  }
66
- export declare const usage = "Usage: prism [--mode print|json|rpc] [-p prompt] [options]\n prism init <dir> [--provider <name>] [--with-workflows] [--with-evals] [--force]\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 --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";
70
+ export declare const usage = "Usage: prism [--mode print|json|rpc] [-p prompt] [options]\n prism init <dir> [--provider <name>] [--with-workflows] [--with-evals] [--force]\n prism providers add <name> [--base-url <url>] [--env-key <name>] [--model <id>] [--force]\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 --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";
67
71
  export declare function parseCliArgs(argv: readonly string[]): CliOptions;
68
72
  export declare function runCli(argv: readonly string[], runtime: CliRuntime): Promise<number>;
69
73
  export declare function runPromptMode(session: AgentSession, options: CliOptions, stdout: Writable, mode: "print" | "json"): Promise<void>;
@@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises";
2
2
  import { basename, dirname } from "node:path";
3
3
  import process from "node:process";
4
4
  import { initUsage, runInitCommand } from "./cli-init.js";
5
+ import { providerAddUsage, runProviderAddCommand } from "./cli-provider-add.js";
5
6
  import { createContributionRegistries, registerDiscoveredContributions } from "./contributions.js";
6
7
  import { createAgent, createContributionRegistry, createMockProvider, providerDone, providerTextDelta, resolveInstructionInjectors, } from "./index.js";
7
8
  import { discoverAgentBundles } from "./node/agent-definitions.js";
@@ -13,6 +14,7 @@ import { runRpcServer } from "./rpc.js";
13
14
  import { createSkillRegistry } from "./skills.js";
14
15
  export const usage = `Usage: prism [--mode print|json|rpc] [-p prompt] [options]
15
16
  prism init <dir> [--provider <name>] [--with-workflows] [--with-evals] [--force]
17
+ prism providers add <name> [--base-url <url>] [--env-key <name>] [--model <id>] [--force]
16
18
 
17
19
  Options:
18
20
  -p, --prompt <text> Prompt to run in print/json mode
@@ -209,6 +211,16 @@ export async function runCli(argv, runtime) {
209
211
  };
210
212
  return runInitCommand(argv.slice(1), initRuntime);
211
213
  }
214
+ if (argv[0] === "providers" && argv[1] === "add") {
215
+ const providerRuntime = {
216
+ stdout: runtime.stdout,
217
+ stderr: runtime.stderr,
218
+ ...(runtime.providerTemplatesRoot !== undefined ? { templatesRoot: runtime.providerTemplatesRoot } : {}),
219
+ ...(runtime.providerPackageVersion !== undefined ? { packageVersion: runtime.providerPackageVersion } : {}),
220
+ ...(runtime.cwd !== undefined ? { cwd: runtime.cwd } : {}),
221
+ };
222
+ return runProviderAddCommand(argv.slice(2), providerRuntime);
223
+ }
212
224
  let options;
213
225
  try {
214
226
  options = parseCliArgs(argv);
@@ -218,7 +230,7 @@ export async function runCli(argv, runtime) {
218
230
  return 2;
219
231
  }
220
232
  if (options.help) {
221
- write(runtime.stdout, `${usage}\n${initUsage}`);
233
+ write(runtime.stdout, `${usage}\n${initUsage}\n${providerAddUsage}`);
222
234
  return 0;
223
235
  }
224
236
  try {
@@ -156,6 +156,17 @@ export interface AgentRunStateOptions {
156
156
  * Default off: checkpoint shape is identical to 0.1.2.
157
157
  */
158
158
  readonly persistSessionState?: boolean;
159
+ /**
160
+ * Opt-in (plan 018 Task 6 closeout `checkpoint-bodies`): alongside
161
+ * `persistSessionState`, persist the exact loaded-skill instructions
162
+ * (`{name, instructions}` pairs, redacted at the checkpoint boundary like all state)
163
+ * so resume re-renders them registry-independently — no `load_skill` round-trip, no
164
+ * drift when the live registry changed or lost the skill. Both the run and the resume
165
+ * options must set it. Bounds: ≤64 bodies, ≤256-char names, ≤262144-byte bodies,
166
+ * ≤1 MiB total; the `maxStateBytes` ceiling refuses oversize with a recorded error
167
+ * (never silently truncates). Default off: checkpoint shape is identical to 0.1.3.
168
+ */
169
+ readonly includeSkillBodies?: boolean;
159
170
  }
160
171
  /** Versioned, redacted checkpoint payload. Treat as opaque except status/version/interruption. */
161
172
  export interface AgentRunState {
@@ -188,6 +199,8 @@ export interface AgentRunResumeOptions {
188
199
  readonly resumeNestedRun?: ResumeNestedRun;
189
200
  /** Opt-in (plan 015 Task 4): restore persisted loaded-skill names into the resumed session catalog. */
190
201
  readonly persistSessionState?: boolean;
202
+ /** Opt-in (plan 018 Task 6): restore persisted loaded-skill bodies (requires `persistSessionState` too). */
203
+ readonly includeSkillBodies?: boolean;
191
204
  }
192
205
  /** Bounded, abortable options for `resumeAgentRunStream()`. */
193
206
  export interface AgentRunResumeStreamOptions extends AgentRunResumeOptions, SubscribeOptions {
package/dist/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export type { ArtifactApproval, ArtifactApprovalState, ArtifactBodyErrorCode, Ar
11
11
  export { ARTIFACT_BODY_ERROR_CODES, ARTIFACT_CHECKPOINT_NAMESPACE, ArtifactBodyStoreError, ArtifactError, artifactApprovalState, artifactCheckpointKey, } from "./artifacts.js";
12
12
  export type { ApplyCacheControlOptions, CacheControlledContentBlock, CacheControlledMessage, CacheControlValue, CacheUsageReport, } from "./cache-helpers.js";
13
13
  export { applyCacheControl, cacheHitRate, cacheSavings, cacheUsageReport, mapCacheRetention, sanitizeCacheKey } from "./cache-helpers.js";
14
+ export type { CacheTelemetry, CacheTelemetryOptions, CacheTelemetryReport, CacheTelemetrySample, } from "./cache-telemetry.js";
15
+ export { CACHE_TELEMETRY_OVERFLOW_KEY, CacheTelemetryError, DEFAULT_CACHE_TELEMETRY_CAP, createCacheTelemetry, } from "./cache-telemetry.js";
14
16
  export type { MemoryCheckpointStoreOptions } from "./checkpoints.js";
15
17
  export { CHECKPOINT_CONFLICT_CODE, CheckpointConflictError, createMemoryCheckpointStore } from "./checkpoints.js";
16
18
  export type { DefaultCompactionStrategyOptions } from "./compaction.js";
@@ -86,8 +88,8 @@ export { createMemorySessionStore, createSessionEntry, getSessionBranchEntries,
86
88
  export { createChainedSettingsProvider, createStaticSettingsProvider } from "./settings.js";
87
89
  export type { LoadedSkillSet, SkillRenderContext, SkillsDisclosure } from "./skill-disclosure.js";
88
90
  export { createLoadedSkillSet, DEFAULT_MAX_SKILL_CATALOG_ENTRIES, DEFAULT_MAX_SKILL_DESCRIPTION_BYTES, DEFAULT_MAX_SKILL_INSTRUCTION_BYTES, EMPTY_SKILL_DESCRIPTION, HARD_MAX_SKILL_CATALOG_ENTRIES, HARD_MAX_SKILL_DESCRIPTION_BYTES, HARD_MAX_SKILL_INSTRUCTION_BYTES, isSkillDisclosureError, resolveSkillsDisclosure, SkillDisclosureError, } from "./skill-disclosure.js";
89
- export type { CreateLoadSkillToolOptions, ResolveSkillLoadOptions } from "./skill-load.js";
90
- export { createLoadSkillTool, DEFAULT_LOAD_SKILL_TOOL_NAME, isSkillLoadError, MAX_LOAD_SKILL_RESULT_BYTES, resolveSkillLoad, SKILL_LOAD_ERROR_CODE, SkillLoadError, } from "./skill-load.js";
91
+ export type { CreateLoadSkillToolOptions, LoadedSkillBodiesEntry, ResolveSkillLoadOptions, } from "./skill-load.js";
92
+ export { applyRestoredSkillBodies, createLoadSkillTool, DEFAULT_LOAD_SKILL_TOOL_NAME, HARD_MAX_PERSISTED_SKILL_BODY_TOTAL_BYTES, isSkillLoadError, MAX_LOAD_SKILL_RESULT_BYTES, MAX_PERSISTED_SKILL_BODIES, MAX_PERSISTED_SKILL_BODY_BYTES, MAX_PERSISTED_SKILL_BODY_NAME_CHARS, resolveSkillLoad, SKILL_LOAD_ERROR_CODE, SkillLoadError, snapshotLoadedSkillBodies, validateLoadedSkillBodies, } from "./skill-load.js";
91
93
  export type { ResolveActiveSkillsOptions, SkillRegistryOptions } from "./skills.js";
92
94
  export { createSkillRegistry, resolveActiveSkills } from "./skills.js";
93
95
  export { artifactStructuredOutputRequest, assertStructuredOutputRequestSupported, DEFAULT_MAX_STRUCTURED_OUTPUT_NAME_LENGTH, DEFAULT_MAX_STRUCTURED_OUTPUT_SCHEMA_BYTES, modelSupportsStructuredOutput, resolveRunProviderOptions, StructuredOutputError, validateStructuredOutputOptions, withoutStructuredOutput, } from "./structured-output.js";
@@ -105,5 +107,5 @@ export { createToolParameterValidator, createToolRegistry, dispatchToolCall, fil
105
107
  export type { ResolvedUseCaseModel, ResolveUseCaseModelInput, UseCaseModelBinding, } from "./use-case-model.js";
106
108
  export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
107
109
  export declare const name = "prism";
108
- export declare const version = "0.1.5";
110
+ export declare const version = "0.1.7";
109
111
  export declare const description = "Agent harness for AI providers, agents, sessions, and tools.";
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export { AGENT_RUN_STATE_NAMESPACE, AGENT_RUN_STATE_SCHEMA_VERSION, agentFingerp
6
6
  export { createAgent, createAgentSession, resumeAgentRun, resumeAgentRunStream } from "./agents.js";
7
7
  export { ARTIFACT_BODY_ERROR_CODES, ARTIFACT_CHECKPOINT_NAMESPACE, ArtifactBodyStoreError, ArtifactError, artifactApprovalState, artifactCheckpointKey, } from "./artifacts.js";
8
8
  export { applyCacheControl, cacheHitRate, cacheSavings, cacheUsageReport, mapCacheRetention, sanitizeCacheKey } from "./cache-helpers.js";
9
+ export { CACHE_TELEMETRY_OVERFLOW_KEY, CacheTelemetryError, DEFAULT_CACHE_TELEMETRY_CAP, createCacheTelemetry, } from "./cache-telemetry.js";
9
10
  export { CHECKPOINT_CONFLICT_CODE, CheckpointConflictError, createMemoryCheckpointStore } from "./checkpoints.js";
10
11
  export { createDefaultCompactionStrategy, isCompactionEntryData } from "./compaction.js";
11
12
  export { assertJsonObject, isJsonObject, loadConfigLayers, mergeConfigLayers } from "./config.js";
@@ -46,7 +47,7 @@ export { assertPermission, assertTrusted, checkPermission, createStaticPermissio
46
47
  export { createMemorySessionStore, createSessionEntry, getSessionBranchEntries, listSessionBranches, rebuildSessionContext, } from "./session-stores.js";
47
48
  export { createChainedSettingsProvider, createStaticSettingsProvider } from "./settings.js";
48
49
  export { createLoadedSkillSet, DEFAULT_MAX_SKILL_CATALOG_ENTRIES, DEFAULT_MAX_SKILL_DESCRIPTION_BYTES, DEFAULT_MAX_SKILL_INSTRUCTION_BYTES, EMPTY_SKILL_DESCRIPTION, HARD_MAX_SKILL_CATALOG_ENTRIES, HARD_MAX_SKILL_DESCRIPTION_BYTES, HARD_MAX_SKILL_INSTRUCTION_BYTES, isSkillDisclosureError, resolveSkillsDisclosure, SkillDisclosureError, } from "./skill-disclosure.js";
49
- export { createLoadSkillTool, DEFAULT_LOAD_SKILL_TOOL_NAME, isSkillLoadError, MAX_LOAD_SKILL_RESULT_BYTES, resolveSkillLoad, SKILL_LOAD_ERROR_CODE, SkillLoadError, } from "./skill-load.js";
50
+ export { applyRestoredSkillBodies, createLoadSkillTool, DEFAULT_LOAD_SKILL_TOOL_NAME, HARD_MAX_PERSISTED_SKILL_BODY_TOTAL_BYTES, isSkillLoadError, MAX_LOAD_SKILL_RESULT_BYTES, MAX_PERSISTED_SKILL_BODIES, MAX_PERSISTED_SKILL_BODY_BYTES, MAX_PERSISTED_SKILL_BODY_NAME_CHARS, resolveSkillLoad, SKILL_LOAD_ERROR_CODE, SkillLoadError, snapshotLoadedSkillBodies, validateLoadedSkillBodies, } from "./skill-load.js";
50
51
  export { createSkillRegistry, resolveActiveSkills } from "./skills.js";
51
52
  export { artifactStructuredOutputRequest, assertStructuredOutputRequestSupported, DEFAULT_MAX_STRUCTURED_OUTPUT_NAME_LENGTH, DEFAULT_MAX_STRUCTURED_OUTPUT_SCHEMA_BYTES, modelSupportsStructuredOutput, resolveRunProviderOptions, StructuredOutputError, validateStructuredOutputOptions, withoutStructuredOutput, } from "./structured-output.js";
52
53
  export { composeSystemPrompt, mergeSystemPromptConfig } from "./system-prompts.js";
@@ -57,6 +58,6 @@ export { DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES, DEFAULT_TOOL_RESULT_FOLD_MI
57
58
  export { createToolParameterValidator, createToolRegistry, dispatchToolCall, filterTools } from "./tools.js";
58
59
  export { resolveUseCaseModel, resolveUseCaseModelBinding, useCaseCredentialProviderId, } from "./use-case-model.js";
59
60
  export const name = "prism";
60
- export const version = "0.1.5";
61
+ export const version = "0.1.7";
61
62
  export const description = "Agent harness for AI providers, agents, sessions, and tools.";
62
63
  //# sourceMappingURL=index.js.map
@@ -3,6 +3,29 @@ import { type LoadedSkillSet } from "./skill-disclosure.js";
3
3
  export declare const DEFAULT_LOAD_SKILL_TOOL_NAME: "load_skill";
4
4
  export declare const SKILL_LOAD_ERROR_CODE: "skill_load_failed";
5
5
  export declare const MAX_LOAD_SKILL_RESULT_BYTES = 512;
6
+ export declare const MAX_PERSISTED_SKILL_BODIES = 64;
7
+ export declare const MAX_PERSISTED_SKILL_BODY_NAME_CHARS = 256;
8
+ export declare const MAX_PERSISTED_SKILL_BODY_BYTES = 262144;
9
+ export declare const HARD_MAX_PERSISTED_SKILL_BODY_TOTAL_BYTES: number;
10
+ /** One persisted loaded-skill body (plan 018 closeout `checkpoint-bodies`). */
11
+ export interface LoadedSkillBodiesEntry {
12
+ readonly name: string;
13
+ readonly instructions: string;
14
+ }
15
+ /** Fail-closed shape/cap validation for a persisted bodies payload (load and save sides). */
16
+ export declare function validateLoadedSkillBodies(value: unknown): asserts value is readonly LoadedSkillBodiesEntry[];
17
+ /**
18
+ * Snapshot the exact instructions of every loaded skill (registry-independent).
19
+ * Loaded names without instructions are skipped (they render no body anyway);
20
+ * a restored body for a loaded name wins over the registry body.
21
+ */
22
+ export declare function snapshotLoadedSkillBodies(skills: readonly Skill[], loaded: LoadedSkillSet, restored?: ReadonlyMap<string, string>): LoadedSkillBodiesEntry[];
23
+ /**
24
+ * Apply persisted bodies to the skills a resumed session will render: replace the
25
+ * instructions of known skills and append synthesized skills for names the live
26
+ * registry no longer serves, so the exact loaded text renders registry-independently.
27
+ */
28
+ export declare function applyRestoredSkillBodies(skills: readonly Skill[], bodies: readonly LoadedSkillBodiesEntry[]): readonly Skill[];
6
29
  export declare class SkillLoadError extends Error {
7
30
  readonly code: "skill_load_failed";
8
31
  constructor(message: string);
@@ -3,6 +3,80 @@ import { HARD_MAX_SKILL_INSTRUCTION_BYTES } from "./skill-disclosure.js";
3
3
  export const DEFAULT_LOAD_SKILL_TOOL_NAME = "load_skill";
4
4
  export const SKILL_LOAD_ERROR_CODE = "skill_load_failed";
5
5
  export const MAX_LOAD_SKILL_RESULT_BYTES = 512;
6
+ // Bodies-mode persistence bounds (plan 018 Task 6 closeout `checkpoint-bodies`): the
7
+ // checkpoint `maxStateBytes` ceiling is the documented outer bound (oversize refuses at
8
+ // save with a recorded error); these caps bound the bodies payload itself on load.
9
+ // ponytail: module constants, not tunable options — hosts tune maxStateBytes for the ceiling.
10
+ export const MAX_PERSISTED_SKILL_BODIES = 64; // names-mode parity (MAX_PERSISTED_SKILL_NAMES)
11
+ export const MAX_PERSISTED_SKILL_BODY_NAME_CHARS = 256; // names-mode parity
12
+ export const MAX_PERSISTED_SKILL_BODY_BYTES = 262_144; // = HARD_MAX_SKILL_INSTRUCTION_BYTES (loader parity)
13
+ export const HARD_MAX_PERSISTED_SKILL_BODY_TOTAL_BYTES = 1024 * 1024; // = HARD_MAX_AGENT_RUN_STATE_BYTES
14
+ /** Fail-closed shape/cap validation for a persisted bodies payload (load and save sides). */
15
+ export function validateLoadedSkillBodies(value) {
16
+ if (!Array.isArray(value) || value.length > MAX_PERSISTED_SKILL_BODIES) {
17
+ throw new SkillLoadError(`Loaded-skill bodies exceed ${MAX_PERSISTED_SKILL_BODIES} entries`);
18
+ }
19
+ let totalBytes = 0;
20
+ for (const entry of value) {
21
+ if (!entry || typeof entry !== "object" || typeof entry.name !== "string") {
22
+ throw new SkillLoadError("Malformed loaded-skill body entry: name must be a string");
23
+ }
24
+ const { name, instructions } = entry;
25
+ if (name.length > MAX_PERSISTED_SKILL_BODY_NAME_CHARS) {
26
+ throw new SkillLoadError(`Loaded-skill body name exceeds ${MAX_PERSISTED_SKILL_BODY_NAME_CHARS} chars`);
27
+ }
28
+ if (typeof instructions !== "string") {
29
+ throw new SkillLoadError("Malformed loaded-skill body entry: instructions must be a string");
30
+ }
31
+ const bodyBytes = Buffer.byteLength(instructions, "utf8");
32
+ if (bodyBytes > MAX_PERSISTED_SKILL_BODY_BYTES) {
33
+ throw new SkillLoadError(`Loaded-skill body exceeds ${MAX_PERSISTED_SKILL_BODY_BYTES} bytes`);
34
+ }
35
+ totalBytes += Buffer.byteLength(name, "utf8") + bodyBytes;
36
+ if (totalBytes > HARD_MAX_PERSISTED_SKILL_BODY_TOTAL_BYTES) {
37
+ throw new SkillLoadError(`Loaded-skill bodies exceed ${HARD_MAX_PERSISTED_SKILL_BODY_TOTAL_BYTES} total bytes`);
38
+ }
39
+ }
40
+ }
41
+ /**
42
+ * Snapshot the exact instructions of every loaded skill (registry-independent).
43
+ * Loaded names without instructions are skipped (they render no body anyway);
44
+ * a restored body for a loaded name wins over the registry body.
45
+ */
46
+ export function snapshotLoadedSkillBodies(skills, loaded, restored) {
47
+ const byName = new Map(skills.map((skill) => [skill.name, skill]));
48
+ const entries = [];
49
+ for (const name of loaded.list()) {
50
+ const instructions = restored?.get(name) ?? byName.get(name)?.instructions;
51
+ if (!instructions)
52
+ continue;
53
+ entries.push({ name, instructions });
54
+ }
55
+ validateLoadedSkillBodies(entries); // refuses oversize with a recorded error (never truncates)
56
+ return entries;
57
+ }
58
+ /**
59
+ * Apply persisted bodies to the skills a resumed session will render: replace the
60
+ * instructions of known skills and append synthesized skills for names the live
61
+ * registry no longer serves, so the exact loaded text renders registry-independently.
62
+ */
63
+ export function applyRestoredSkillBodies(skills, bodies) {
64
+ validateLoadedSkillBodies(bodies);
65
+ if (bodies.length === 0)
66
+ return skills;
67
+ const byName = new Map(skills.map((skill) => [skill.name, skill]));
68
+ const out = skills.map((skill) => {
69
+ const body = bodies.find((entry) => entry.name === skill.name);
70
+ return body ? { ...skill, instructions: body.instructions } : skill;
71
+ });
72
+ const known = new Set(skills.map((skill) => skill.name));
73
+ for (const entry of bodies) {
74
+ if (!known.has(entry.name)) {
75
+ out.push({ name: entry.name, instructions: entry.instructions });
76
+ }
77
+ }
78
+ return out;
79
+ }
6
80
  export class SkillLoadError extends Error {
7
81
  code = SKILL_LOAD_ERROR_CODE;
8
82
  constructor(message) {
package/docs/acp.md CHANGED
@@ -111,7 +111,7 @@ const agent = createPrismAcpAgent({
111
111
 
112
112
  ### Persistence and ownership
113
113
 
114
- - **The agent never persists `modeId`/`configValues`.** Defaults are recomputed per session from the `modes`/`configOptions` seams — a fresh `session/new`, `load`, or `resume` always starts from `defaultModeId` / option `defaultValue`, and the agent's per-session registry is in-memory only. Persisting mode/config across sessions is a **host** decision, and host-side persistence MUST be ownership-scoped.
114
+ - **Without the durability seam the agent never persists `modeId`/`configValues`.** Defaults are recomputed per session from the `modes`/`configOptions` seams — a fresh `session/new`, `load`, or `resume` always starts from `defaultModeId` / option `defaultValue`, and the agent's per-session registry is in-memory only. Persisting mode/config across sessions is a **host** decision, and host-side persistence MUST be ownership-scoped.
115
115
  - **Host persistence MUST key by `sessions.ownership`.** `authorize` binds transport identity to ownership; a host store that persists `modeId`/`configValues` must refuse any restore whose stored ownership differs from the current session's ownership — a `sessionId` alone is never a sufficient key (session ids may collide across tenants). A cross-tenant restore rejects with `ERR_PRISM_ACP_INPUT` and never returns the other tenant's mode/config.
116
116
  - **Ownership-scoped restore (host-owned store).** The store is keyed by `sessionId` and records the owning `userId`; restore refuses on mismatch (this exact pattern is asserted in `packages/ag-ui/src/__tests__/acp-modes-config.test.ts`):
117
117
 
@@ -133,7 +133,7 @@ const agent = createPrismAcpAgent({
133
133
  ```
134
134
 
135
135
  Because the agent recomputes defaults on every `load`/`resume`, a host that restores state re-applies it after load through the same gated seams (`session/set_mode`, `session/set_config_option` — both run the `apply`/`onChange` hooks) and must refuse cross-tenant loads at the `authorize` seam first (falsy `authorize` = `Unauthorized ACP session`, before any mode/config state is reachable).
136
- - **Agent-owned persistence is 0.2.0.** A durable, ownership-scoped ACP session store (agent-side persistence of mode/config and session state) is roadmap 0.2.0 Module E, demand-gated; on the 0.1.x line the agent stays a thin per-session registry. See the [Host security guide](host-security.md) fail-closed checklist for the ACP boundary rows.
136
+ - **Durable registry (0.1.6, plan 018 closeout `acp-session-store`).** Pass `sessionStore` (`AcpSessionStore` from `@arnilo/prism-ag-ui/acp`) to let a restarted agent restore its live-session registry: `save` (on `session/new`, `set_mode`, `set_config_option`), `loadAll` (once per agent instance, lazily on first authorized touch), `evict` (on `close`/`delete`). The stored entry carries `sessionId`, `ownership`, `modeId`, `configValues`, `cwd`, `additionalDirectories`, `updatedAt` — never ephemeral stream state (client/controller/budget) or pending decisions. Restore re-resolves the live `AgentSession` through your `sessionFactory`, re-validates cwd/directories and mode/config values against the seams, enforces the registry cap, and drops corrupt or seam-mismatched entries fail-closed. The seam is additive-only: absent `sessionStore` ⇒ the agent behaves exactly as 0.1.5. Storage topology stays host-owned; the store is the trust boundary for tampering/replay. The full threat model and test mapping live in `docs/_evidence/phase18-primitive-review.md`; enforcement tests in `packages/ag-ui/src/__tests__/acp-session-store.test.ts`. The host-owned mode/config store pattern above stays valid for hosts that persist without the agent seam.
137
137
 
138
138
  ## Security and performance notes
139
139
 
@@ -198,7 +198,7 @@ if (result.status === "suspended") {
198
198
  }
199
199
  ```
200
200
 
201
- Resume requires exact checkpoint ownership, version, agent fingerprint, and revision. The fingerprint hashes the agent id/name, `definitionRevision`, model, instructions, system-prompt contributions, skills (name/instructions/tool names), tool definitions (name/parameters/exclusive), guardrail definitions (name/stage/revision), and loop strategy — changing any of them without bumping `definitionRevision` fails resume closed instead of silently continuing with different agent semantics. Prism CAS-claims approval before work, rechecks normal guardrail/permission/validation/limit paths, and marks a pending tool dispatched before its side effect. `createAgentRunLifecycle()` wraps the same core path for server/MCP hosts: adapters pass only authorized ownership, status returns only `{ state, version }`, and `resolveAgent()` supplies current agent/revision. `resumeStream()` uses that same claim path and bounded subscriber, so adapters do not poll or duplicate resume logic. Remote restart requires both checkpoint and session stores to be durable. A crash after that mark is ambiguous and is never replayed automatically; use host tool idempotency keyed by `runId`/`toolCallId` or resolve it manually. Checkpoints contain bounded redacted state plus session/leaf references, never provider objects, callbacks, signals, credentials, or raw secrets. State is bounded at save by `runState.maxStateBytes` (default 256 KB, at most the 1 MB hard cap); load bounds against the 1 MB hard cap only, so state saved with a raised limit stays resumable while oversized records are still rejected. Since 0.1.3 (plan 015 Task 4), durable runs may opt in to session-state persistence with `persistSessionState: true` on both the run and resume options: the loaded-skill **name catalog** (≤64 names, ≤256 chars each) rides the checkpoint and is restored into the resumed session's `LoadedSkillSet`; skill **bodies are never persisted** and re-resolve from the live registry via `load_skill`. Default off keeps the checkpoint shape byte-identical to 0.1.2. Built-in loop options are durable; custom `AgentLoopStrategy` instances are durable when they declare `snapshot`/`restore` hooks (see [Agent loops § Durable runs](agent-loops.md#durable-runs)) and reject before provider work otherwise.
201
+ Resume requires exact checkpoint ownership, version, agent fingerprint, and revision. The fingerprint hashes the agent id/name, `definitionRevision`, model, instructions, system-prompt contributions, skills (name/instructions/tool names), tool definitions (name/parameters/exclusive), guardrail definitions (name/stage/revision), and loop strategy — changing any of them without bumping `definitionRevision` fails resume closed instead of silently continuing with different agent semantics. Prism CAS-claims approval before work, rechecks normal guardrail/permission/validation/limit paths, and marks a pending tool dispatched before its side effect. `createAgentRunLifecycle()` wraps the same core path for server/MCP hosts: adapters pass only authorized ownership, status returns only `{ state, version }`, and `resolveAgent()` supplies current agent/revision. `resumeStream()` uses that same claim path and bounded subscriber, so adapters do not poll or duplicate resume logic. Remote restart requires both checkpoint and session stores to be durable. A crash after that mark is ambiguous and is never replayed automatically; use host tool idempotency keyed by `runId`/`toolCallId` or resolve it manually. Checkpoints contain bounded redacted state plus session/leaf references, never provider objects, callbacks, signals, credentials, or raw secrets. State is bounded at save by `runState.maxStateBytes` (default 256 KB, at most the 1 MB hard cap); load bounds against the 1 MB hard cap only, so state saved with a raised limit stays resumable while oversized records are still rejected. Since 0.1.3 (plan 015 Task 4), durable runs may opt in to session-state persistence with `persistSessionState: true` on both the run and resume options: the loaded-skill **name catalog** (≤64 names, ≤256 chars each) rides the checkpoint and is restored into the resumed session's `LoadedSkillSet`; skill **bodies are never persisted** and re-resolve from the live registry via `load_skill`. Since 0.1.6 (plan 018 closeout `checkpoint-bodies`), `includeSkillBodies: true` on BOTH the run and resume options additionally persists the exact loaded-skill **instructions** (`{name, instructions}` pairs, redacted at the checkpoint boundary like all state, ≤64 bodies / ≤256-char names / ≤262144-byte bodies / ≤1 MiB total) so resume re-renders them registry-independently — no `load_skill` round-trip and no dependence on the registry still serving the same text; `maxStateBytes` (default 256 KB) refuses oversize bodies with a recorded error, never silently truncates. Default off keeps the checkpoint shape byte-identical to 0.1.3. Built-in loop options are durable; custom `AgentLoopStrategy` instances are durable when they declare `snapshot`/`restore` hooks (see [Agent loops § Durable runs](agent-loops.md#durable-runs)) and reject before provider work otherwise.
202
202
 
203
203
  ## Secure composition
204
204
 
package/docs/cli-rpc.md CHANGED
@@ -36,6 +36,38 @@ prism init <dir> [--provider <name>] [--with-workflows] [--with-evals] [--force]
36
36
 
37
37
  Default generation installs only `@arnilo/prism` (mock provider). Selecting a real provider adds exactly one `@arnilo/prism-provider-*` package. Storage, telemetry, memory, and server packages are never added unless a later phase introduces an explicit flag for them. Rerunning without `--force` refuses non-empty destinations and existing generated files. `.env.example` contains placeholders only; `.gitignore` excludes `.env` and local stores.
38
38
 
39
+ ### `prism providers add` (0.1.7)
40
+
41
+ ```bash
42
+ prism providers add <name> [--base-url <url>] [--env-key <name>] [--model <id>] [--force]
43
+ ```
44
+
45
+ Scaffolds an OpenAI-compatible provider package into `./<name>`: `package.json`
46
+ (peer dep on `@arnilo/prism`, `sideEffects: false`, publish metadata mirroring
47
+ first-party providers), `tsconfig.json`, `README.md`, `CHANGELOG.md`,
48
+ `src/index.ts` (`defineProviderPackage` + auth-method registration),
49
+ `src/provider.ts` (built on `createOpenAICompatibleProvider`),
50
+ `src/models.ts` (starter `ModelConfig` list), `src/cache.ts` (cache-hint
51
+ mapping helpers via the shared core helpers), `src/__tests__/provider.test.ts`
52
+ (wired to `@arnilo/prism/testing/provider-conformance`), and a
53
+ `docs/providers/<name>.md` stub.
54
+
55
+ | Flag / arg | Purpose |
56
+ | --- | --- |
57
+ | `<name>` | npm-validated provider/package name (lowercase); also the target directory. |
58
+ | `--base-url <url>` | Default Chat Completions base URL (default `https://api.example.com/v1`). |
59
+ | `--env-key <name>` | Credential environment-var identifier, e.g. `ACME_API_KEY` (default `<NAME>_API_KEY`). |
60
+ | `--model <id>` | Starter model id (default `<name>-large`). |
61
+ | `--force` | Overwrite generated files when the destination already exists. |
62
+ | `-h`, `--help` | Print providers-add usage. |
63
+
64
+ Scaffold output is host-chosen: it is never auto-registered into repo
65
+ workspaces, umbrellas, or any resolver. The generated conformance test is
66
+ offline (mock fetch) and proves stream shape, tool-call delta reconstruction,
67
+ header ownership, secret-leak redaction, and serialized-content coverage
68
+ against the base provider. Replace the starter model metadata and the docs
69
+ stub with docs-verified values before publishing.
70
+
39
71
  ### Run/RPC CLI flags
40
72
 
41
73
  | Flag | Purpose |
@@ -181,6 +213,7 @@ Suspended workflow resume parameters are `{ workflowId, runId, decision: "approv
181
213
  - JSONL is processed line by line with Node stdlib; no parser dependency, worker, watcher, or queue is added.
182
214
  - Unknown or malformed CLI/RPC input fails closed. Workflow resume validates decision and positive `expectedVersion`; ownership remains host-selected and checkpoint-enforced.
183
215
  - `prism init` refuses non-empty destinations without `--force`, keeps writes inside the destination root, and never executes downloaded code beyond the user's later `npm install`.
216
+ - `prism providers add` validates the name against npm package-name rules (lowercase, no separators or `..`), refuses path traversal and symlinked directories escaping the destination, validates `--base-url` as an http(s) URL and `--env-key` as a shell-safe identifier, and writes placeholders only — generated code never contains secrets.
184
217
  - Generated `.env.example` values are placeholders only; `.gitignore` excludes `.env` and local store files.
185
218
  - Default generated install stays small (~27 MB with TypeScript tooling in a clean consumer install versus Mastra's measured 439 MB scaffold); unselected storage/telemetry/eval/workflow packages are omitted.
186
219
  - Branch handles (`handleId`, `sessionId`, `leafId`) are identifiers only; do not encode credentials, tokens, provider objects, or secrets into them.