create-quorum-router 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.
package/README.md CHANGED
@@ -13,9 +13,10 @@ cd my-quorum-router-demo
13
13
  deno --version
14
14
  deno task smoke
15
15
  deno task intake
16
+ deno task supabase:status
16
17
  ```
17
18
 
18
- Current package version: `create-quorum-router@0.1.5`. Releases are published
19
+ Current package version: `create-quorum-router@0.1.7`. Releases are published
19
20
  from an immutable Git tag through GitHub Actions OIDC Trusted Publishing.
20
21
 
21
22
  ## What the generated project supports
@@ -63,6 +64,19 @@ preferred public dogfood path.
63
64
  Do not commit `.env`, `router.config.local.json`, `.quorum-router/`, or `out/`.
64
65
  Do not paste tokens into chat/logs.
65
66
 
67
+ ## Optional user-owned Supabase audit
68
+
69
+ The generated project works without Supabase. Audit defaults to `disabled`.
70
+ Users who opt in apply the generated migration to a Supabase project they own,
71
+ choose `optional` or `required` in the non-secret feature config, inject a
72
+ project URL, publishable/anon key, and Supabase Auth session JWT at runtime, and
73
+ run `deno task supabase:status` before routing.
74
+
75
+ The runtime rejects service-role/admin credentials. Audit records exclude
76
+ prompts, model responses, credentials, client-supplied actor/org identity, and
77
+ client timestamps. Agent Bus, Realtime, state sync, and analytics dashboards are
78
+ not part of this integration.
79
+
66
80
  ## Runtime boundaries
67
81
 
68
82
  - Best Route/direct is production-ready best-answer routing.
@@ -71,7 +85,7 @@ Do not paste tokens into chat/logs.
71
85
  slice only when separately configured with signed policy and distinct
72
86
  approval.
73
87
  - The generated scaffold does not enable mutation by default.
74
- - No service-role runtime.
88
+ - No service-role/admin runtime credentials.
75
89
  - No live Supabase Agent Bus runtime writes.
76
90
  - Public launch requires the repository verification, package tarball, registry
77
91
  readback, and clean-room NPX scaffold checks to pass.
@@ -3,7 +3,7 @@ const fs = require("fs");
3
3
  const path = require("path");
4
4
  const { spawnSync } = require("child_process");
5
5
 
6
- const VERSION = "0.1.5";
6
+ const VERSION = "0.1.7";
7
7
  const SUPPORTED_TEMPLATES = new Set(["basic"]);
8
8
 
9
9
  function usage() {
@@ -56,13 +56,33 @@ function parseArgs(argv) {
56
56
  return options;
57
57
  }
58
58
 
59
- function copyRecursive(from, to) {
59
+ function rejectSymlink(destination) {
60
+ try {
61
+ if (fs.lstatSync(destination).isSymbolicLink()) {
62
+ throw new Error(`refusing to write through symlink: ${destination}`);
63
+ }
64
+ } catch (error) {
65
+ if (error && error.code === "ENOENT") return;
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ function copyRecursive(from, to, targetRoot) {
71
+ const relative = path.relative(targetRoot, to);
72
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
73
+ throw new Error(`refusing to write outside target directory: ${to}`);
74
+ }
75
+ rejectSymlink(to);
60
76
  const stat = fs.statSync(from);
61
77
  if (stat.isDirectory()) {
62
78
  fs.mkdirSync(to, { recursive: true });
63
79
  for (const entry of fs.readdirSync(from)) {
64
80
  const targetEntry = entry === "gitignore" ? ".gitignore" : entry;
65
- copyRecursive(path.join(from, entry), path.join(to, targetEntry));
81
+ copyRecursive(
82
+ path.join(from, entry),
83
+ path.join(to, targetEntry),
84
+ targetRoot,
85
+ );
66
86
  }
67
87
  return;
68
88
  }
@@ -97,6 +117,7 @@ function main() {
97
117
  }
98
118
 
99
119
  const targetDir = path.resolve(process.cwd(), parsed.dir);
120
+ rejectSymlink(targetDir);
100
121
  if (isNonEmptyDirectory(targetDir) && !parsed.force) {
101
122
  throw new Error(
102
123
  `refusing to overwrite non-empty directory: ${targetDir}\n` +
@@ -106,7 +127,7 @@ function main() {
106
127
 
107
128
  const templateDir = path.join(__dirname, "..", "templates", parsed.template);
108
129
  fs.mkdirSync(targetDir, { recursive: true });
109
- copyRecursive(templateDir, targetDir);
130
+ copyRecursive(templateDir, targetDir, targetDir);
110
131
  fs.mkdirSync(path.join(targetDir, "out"), { recursive: true });
111
132
  fs.writeFileSync(path.join(targetDir, "out", ".gitkeep"), "");
112
133
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-quorum-router",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Create a local QuorumRouter project.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  # QuorumRouter generated workspace
2
2
 
3
3
  This generated workspace contains the MIT-licensed QuorumRouter current release.
4
- npm latest targets v0.1.5.
4
+ npm latest targets v0.1.7.
5
5
 
6
6
  QuorumRouter is **MIT**. It is **open source**. Commercial and production use
7
7
  are permitted under the MIT License.
@@ -19,8 +19,10 @@ are permitted under the MIT License.
19
19
  - Conversation-only `agent_chat` is explicit opt-in.
20
20
  - SafeLoop-backed production repository execution is not enabled by this
21
21
  generated scaffold; it requires external signed policy and distinct approval.
22
- - No service-role runtime.
23
- - No live Supabase runtime writes.
22
+ - No service-role runtime; service-role/admin credentials are rejected even when
23
+ audit is disabled.
24
+ - BYO Supabase audit is disabled by default and writes only when explicitly set
25
+ to `optional` or `required`.
24
26
  - No live Supabase Agent Bus runtime writes.
25
27
  - Best Route/direct is the production-ready best-answer routing path.
26
28
  - `agent-chat` is read-only explicit opt-in only.
@@ -39,6 +41,7 @@ deno task intake
39
41
  deno task auth:status
40
42
  deno task models:list
41
43
  deno task health
44
+ deno task supabase:status
42
45
  ```
43
46
 
44
47
  `smoke` proves the local scaffold runs with deterministic fixtures only. It does
@@ -51,6 +54,37 @@ under `out/`, and recommends the next command.
51
54
  In short: intake is the first real setup command before `route:once`,
52
55
  `best-route`, or read-only `agent-chat`.
53
56
 
57
+ ## Optional BYO Supabase audit
58
+
59
+ Supabase is not required. With no Supabase config or env, every existing task
60
+ keeps its prior behavior and `deno task supabase:status` exits successfully with
61
+ `state: disabled` without making a network request.
62
+
63
+ To persist route outcomes in a Supabase project you own:
64
+
65
+ 1. Create or choose your Supabase project.
66
+ 2. Apply **both** files under `supabase/migrations/` in filename order using an
67
+ admin context outside the router runtime. The later limits migration is
68
+ mandatory.
69
+ 3. Ensure the active Supabase Auth user session JWT has an authenticated `sub`.
70
+ 4. Copy `router.config.example.json` to `router.config.json` and set only
71
+ `features.supabase.audit.mode` to `optional` or `required`.
72
+ 5. Inject `QUORUM_ROUTER_SUPABASE_URL`, `QUORUM_ROUTER_SUPABASE_ANON_KEY`, and
73
+ `QUORUM_ROUTER_SUPABASE_SESSION_JWT` at runtime.
74
+ 6. Run `deno task supabase:status`, then run `route:once` or `best-route`.
75
+
76
+ `optional` preserves a successful route and prints a warning when audit delivery
77
+ fails. `required` withholds the route result and exits nonzero when audit
78
+ delivery fails. The RPC receives only the route decision and bounded metadata.
79
+ It never receives prompts, model responses, credentials, `org_id`, `actor_id`,
80
+ or `created_at`; the database derives both the actor and this single-user BYO
81
+ audit namespace from `auth.uid()` and owns the timestamp. There is no central or
82
+ shared database and no client tenant claim.
83
+
84
+ Runtime service-role/admin credentials are forbidden. Agent Bus, Realtime
85
+ wakeup, state sync, and an analytics dashboard are future features and are not
86
+ enabled by this audit integration.
87
+
54
88
  ## Real provider dogfood commands
55
89
 
56
90
  Run only after `intake` reports a usable OAuth/session/wrapper provider:
@@ -87,6 +121,25 @@ Behavior:
87
121
  Only use this with repositories you are allowed to send to the selected
88
122
  provider.
89
123
 
124
+ ### Cost-aware Best Route
125
+
126
+ Cost-aware routing is disabled unless both budget variables are set. Estimates
127
+ are user-supplied per invocation, not live provider billing data.
128
+
129
+ ```bash
130
+ QUORUM_ROUTER_MAX_BUDGET_USD=0.05 \
131
+ QUORUM_ROUTER_ESTIMATED_COSTS_JSON='{"openai/gpt-5":0.03,"xai/grok":0.02}' \
132
+ RUN_EXTERNAL_MODEL_DOGFOOD=1 \
133
+ deno task best-route --prompt "Choose the safest launch copy."
134
+ ```
135
+
136
+ Best Route preserves candidate quality/readiness order, admits candidates while
137
+ their configured estimates fit the budget, and records selected and excluded
138
+ model IDs plus reasons in `out/best-route-trace.json`. Models without an
139
+ estimate are excluded when cost-aware routing is enabled. If no candidate fits,
140
+ the command fails before invoking a provider. This is pre-invocation budget
141
+ control; it does not claim exact or live API spend.
142
+
90
143
  ### Forced wrapper provider/model selection
91
144
 
92
145
  Use these only when you want a specific local wrapper/model. The scaffold fails
@@ -152,17 +205,22 @@ not paste them into chat/logs and do not commit `.env`.
152
205
  - `.gitignore` — excludes `.env`, `out/`, `router.config.local.json`,
153
206
  `provider_config.json`, and `.quorum-router/`.
154
207
  - `router.config.example.json` — non-secret example boundaries.
208
+ - `supabase/migrations/20260701130000_workflow_access_audit.sql` — optional BYO
209
+ Supabase audit migration
210
+ - `supabase/migrations/20260712211500_workflow_access_audit_limits.sql` —
211
+ required RPC narrowing and payload-limit migration
155
212
  - `src/cli.ts` — command dispatcher.
213
+ - `src/supabase.ts` — offline status and selective audit RPC hook.
156
214
  - `src/intake.ts` — first-run onboarding.
157
215
  - `src/auth.ts`, `src/auth_oauth.ts`, `src/auth_session.ts`,
158
216
  `src/auth_env_fallback.ts` — auth/session/fallback boundaries.
159
217
  - `src/provider_registry.ts`, `src/model_inventory.ts`, `src/wrapper_client.ts`,
160
218
  `src/provider_client.ts` — provider discovery and safe invocation.
161
219
  - `src/best_route.ts`, `src/agent_chat.ts` — gated dogfood commands.
220
+ - `src/cost_aware.ts` — estimated-cost budget selection for Best Route.
162
221
  - `src/trace.ts`, `src/redact.ts`, `src/schema.ts`, `src/fixture_smoke.ts` —
163
222
  trace/redaction/schema/fixture support.
164
223
  - `out/.gitkeep` — local output directory placeholder.
165
224
 
166
- Public Product Hunt/X launch remains blocked until the user personally runs a
167
- local pre-release workspace and approves release continuation. This scaffold
168
- does not publish npm, create a GitHub release, or mutate tags/dist-tags.
225
+ This generated scaffold operates locally. It does not publish npm packages,
226
+ create GitHub releases, or mutate tags or dist-tags.
@@ -12,6 +12,7 @@
12
12
  "auth:logout": "deno run --allow-read --allow-write --allow-env src/cli.ts auth:logout",
13
13
  "models:list": "deno run --allow-read --allow-write --allow-env --allow-run=grok,codex,claude,gemini,qwen,devin src/cli.ts models:list",
14
14
  "health": "deno run --allow-read --allow-write --allow-env --allow-run=grok,codex,claude,gemini,qwen,devin src/cli.ts health",
15
+ "supabase:status": "deno run --allow-read --allow-env src/cli.ts supabase:status",
15
16
  "route:once": "deno run --allow-read --allow-write --allow-env --allow-net --allow-run=grok,codex,claude,gemini,qwen,devin src/cli.ts route:once",
16
17
  "best-route": "deno run --allow-read --allow-write --allow-env --allow-net --allow-run=grok,codex,claude,gemini,qwen,devin src/cli.ts best-route",
17
18
  "agent-chat": "deno run --allow-read --allow-write --allow-env --allow-net --allow-run=grok,codex,claude,gemini,qwen,devin src/cli.ts agent-chat"
@@ -5,6 +5,17 @@
5
5
  "enabled_by": "QUORUM_ROUTER_AUTH_MODE=env",
6
6
  "usage": "private manual fallback only"
7
7
  },
8
+ "features": {
9
+ "supabase": {
10
+ "audit": {
11
+ "mode": "disabled"
12
+ }
13
+ }
14
+ },
15
+ "cost_aware": {
16
+ "enabled_by": "QUORUM_ROUTER_MAX_BUDGET_USD + QUORUM_ROUTER_ESTIMATED_COSTS_JSON",
17
+ "pricing_source": "user-supplied estimates, not live billing"
18
+ },
8
19
  "runtime_boundaries": {
9
20
  "best_route_direct": "production-ready best-answer routing",
10
21
  "agent_chat": "experimental explicit opt-in only",
@@ -88,6 +88,7 @@ export function discoverInventory(
88
88
  can_invoke: exists,
89
89
  command: spec.command,
90
90
  args_template: spec.args_template,
91
+ prompt_transport: spec.prompt_transport,
91
92
  notes: exists
92
93
  ? [
93
94
  ...spec.notes,
@@ -21,6 +21,7 @@ import {
21
21
  import { buildTrace, score, writeTrace } from "./trace.ts";
22
22
  import { callWrapper } from "./wrapper_client.ts";
23
23
  import { preparePromptWithContext } from "./context.ts";
24
+ import { selectCandidatesWithinBudget } from "./cost_aware.ts";
24
25
 
25
26
  function hasExplicitSelection(request: ProviderSelectionRequest): boolean {
26
27
  return Boolean(
@@ -261,7 +262,12 @@ export async function runBestRoute(
261
262
  > {
262
263
  assertOptIn();
263
264
  const authMode = parseAuthMode(readRouterEnv("QUORUM_ROUTER_AUTH_MODE"));
264
- const { request, candidates } = await discoverCandidates(authMode);
265
+ const { request, candidates: discoveredCandidates } =
266
+ await discoverCandidates(
267
+ authMode,
268
+ );
269
+ const costAwareDecision = selectCandidatesWithinBudget(discoveredCandidates);
270
+ const candidates = costAwareDecision.candidates;
265
271
  if (candidates.length === 0) {
266
272
  throw new Error(
267
273
  "OAuth/session-first provider unavailable. best-route has no usable OAuth/session/wrapper provider yet. Next: deno task auth:login",
@@ -316,6 +322,9 @@ export async function runBestRoute(
316
322
  selectionHonored(request, result)
317
323
  ),
318
324
  fallbackUsed: usedEnvFallback,
325
+ costAware: costAwareDecision.cost.enabled
326
+ ? costAwareDecision.cost
327
+ : undefined,
319
328
  });
320
329
  const tracePath = await writeTrace("best-route-trace", trace);
321
330
  return { results, tracePath, trace };
@@ -11,6 +11,11 @@ import {
11
11
  } from "./model_inventory.ts";
12
12
  import { redact, summarize } from "./redact.ts";
13
13
  import { parseAuthMode } from "./schema.ts";
14
+ import {
15
+ auditRouteOutcome,
16
+ preflightRequiredSupabaseAudit,
17
+ runSupabaseStatus,
18
+ } from "./supabase.ts";
14
19
  import { buildTrace, writeTrace } from "./trace.ts";
15
20
 
16
21
  const DEFAULT_PROMPT = "Review this README for risky claims.";
@@ -22,6 +27,7 @@ type CommandName =
22
27
  | "auth:logout"
23
28
  | "models:list"
24
29
  | "health"
30
+ | "supabase:status"
25
31
  | "route:once"
26
32
  | "best-route"
27
33
  | "agent-chat";
@@ -32,11 +38,12 @@ function commandName(): CommandName {
32
38
  command === "intake" || command === "auth:status" ||
33
39
  command === "auth:login" || command === "auth:logout" ||
34
40
  command === "models:list" || command === "health" ||
41
+ command === "supabase:status" ||
35
42
  command === "route:once" || command === "best-route" ||
36
43
  command === "agent-chat"
37
44
  ) return command;
38
45
  throw new Error(
39
- "usage: deno task intake|auth:status|auth:login|auth:logout|models:list|health|route:once|best-route|agent-chat",
46
+ "usage: deno task intake|auth:status|auth:login|auth:logout|models:list|health|supabase:status|route:once|best-route|agent-chat",
40
47
  );
41
48
  }
42
49
 
@@ -101,10 +108,13 @@ try {
101
108
  else if (command === "auth:logout") await runAuthLogout();
102
109
  else if (command === "models:list") await runModelsList();
103
110
  else if (command === "health") await runHealth();
111
+ else if (command === "supabase:status") await runSupabaseStatus();
104
112
  else if (command === "route:once") {
113
+ await preflightRequiredSupabaseAudit();
105
114
  const { results, tracePath, trace } = await invokeSelected(
106
115
  promptFromArgs(),
107
116
  );
117
+ await auditRouteOutcome(trace);
108
118
  console.log("QuorumRouter route:once");
109
119
  console.log(`provider: ${results[0].provider}`);
110
120
  console.log(`model: ${results[0].model}`);
@@ -116,7 +126,9 @@ try {
116
126
  console.log(`final: ${summarize(results[0].response_summary, 500)}`);
117
127
  console.log(`trace: ${tracePath}`);
118
128
  } else if (command === "best-route") {
119
- const { results, tracePath } = await runBestRoute(promptFromArgs());
129
+ await preflightRequiredSupabaseAudit();
130
+ const { results, tracePath, trace } = await runBestRoute(promptFromArgs());
131
+ await auditRouteOutcome(trace);
120
132
  console.log("QuorumRouter best-route");
121
133
  console.log(`models_called: ${results.length}`);
122
134
  console.log(`trace: ${tracePath}`);
@@ -0,0 +1,150 @@
1
+ import { readRouterEnv } from "./env.ts";
2
+ import type { CostAwareTrace, ModelInventoryEntry } from "./schema.ts";
3
+
4
+ export type CostExclusion = {
5
+ model_id: string;
6
+ estimated_cost_usd?: number;
7
+ reason: "missing_estimate" | "budget_exceeded";
8
+ };
9
+
10
+ export type CostAwareSelection =
11
+ | CostAwareTrace
12
+ | {
13
+ enabled: false;
14
+ selected_model_ids: string[];
15
+ excluded: CostExclusion[];
16
+ };
17
+
18
+ export type CostAwareConfig = {
19
+ maxBudgetUsd: number;
20
+ estimates: Map<string, number>;
21
+ };
22
+
23
+ function normalizedModelId(value: string): string {
24
+ return value.trim().toLowerCase();
25
+ }
26
+
27
+ function parsePositiveBudget(raw: string): number {
28
+ const value = Number(raw);
29
+ if (!Number.isFinite(value) || value <= 0) {
30
+ throw new Error(
31
+ "QuorumRouter blocked: QUORUM_ROUTER_MAX_BUDGET_USD must be a finite number > 0",
32
+ );
33
+ }
34
+ return value;
35
+ }
36
+
37
+ function parseEstimates(raw: string): Map<string, number> {
38
+ let parsed: unknown;
39
+ try {
40
+ parsed = JSON.parse(raw);
41
+ } catch {
42
+ throw new Error(
43
+ "QuorumRouter blocked: QUORUM_ROUTER_ESTIMATED_COSTS_JSON must be valid JSON",
44
+ );
45
+ }
46
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
47
+ throw new Error(
48
+ "QuorumRouter blocked: QUORUM_ROUTER_ESTIMATED_COSTS_JSON must be a model-id to USD object",
49
+ );
50
+ }
51
+
52
+ const estimates = new Map<string, number>();
53
+ for (const [rawModelId, rawCost] of Object.entries(parsed)) {
54
+ const modelId = normalizedModelId(rawModelId);
55
+ if (
56
+ !modelId || typeof rawCost !== "number" || !Number.isFinite(rawCost) ||
57
+ rawCost < 0
58
+ ) {
59
+ throw new Error(
60
+ "QuorumRouter blocked: every cost estimate must use a non-empty model id and a finite USD number >= 0",
61
+ );
62
+ }
63
+ if (estimates.has(modelId)) {
64
+ throw new Error(
65
+ `QuorumRouter blocked: duplicate normalized cost estimate for ${modelId}`,
66
+ );
67
+ }
68
+ estimates.set(modelId, rawCost);
69
+ }
70
+ if (estimates.size === 0) {
71
+ throw new Error(
72
+ "QuorumRouter blocked: QUORUM_ROUTER_ESTIMATED_COSTS_JSON must contain at least one estimate",
73
+ );
74
+ }
75
+ return estimates;
76
+ }
77
+
78
+ export function readCostAwareConfig(): CostAwareConfig | undefined {
79
+ const rawBudget = readRouterEnv("QUORUM_ROUTER_MAX_BUDGET_USD");
80
+ const rawEstimates = readRouterEnv("QUORUM_ROUTER_ESTIMATED_COSTS_JSON");
81
+ if (rawBudget === undefined && rawEstimates === undefined) return undefined;
82
+ if (rawBudget === undefined || rawEstimates === undefined) {
83
+ throw new Error(
84
+ "QuorumRouter blocked: cost-aware routing requires both QUORUM_ROUTER_MAX_BUDGET_USD and QUORUM_ROUTER_ESTIMATED_COSTS_JSON",
85
+ );
86
+ }
87
+ return {
88
+ maxBudgetUsd: parsePositiveBudget(rawBudget),
89
+ estimates: parseEstimates(rawEstimates),
90
+ };
91
+ }
92
+
93
+ export function selectCandidatesWithinBudget(
94
+ candidates: ModelInventoryEntry[],
95
+ config = readCostAwareConfig(),
96
+ ): { candidates: ModelInventoryEntry[]; cost: CostAwareSelection } {
97
+ if (!config) {
98
+ return {
99
+ candidates,
100
+ cost: {
101
+ enabled: false,
102
+ selected_model_ids: candidates.map((candidate) => candidate.model_id),
103
+ excluded: [],
104
+ },
105
+ };
106
+ }
107
+
108
+ const selected: ModelInventoryEntry[] = [];
109
+ const excluded: CostExclusion[] = [];
110
+ let estimatedTotalUsd = 0;
111
+ for (const candidate of candidates) {
112
+ const estimate = config.estimates.get(
113
+ normalizedModelId(candidate.model_id),
114
+ );
115
+ if (estimate === undefined) {
116
+ excluded.push({
117
+ model_id: candidate.model_id,
118
+ reason: "missing_estimate",
119
+ });
120
+ continue;
121
+ }
122
+ if (estimatedTotalUsd + estimate > config.maxBudgetUsd) {
123
+ excluded.push({
124
+ model_id: candidate.model_id,
125
+ estimated_cost_usd: estimate,
126
+ reason: "budget_exceeded",
127
+ });
128
+ continue;
129
+ }
130
+ selected.push(candidate);
131
+ estimatedTotalUsd += estimate;
132
+ }
133
+
134
+ if (selected.length === 0) {
135
+ throw new Error(
136
+ "QuorumRouter blocked: cost-aware routing selected no candidates within the configured budget",
137
+ );
138
+ }
139
+ return {
140
+ candidates: selected,
141
+ cost: {
142
+ enabled: true,
143
+ pricing_source: "configured_estimate_not_live_billing",
144
+ max_budget_usd: config.maxBudgetUsd,
145
+ estimated_total_usd: estimatedTotalUsd,
146
+ selected_model_ids: selected.map((candidate) => candidate.model_id),
147
+ excluded,
148
+ },
149
+ };
150
+ }
@@ -9,6 +9,7 @@ export type ProviderSpec = {
9
9
  source: "wrapper" | "oauth_session" | "local_cli" | "env_fallback";
10
10
  command?: string;
11
11
  args_template?: string[];
12
+ prompt_transport?: "stdin" | "file";
12
13
  list_models_args?: string[];
13
14
  list_blocked_reason?: string;
14
15
  notes: string[];
@@ -145,8 +146,9 @@ export const LOCAL_PROVIDER_SPECS: ProviderSpec[] = [
145
146
  "__CWD__",
146
147
  "--output-last-message",
147
148
  "__OUT__",
148
- "__PROMPT__",
149
+ "-",
149
150
  ],
151
+ prompt_transport: "stdin",
150
152
  list_blocked_reason:
151
153
  "codex models is tty/stdin dependent in this environment; model catalog listing is unavailable from non-interactive Deno.",
152
154
  notes: [
@@ -160,7 +162,8 @@ export const LOCAL_PROVIDER_SPECS: ProviderSpec[] = [
160
162
  auth_mode: "oauth",
161
163
  source: "oauth_session",
162
164
  command: "claude",
163
- args_template: ["-p", "__PROMPT__"],
165
+ args_template: ["-p"],
166
+ prompt_transport: "stdin",
164
167
  list_blocked_reason:
165
168
  "Claude Code model listing is blocked by organization policy / disabled subscription access in this environment.",
166
169
  notes: [
@@ -174,7 +177,8 @@ export const LOCAL_PROVIDER_SPECS: ProviderSpec[] = [
174
177
  auth_mode: "oauth",
175
178
  source: "oauth_session",
176
179
  command: "gemini",
177
- args_template: ["-p", "__PROMPT__"],
180
+ args_template: ["-p", ""],
181
+ prompt_transport: "stdin",
178
182
  list_blocked_reason:
179
183
  "Gemini CLI list/invoke paths require a trusted directory in non-interactive runs unless the user opts into the trust setting.",
180
184
  notes: [
@@ -189,14 +193,15 @@ export const LOCAL_PROVIDER_SPECS: ProviderSpec[] = [
189
193
  source: "oauth_session",
190
194
  command: "grok",
191
195
  args_template: [
192
- "-p",
193
- "__PROMPT__",
196
+ "--prompt-file",
197
+ "__PROMPT_FILE__",
194
198
  "--output-format",
195
199
  "plain",
196
200
  "--permission-mode",
197
201
  "plan",
198
202
  "--disable-web-search",
199
203
  ],
204
+ prompt_transport: "file",
200
205
  list_models_args: ["models"],
201
206
  notes: [
202
207
  "Uses existing Grok CLI session; `grok models` is safe list-only discovery.",
@@ -209,7 +214,8 @@ export const LOCAL_PROVIDER_SPECS: ProviderSpec[] = [
209
214
  auth_mode: "session",
210
215
  source: "local_cli",
211
216
  command: "devin",
212
- args_template: ["-p", "__PROMPT__"],
217
+ args_template: ["-p", "--prompt-file", "__PROMPT_FILE__"],
218
+ prompt_transport: "file",
213
219
  list_blocked_reason:
214
220
  "Devin CLI does not expose a models subcommand in this environment.",
215
221
  notes: ["Uses existing Devin CLI session."],
@@ -221,7 +227,8 @@ export const LOCAL_PROVIDER_SPECS: ProviderSpec[] = [
221
227
  auth_mode: "session",
222
228
  source: "local_cli",
223
229
  command: "qwen",
224
- args_template: ["-p", "__PROMPT__"],
230
+ args_template: ["-p", ""],
231
+ prompt_transport: "stdin",
225
232
  list_blocked_reason:
226
233
  "Qwen CLI model listing path is stdin/API-key dependent in this environment.",
227
234
  notes: ["Uses existing local Qwen CLI session."],