@arnilo/prism 0.1.6 → 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.
- package/CHANGELOG.md +5 -0
- package/dist/cache-telemetry.d.ts +58 -0
- package/dist/cache-telemetry.js +102 -0
- package/dist/cli-provider-add.d.ts +37 -0
- package/dist/cli-provider-add.js +293 -0
- package/dist/cli-runner.d.ts +5 -1
- package/dist/cli-runner.js +13 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/docs/cli-rpc.md +33 -0
- package/docs/index.md +4 -4
- package/docs/model-routing.md +45 -0
- package/docs/provider-caching.md +63 -0
- package/docs/provider-packages.md +2 -0
- package/docs/release-and-install.md +24 -0
- package/package.json +2 -2
- package/templates/provider/CHANGELOG.md.tmpl +5 -0
- package/templates/provider/README.md.tmpl +41 -0
- package/templates/provider/docs/providers/NAME.md.tmpl +61 -0
- package/templates/provider/package.json.tmpl +49 -0
- package/templates/provider/src/cache.ts.tmpl +20 -0
- package/templates/provider/src/index.ts.tmpl +33 -0
- package/templates/provider/src/models.ts.tmpl +16 -0
- package/templates/provider/src/provider.ts.tmpl +23 -0
- package/templates/provider/src/tests/provider.test.ts.tmpl +104 -0
- package/templates/provider/tsconfig.json.tmpl +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.7] - 2026-08-12
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **Release 0.1.7 (plan 019)** is the performance-and-DX patch on the frozen 0.1.x line — additive-only vs 0.1.6 (freeze manifest `scripts/phase19-freeze-manifest.json`; every task's diff stayed inside its allowed files, enforced by the phase19 freeze machine; the async `AgUiProjection` item is a verification closeout, not new code). (1) **Prompt-cache telemetry surface** (`cache-telemetry`): dependency-free `createCacheTelemetry()` aggregator in core — host-activated (nothing subscribes by import), consumes `Usage` + `ModelConfig` pairs from the usage `ProviderEvent` or run-ledger records, and reports per-provider/model request counts, aggregate hit rate via the existing `cacheHitRate` math, cache-read/write token totals, and estimated savings via `cacheSavings` when cost metadata exists; bounded cardinality (default cap 256 distinct provider/model keys, overflow collapses into the `CACHE_TELEMETRY_OVERFLOW_KEY` `__overflow__` bucket with an `overflowed` flag, ponytail ceiling named — host-configurable caps or LRU eviction only if a real deployment exceeds it); reports carry token counters/rates only — never prompt content, cache keys, or identity; `record()` is O(1) with validated finite non-negative inputs; no OTel metric emission (demand-gated follow-up). (2) **Model-router selection policies** (`router-selection`): additive `selection` hook on `CreateModelRouterOptions` — `ModelRouterSelectionPolicy` (`name`/`rank`/`observe`); default ordered behavior byte-identical (regression test); reference `createCostLatencySelection` ranks candidates by `ModelCost` (input/output/cacheRead with per-million normalization) then in-memory EMA latency fed from `recordOutcome`'s new optional `latencyMs` (`latencyWeight` 0–1, default 0.5; pure-cost order on cold start); the policy is a permutation-only reorder of already-allowed candidates so it cannot widen allow-list/residency/budget decisions, and any drop/add/duplicate misbehavior fails closed with `ERR_PRISM_MODEL_ROUTER_POLICY`; policy name rides the still-redacted diagnostics; durable latency stats are a demand-gated follow-up requiring a `ModelRouterStateStore` contract change. (3) **Async AgUiProjection closeout** (`async-hooks`): plan 009 Task 15 surface verified with evidence — hooks are typed `Awaitable<T>` (17 hooks), `getMessages` accepts `() => readonly AgUiMessage[] | Promise<...>` so `messagesFromSession` can call async host APIs like `session.entries()`, snapshots are awaited strictly in event order (never `Promise.all`), rejection fails closed per event with sibling hooks still projected, caps apply to awaited values; evidence in `scripts/phase19-baseline.json` `asyncHooks` (`verified: true`, `gapFound: false`). (4) **`prism providers add <name>` scaffold** (`provider-scaffold`): new CLI subcommand (stdlib-only, mirrors `prism init`) scaffolds an OpenAI-compatible provider package into `./<name>` — `package.json` (peer dep `@arnilo/prism`, `sideEffects: false`, publish metadata), `tsconfig.json`, `README.md`, `CHANGELOG.md`, `src/index.ts` (`defineProviderPackage` + `api_key` auth-method registration), `src/provider.ts` (built on `createOpenAICompatibleProvider`), `src/models.ts` (starter `ModelConfig` list), `src/cache.ts` (cache-hint mapping via the shared core helpers), `src/__tests__/provider.test.ts` (offline conformance: stream shape + usage, header ownership, tool-call delta reconstruction, serialized-content coverage, secret-leak redaction), and `docs/providers/<name>.md` stub; flags `--base-url` (http(s) validated), `--env-key` (shell-safe identifier), `--model`, `--force`; npm package-name validation, path-traversal and symlink-escape refusal (nothing can land outside the destination), usage errors exit 2 with nothing written, generated code contains placeholders only — never secrets; scaffold output is host-chosen and never auto-registered into repo workspaces or resolvers; a fixture test proves the generated package typechecks and passes its conformance test offline against the repo build. Release graph stays **50** publishable manifests (root + 49 workspace packages — 14 provider adapters, 9 `prism-*` family/profile, 26 capability incl. `@arnilo/prism-document-reader`) at exact **0.1.7**. Exit gate green (core tests + script gates incl. phase19-freeze done-phase, `sdk:ready`, audit 0 moderate, pack dry-run 50/50 twice byte-identical, plain compat gate at 0.1.7 with 0 breaking deltas then version-literal baseline refresh — no `--allow-break` anywhere, evidence in `scripts/phase19-baseline.json`). Store compatibility with 0.1.6: **compatible, no migration** (additive-only; no persisted-shape change; `docs/migration.md` gains no entries). **Publication remains the operator handoff** (`docs/release-and-install.md` `0.1.7 publish handoff` — signed `v0.1.7` tag + npm OIDC). **CI hardening after the exit gate** (same day): the release `verify` job now runs the sdk:ready legs phase-by-phase (explicit rc per leg so a silent failure still names the failing phase) and uploads `sdk-ready.log` as an artifact on failure; the examples demo test compiles the demos in place with the repo tsc before spawning them, so the spawned children are plain JS instead of loading the amaro type-stripping WASM module (whose large per-process virtual reservation fails with `WebAssembly.Instance(): Out of memory` on memory-constrained CI runners); emitted .js files are removed in a finally block.
|
|
7
|
+
|
|
3
8
|
## [0.1.6] - 2026-08-11
|
|
4
9
|
|
|
5
10
|
### Changed
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ModelConfig, Usage } from "./contracts-core.js";
|
|
2
|
+
/**
|
|
3
|
+
* Prompt-cache telemetry surface (0.1.7, plan 019 Task 2).
|
|
4
|
+
*
|
|
5
|
+
* Dependency-free aggregator hosts attach to their `usage` `ProviderEvent`
|
|
6
|
+
* stream (or run-ledger usage records) to get per-provider/model cache
|
|
7
|
+
* statistics for tuning the `cache_aware` input layout. Explicit activation:
|
|
8
|
+
* nothing subscribes by import — the host calls `record()`.
|
|
9
|
+
*/
|
|
10
|
+
/** Single provider/model statistics sample. */
|
|
11
|
+
export interface CacheTelemetrySample {
|
|
12
|
+
readonly provider: string;
|
|
13
|
+
readonly model: string;
|
|
14
|
+
readonly requests: number;
|
|
15
|
+
readonly cacheReadTokens: number;
|
|
16
|
+
readonly cacheWriteTokens: number;
|
|
17
|
+
readonly inputTokens: number;
|
|
18
|
+
/** Cached-input ratio across the sample (`cacheHitRate` math, aggregated). */
|
|
19
|
+
readonly hitRate?: number;
|
|
20
|
+
/** Estimated read-token savings via `cacheSavings` math; present only when
|
|
21
|
+
* the sample's model carries cost metadata (`ModelCost.input`/`cacheRead`). */
|
|
22
|
+
readonly estimatedSavings?: number;
|
|
23
|
+
readonly currency?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Aggregated report. Samples are sorted by provider then model. */
|
|
26
|
+
export interface CacheTelemetryReport {
|
|
27
|
+
readonly samples: readonly CacheTelemetrySample[];
|
|
28
|
+
readonly overflowed: boolean;
|
|
29
|
+
readonly totalRequests: number;
|
|
30
|
+
readonly totalCacheReadTokens: number;
|
|
31
|
+
readonly totalCacheWriteTokens: number;
|
|
32
|
+
}
|
|
33
|
+
export interface CacheTelemetryOptions {
|
|
34
|
+
/** Distinct provider/model keys before excess keys collapse into the
|
|
35
|
+
* `__overflow__` bucket. Default {@link DEFAULT_CACHE_TELEMETRY_CAP}. */
|
|
36
|
+
readonly maxKeys?: number;
|
|
37
|
+
}
|
|
38
|
+
export interface CacheTelemetry {
|
|
39
|
+
/** Aggregate one usage record attributed to `model` (or an unknown bucket
|
|
40
|
+
* when no model is supplied). Rejects non-finite/negative token counts
|
|
41
|
+
* with {@link CacheTelemetryError}; validates before mutating. */
|
|
42
|
+
record(usage: Usage, model?: ModelConfig): void;
|
|
43
|
+
/** Snapshot of all samples (O(keys)); never throws. */
|
|
44
|
+
report(): CacheTelemetryReport;
|
|
45
|
+
/** Clear all samples (host rotation / long-run reset). */
|
|
46
|
+
reset(): void;
|
|
47
|
+
/** Number of distinct provider/model keys held (excluding the overflow bucket). */
|
|
48
|
+
readonly size: number;
|
|
49
|
+
}
|
|
50
|
+
/** Cardinality ceiling: keys beyond this collapse into `__overflow__`. */
|
|
51
|
+
export declare const DEFAULT_CACHE_TELEMETRY_CAP = 256;
|
|
52
|
+
/** Sample bucket key for provider/model keys beyond the cap. */
|
|
53
|
+
export declare const CACHE_TELEMETRY_OVERFLOW_KEY = "__overflow__";
|
|
54
|
+
export declare class CacheTelemetryError extends Error {
|
|
55
|
+
readonly code = "ERR_PRISM_CACHE_TELEMETRY";
|
|
56
|
+
constructor(message: string);
|
|
57
|
+
}
|
|
58
|
+
export declare function createCacheTelemetry(options?: CacheTelemetryOptions): CacheTelemetry;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { cacheHitRate, cacheSavings } from "./cache-helpers.js";
|
|
2
|
+
/** Cardinality ceiling: keys beyond this collapse into `__overflow__`. */
|
|
3
|
+
export const DEFAULT_CACHE_TELEMETRY_CAP = 256;
|
|
4
|
+
/** Sample bucket key for provider/model keys beyond the cap. */
|
|
5
|
+
export const CACHE_TELEMETRY_OVERFLOW_KEY = "__overflow__";
|
|
6
|
+
export class CacheTelemetryError extends Error {
|
|
7
|
+
code = "ERR_PRISM_CACHE_TELEMETRY";
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "CacheTelemetryError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function validateTokens(name, value) {
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
return;
|
|
16
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
17
|
+
throw new CacheTelemetryError(`${name} must be a non-negative safe integer, got ${value}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function sampleFor(usage, model) {
|
|
21
|
+
if (model)
|
|
22
|
+
return { provider: model.provider, model: model.model };
|
|
23
|
+
// Provider-only aggregation: no model supplied, attribute to the unknown bucket.
|
|
24
|
+
return { provider: "unknown", model: "unknown" };
|
|
25
|
+
}
|
|
26
|
+
export function createCacheTelemetry(options = {}) {
|
|
27
|
+
const maxKeys = options.maxKeys ?? DEFAULT_CACHE_TELEMETRY_CAP;
|
|
28
|
+
if (!Number.isSafeInteger(maxKeys) || maxKeys < 1) {
|
|
29
|
+
throw new CacheTelemetryError(`maxKeys must be a positive safe integer, got ${options.maxKeys}`);
|
|
30
|
+
}
|
|
31
|
+
// ponytail: fixed per-provider/model key cap with a single __overflow__ bucket;
|
|
32
|
+
// if real deployments exceed it, upgrade to host-configurable caps or LRU eviction.
|
|
33
|
+
const samples = new Map();
|
|
34
|
+
let overflow = false;
|
|
35
|
+
let overflowSample;
|
|
36
|
+
function bucket(provider, model) {
|
|
37
|
+
const key = `${provider}\u0000${model}`;
|
|
38
|
+
let sample = samples.get(key);
|
|
39
|
+
if (sample)
|
|
40
|
+
return sample;
|
|
41
|
+
if (samples.size >= maxKeys) {
|
|
42
|
+
overflow = true;
|
|
43
|
+
if (!overflowSample) {
|
|
44
|
+
overflowSample = {
|
|
45
|
+
provider: CACHE_TELEMETRY_OVERFLOW_KEY,
|
|
46
|
+
model: CACHE_TELEMETRY_OVERFLOW_KEY,
|
|
47
|
+
requests: 0,
|
|
48
|
+
cacheReadTokens: 0,
|
|
49
|
+
cacheWriteTokens: 0,
|
|
50
|
+
inputTokens: 0,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return overflowSample;
|
|
54
|
+
}
|
|
55
|
+
sample = { provider, model, requests: 0, cacheReadTokens: 0, cacheWriteTokens: 0, inputTokens: 0 };
|
|
56
|
+
samples.set(key, sample);
|
|
57
|
+
return sample;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
record(usage, model) {
|
|
61
|
+
// Validate everything before mutating: a bad record mutates nothing.
|
|
62
|
+
validateTokens("usage.cacheReadTokens", usage.cacheReadTokens);
|
|
63
|
+
validateTokens("usage.cacheWriteTokens", usage.cacheWriteTokens);
|
|
64
|
+
validateTokens("usage.inputTokens", usage.inputTokens);
|
|
65
|
+
const { provider, model: modelName } = sampleFor(usage, model);
|
|
66
|
+
const sample = bucket(provider, modelName);
|
|
67
|
+
sample.requests += 1;
|
|
68
|
+
sample.cacheReadTokens += usage.cacheReadTokens ?? 0;
|
|
69
|
+
sample.cacheWriteTokens += usage.cacheWriteTokens ?? 0;
|
|
70
|
+
sample.inputTokens += usage.inputTokens ?? 0;
|
|
71
|
+
sample.hitRate = cacheHitRate({
|
|
72
|
+
cacheReadTokens: sample.cacheReadTokens,
|
|
73
|
+
inputTokens: sample.inputTokens,
|
|
74
|
+
});
|
|
75
|
+
if (model?.cost) {
|
|
76
|
+
// cacheSavings depends only on read tokens + cost metadata, so the
|
|
77
|
+
// aggregate equals the sum of per-call savings; feed it the totals
|
|
78
|
+
// to reuse the exact cache-helpers math rather than reimplementing it.
|
|
79
|
+
const savings = cacheSavings({ cacheReadTokens: sample.cacheReadTokens }, model);
|
|
80
|
+
sample.estimatedSavings = savings;
|
|
81
|
+
sample.currency = model.cost.currency;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
report() {
|
|
85
|
+
const samplesAll = [...samples.values()].sort((a, b) => a.provider === b.provider ? a.model.localeCompare(b.model) : a.provider.localeCompare(b.provider));
|
|
86
|
+
const listed = overflowSample ? [...samplesAll, overflowSample] : samplesAll;
|
|
87
|
+
const totalRequests = listed.reduce((sum, s) => sum + s.requests, 0);
|
|
88
|
+
const totalCacheReadTokens = listed.reduce((sum, s) => sum + s.cacheReadTokens, 0);
|
|
89
|
+
const totalCacheWriteTokens = listed.reduce((sum, s) => sum + s.cacheWriteTokens, 0);
|
|
90
|
+
return { samples: listed, overflowed: overflow, totalRequests, totalCacheReadTokens, totalCacheWriteTokens };
|
|
91
|
+
},
|
|
92
|
+
reset() {
|
|
93
|
+
samples.clear();
|
|
94
|
+
overflow = false;
|
|
95
|
+
overflowSample = undefined;
|
|
96
|
+
},
|
|
97
|
+
get size() {
|
|
98
|
+
return samples.size;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=cache-telemetry.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Writable } from "node:stream";
|
|
2
|
+
export declare class ProviderAddUsageError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export interface ProviderAddOptions {
|
|
5
|
+
/** npm-validated provider/package name; also the target directory name. */
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly baseUrl: string;
|
|
8
|
+
/** Shell-safe identifier, e.g. `ACME_API_KEY`. Never a secret value. */
|
|
9
|
+
readonly envKey: string;
|
|
10
|
+
readonly model: string;
|
|
11
|
+
readonly force: boolean;
|
|
12
|
+
readonly help: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ProviderAddRuntime {
|
|
15
|
+
readonly stdout: Writable;
|
|
16
|
+
readonly stderr: Writable;
|
|
17
|
+
/** Override template root (tests). Defaults to package `templates/provider`. */
|
|
18
|
+
readonly templatesRoot?: string;
|
|
19
|
+
/** Override package version stamped into the generated package.json. */
|
|
20
|
+
readonly packageVersion?: string;
|
|
21
|
+
/** Working directory used to resolve relative destinations. Defaults to process.cwd(). */
|
|
22
|
+
readonly cwd?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ProviderAddResult {
|
|
25
|
+
readonly targetDir: string;
|
|
26
|
+
readonly writtenFiles: readonly string[];
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly totalBytes: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function getProviderAddUsage(): string;
|
|
31
|
+
export declare const providerAddUsage: string;
|
|
32
|
+
export declare function parseProviderAddArgs(argv: readonly string[]): ProviderAddOptions;
|
|
33
|
+
export declare function runProviderAddCommand(argv: readonly string[], runtime: ProviderAddRuntime): Promise<number>;
|
|
34
|
+
export declare function createProviderProject(options: ProviderAddOptions, runtime?: ProviderAddRuntime): Promise<ProviderAddResult>;
|
|
35
|
+
export declare function defaultProviderTemplatesRoot(): string;
|
|
36
|
+
/** npm package-name rules plus traversal refusal. Throws `ProviderAddUsageError` on violation. */
|
|
37
|
+
export declare function validateProviderName(name: string): void;
|
|
@@ -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
|
package/dist/cli-runner.d.ts
CHANGED
|
@@ -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>;
|
package/dist/cli-runner.js
CHANGED
|
@@ -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 {
|
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";
|
|
@@ -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.
|
|
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";
|
|
@@ -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.
|
|
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
|
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.
|