@orangepro/orangepro-mcp 0.1.0

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 (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -0
  3. package/dist/local/agentWorkflow.js +81 -0
  4. package/dist/local/aiGraph/links.js +635 -0
  5. package/dist/local/analyze/analyzer.js +2129 -0
  6. package/dist/local/analyze/behaviorContracts.js +169 -0
  7. package/dist/local/analyze/boilerplate.js +42 -0
  8. package/dist/local/analyze/callGraph.js +458 -0
  9. package/dist/local/analyze/classify.js +219 -0
  10. package/dist/local/analyze/clustering.js +357 -0
  11. package/dist/local/analyze/confirm.js +2422 -0
  12. package/dist/local/analyze/coverage.js +518 -0
  13. package/dist/local/analyze/coverageArtifacts.js +607 -0
  14. package/dist/local/analyze/frameworks.js +115 -0
  15. package/dist/local/analyze/linkage/conventions.js +160 -0
  16. package/dist/local/analyze/parseCache.js +164 -0
  17. package/dist/local/analyze/selfAssert.js +53 -0
  18. package/dist/local/analyze/symbols.js +430 -0
  19. package/dist/local/analyze/testLayer.js +135 -0
  20. package/dist/local/analyze/treeSitter/engine.js +1253 -0
  21. package/dist/local/analyze/treeSitter/languages.js +101 -0
  22. package/dist/local/autoProve.js +620 -0
  23. package/dist/local/cli.js +1468 -0
  24. package/dist/local/cliArgs.js +112 -0
  25. package/dist/local/corpusScope.js +162 -0
  26. package/dist/local/enrich/csv.js +348 -0
  27. package/dist/local/enrich/index.js +43 -0
  28. package/dist/local/enrich/markdown.js +193 -0
  29. package/dist/local/explain/explain.js +91 -0
  30. package/dist/local/exportCli.js +26 -0
  31. package/dist/local/flows/flowWalker.js +215 -0
  32. package/dist/local/flows/llmFlowDiscovery.js +567 -0
  33. package/dist/local/freshness/changed.js +280 -0
  34. package/dist/local/freshness/manifest.js +35 -0
  35. package/dist/local/freshness/status.js +30 -0
  36. package/dist/local/gaps/gaps.js +114 -0
  37. package/dist/local/generate/buckets.js +73 -0
  38. package/dist/local/generate/compareJudge.js +124 -0
  39. package/dist/local/generate/compareReport.js +538 -0
  40. package/dist/local/generate/compareScore.js +105 -0
  41. package/dist/local/generate/deriveImports.js +91 -0
  42. package/dist/local/generate/generator.js +2586 -0
  43. package/dist/local/generate/prompt.js +144 -0
  44. package/dist/local/generate/promptV5.js +438 -0
  45. package/dist/local/generate/providers.js +400 -0
  46. package/dist/local/generate/runHints.js +304 -0
  47. package/dist/local/graph/citations.js +73 -0
  48. package/dist/local/graph/confirmable.js +72 -0
  49. package/dist/local/graph/factories.js +210 -0
  50. package/dist/local/graph/ontology.js +18 -0
  51. package/dist/local/interactive.js +53 -0
  52. package/dist/local/jobs/jobStore.js +80 -0
  53. package/dist/local/jobs/notify.js +29 -0
  54. package/dist/local/jobs/runner.js +75 -0
  55. package/dist/local/ledger.js +117 -0
  56. package/dist/local/localConfig.js +112 -0
  57. package/dist/local/mcp.js +548 -0
  58. package/dist/local/operations.js +1749 -0
  59. package/dist/local/pack/coverageReport.js +192 -0
  60. package/dist/local/pack/exporter.js +195 -0
  61. package/dist/local/pack/schema.js +128 -0
  62. package/dist/local/pack/summary.js +127 -0
  63. package/dist/local/pack/validate.js +25 -0
  64. package/dist/local/proofRunnability.js +366 -0
  65. package/dist/local/recipe/dbSqljs.js +255 -0
  66. package/dist/local/reprove/paths.js +13 -0
  67. package/dist/local/reprove/scoped.js +136 -0
  68. package/dist/local/resolve/barrelWalker.js +178 -0
  69. package/dist/local/resolve/exportIndex.js +270 -0
  70. package/dist/local/resolve/importGraph.js +347 -0
  71. package/dist/local/resolve/resolver.js +122 -0
  72. package/dist/local/resolve/resolverCache.js +117 -0
  73. package/dist/local/rtm.js +413 -0
  74. package/dist/local/score/coverage.js +99 -0
  75. package/dist/local/score/doctor.js +67 -0
  76. package/dist/local/score/risk.js +362 -0
  77. package/dist/local/score/score.js +182 -0
  78. package/dist/local/types.js +1 -0
  79. package/dist/local/util/hash.js +16 -0
  80. package/dist/local/util/ids.js +16 -0
  81. package/dist/local/util/progress.js +8 -0
  82. package/dist/local/util/redact.js +39 -0
  83. package/dist/local/util/time.js +1 -0
  84. package/dist/local/util/walk.js +174 -0
  85. package/dist/local/viz/behaviorReportData.js +367 -0
  86. package/dist/local/viz/behaviorReportHtml.js +664 -0
  87. package/dist/local/viz/d3.bundle.js +3 -0
  88. package/dist/local/viz/html.js +1152 -0
  89. package/dist/local/viz/payload.js +525 -0
  90. package/dist/local/workspace.js +99 -0
  91. package/docs/agent-workflow.md +167 -0
  92. package/docs/agents/claude-code.md +43 -0
  93. package/docs/agents/codex.md +52 -0
  94. package/docs/agents/cursor.md +39 -0
  95. package/docs/agents/opencode.md +43 -0
  96. package/docs/agents/vscode.md +34 -0
  97. package/docs/local-proof-kit.md +269 -0
  98. package/package.json +92 -0
  99. package/scripts/spikes/dynamic-proof-jest-reporter.cjs +66 -0
  100. package/scripts/spikes/dynamic-proof-mocha-reporter.cjs +105 -0
  101. package/scripts/spikes/dynamic-proof-spike.mjs +2335 -0
  102. package/scripts/spikes/dynamic-proof-vitest-reporter.mjs +81 -0
  103. package/scripts/spikes/failure-summary.mjs +29 -0
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Curated, current known-good models per provider. NOT exhaustive — a custom id
3
+ * is always selectable, and the provider adapters self-correct request params,
4
+ * so newer reasoning models (e.g. gpt-5*, o-series) work even when not listed.
5
+ */
6
+ export const SUPPORTED_MODELS = {
7
+ openai: [
8
+ { model: "gpt-4.1", note: "strong general (recommended)" },
9
+ { model: "gpt-4o", note: "fast, multimodal" },
10
+ { model: "gpt-5", note: "reasoning — uses max_completion_tokens" },
11
+ { model: "gpt-4.1-mini", note: "cheap — smoke tests only" }
12
+ ],
13
+ anthropic: [
14
+ { model: "claude-sonnet-4-6", note: "recommended" },
15
+ { model: "claude-opus-4-8", note: "deepest reasoning" },
16
+ { model: "claude-haiku-4-5", note: "fast, cheap" }
17
+ ],
18
+ ollama: [
19
+ { model: "qwen2.5-coder:32b", note: "strong local coder" },
20
+ { model: "llama3.1", note: "general local" }
21
+ ]
22
+ };
23
+ /** Providers offered by the picker, each flagged with whether creds are present. */
24
+ export function detectProviders(env) {
25
+ return [
26
+ { id: "openai", label: "OpenAI / OpenAI-compatible", ready: Boolean(env.OPENAI_API_KEY), hint: "set OPENAI_API_KEY" },
27
+ { id: "anthropic", label: "Anthropic (Claude)", ready: Boolean(env.ANTHROPIC_API_KEY), hint: "set ANTHROPIC_API_KEY" },
28
+ { id: "ollama", label: "Ollama (local)", ready: Boolean(env.OLLAMA_BASE_URL), hint: "needs local Ollama (default :11434)" },
29
+ { id: "deterministic", label: "Deterministic offline stand-in", ready: true, hint: "no key — scaffold tests only" }
30
+ ];
31
+ }
32
+ export async function selectProviderAndModel(env, choose, ask) {
33
+ const providers = detectProviders(env);
34
+ const provIdx = await choose("Select a model provider:", providers.map((p) => `${p.label} ${p.ready ? "[ready]" : `[${p.hint}]`}`));
35
+ if (provIdx < 0 || provIdx >= providers.length)
36
+ return null;
37
+ const chosen = providers[provIdx];
38
+ if (chosen.id === "deterministic")
39
+ return { provider: "deterministic" };
40
+ const models = SUPPORTED_MODELS[chosen.id];
41
+ const labels = models.map((m) => `${m.model} — ${m.note}`);
42
+ labels.push("custom (enter a model id)");
43
+ const modelIdx = await choose(`Select a ${chosen.label} model:`, labels);
44
+ if (modelIdx < 0 || modelIdx > models.length)
45
+ return null;
46
+ if (modelIdx === models.length) {
47
+ const custom = (await ask("Model id: ")).trim();
48
+ if (!custom)
49
+ return null;
50
+ return { provider: chosen.id, model: custom };
51
+ }
52
+ return { provider: chosen.id, model: models[modelIdx].model };
53
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Background-job state, persisted under `.orangepro/jobs/` (gitignored via the
3
+ * `.orangepro` ignore). Pure file helpers — NO process spawning here — so they are
4
+ * unit-testable with a tmp root. The detached child writes its status JSON so an
5
+ * agent can poll completion; the bell/notification is best-effort on top.
6
+ */
7
+ import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { randomBytes } from "node:crypto";
10
+ import { workspacePaths } from "../workspace.js";
11
+ export function jobsDir(root) {
12
+ return join(workspacePaths(root).dir, "jobs");
13
+ }
14
+ export function jobJsonPath(root, id) {
15
+ return join(jobsDir(root), `${id}.json`);
16
+ }
17
+ export function jobLogPath(root, id) {
18
+ return join(jobsDir(root), `${id}.log`);
19
+ }
20
+ /** Result payload for a --single (agent-mode) job: generated tests + run hints. */
21
+ export function jobResultPath(root, id) {
22
+ return join(jobsDir(root), `${id}.result.json`);
23
+ }
24
+ /** Fresh, time-sortable job id. Injectable clock/rng for deterministic tests. */
25
+ export function newJobId(now = Date.now, rnd = () => randomBytes(3).toString("hex")) {
26
+ return `job_${now().toString(36)}_${rnd()}`;
27
+ }
28
+ function ensureDir(root) {
29
+ mkdirSync(jobsDir(root), { recursive: true });
30
+ }
31
+ /** Write the record atomically (temp + rename) so a poller never reads a half file. */
32
+ export function writeJobRecord(root, rec) {
33
+ ensureDir(root);
34
+ const path = jobJsonPath(root, rec.id);
35
+ const tmp = `${path}.tmp`;
36
+ writeFileSync(tmp, JSON.stringify(rec, null, 2) + "\n", "utf8");
37
+ renameSync(tmp, path);
38
+ }
39
+ export function readJobRecord(root, id) {
40
+ const path = jobJsonPath(root, id);
41
+ if (!existsSync(path))
42
+ return null;
43
+ try {
44
+ return JSON.parse(readFileSync(path, "utf8"));
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ }
50
+ /** Immutable read-modify-write of a job record. Returns the new record (or null if absent). */
51
+ export function updateJobRecord(root, id, patch) {
52
+ const cur = readJobRecord(root, id);
53
+ if (!cur)
54
+ return null;
55
+ const next = { ...cur, ...patch };
56
+ writeJobRecord(root, next);
57
+ return next;
58
+ }
59
+ /** All job records, newest first. */
60
+ export function listJobs(root) {
61
+ const dir = jobsDir(root);
62
+ if (!existsSync(dir))
63
+ return [];
64
+ const recs = [];
65
+ for (const f of readdirSync(dir)) {
66
+ if (!f.endsWith(".json"))
67
+ continue;
68
+ try {
69
+ recs.push(JSON.parse(readFileSync(join(dir, f), "utf8")));
70
+ }
71
+ catch {
72
+ // skip a corrupt/partial record
73
+ }
74
+ }
75
+ return recs.sort((a, b) => (a.created_at < b.created_at ? 1 : a.created_at > b.created_at ? -1 : 0));
76
+ }
77
+ export function appendJobLog(root, id, line) {
78
+ ensureDir(root);
79
+ appendFileSync(jobLogPath(root, id), line.endsWith("\n") ? line : `${line}\n`, "utf8");
80
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Best-effort completion notification for a background job. The status JSON is the
3
+ * canonical signal an agent polls; this just nudges a human: a terminal bell
4
+ * (cross-platform, zero deps) and, on macOS, an optional `osascript` notification.
5
+ * All side effects are injectable so tests never fire a real OS notification.
6
+ */
7
+ import { execFile } from "node:child_process";
8
+ const BELL = String.fromCharCode(7); // ASCII BEL — terminal beep, zero deps
9
+ export function notifyJobDone(rec, hooks = {}) {
10
+ const write = hooks.write ?? ((s) => void process.stderr.write(s));
11
+ const platform = hooks.platform ?? process.platform;
12
+ // Terminal bell — the canonical completion signal stays the status file.
13
+ write(BELL);
14
+ const title = "OrangePro";
15
+ const body = `Job ${rec.id} ${rec.status}`;
16
+ const notifier = hooks.notifier ??
17
+ ((t, b) => {
18
+ if (platform !== "darwin")
19
+ return;
20
+ try {
21
+ // Array args (injection-safe); fire-and-forget, swallow any error.
22
+ execFile("osascript", ["-e", `display notification ${JSON.stringify(b)} with title ${JSON.stringify(t)}`], () => { });
23
+ }
24
+ catch {
25
+ // best-effort only
26
+ }
27
+ });
28
+ notifier(title, body);
29
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Executes a background `generate` job IN the detached child. Reuses the same
3
+ * op* functions unchanged; the only extra is streaming progress to the job log
4
+ * and recording status transitions. Directly callable (no spawning) so tests can
5
+ * run it with a tmp root + deterministic provider.
6
+ *
7
+ * Two modes:
8
+ * - "compare" (default A/B): runs opCompare + writeCompareReport.
9
+ * - "single" (agent mode): runs opGenerate and writes a result file with the
10
+ * generated tests + run hints so a polling agent can write & run them.
11
+ */
12
+ import { writeFileSync } from "node:fs";
13
+ import { opCompare, opGenerate, writeCompareReport } from "../operations.js";
14
+ import { runnableRunHintsFor, AGENT_RUN_WORKFLOW } from "../generate/runHints.js";
15
+ import { setProgressReporter } from "../util/progress.js";
16
+ import { appendJobLog, jobLogPath, jobResultPath, readJobRecord, updateJobRecord, writeJobRecord } from "./jobStore.js";
17
+ import { notifyJobDone } from "./notify.js";
18
+ export async function runGenerateJob(root, id, opts, deps, mode = "compare") {
19
+ const stamp = () => (deps ? deps.clock() : new Date().toISOString());
20
+ // The launcher normally pre-creates the queued record; create it if missing so
21
+ // runGenerateJob is self-sufficient (and directly unit-testable).
22
+ if (!readJobRecord(root, id)) {
23
+ writeJobRecord(root, { id, status: "queued", command: "generate", created_at: stamp(), cwd: root, args: {}, log_path: jobLogPath(root, id) });
24
+ }
25
+ updateJobRecord(root, id, { status: "running", started_at: stamp(), pid: process.pid });
26
+ setProgressReporter((m) => appendJobLog(root, id, m));
27
+ try {
28
+ if (mode === "single") {
29
+ appendJobLog(root, id, "Generating tests (single arm — agent mode)…");
30
+ const result = await opGenerate(root, opts, deps);
31
+ const resultPath = jobResultPath(root, id);
32
+ // The durable handoff for an agent: each test's code + suggested path + run command.
33
+ writeFileSync(resultPath, JSON.stringify({
34
+ model_provider: result.model_provider,
35
+ model_name: result.model_name,
36
+ generated_tests: result.generated_tests,
37
+ run_hints: runnableRunHintsFor(result.generated_tests, root),
38
+ agent_workflow: AGENT_RUN_WORKFLOW,
39
+ missing_evidence: result.missing_evidence,
40
+ warnings: result.warnings
41
+ }, null, 2) + "\n", "utf8");
42
+ updateJobRecord(root, id, { status: "done", finished_at: stamp(), outputs: { result_path: resultPath } });
43
+ appendJobLog(root, id, `Done — ${result.generated_tests.length} test(s) + run hints: ${resultPath}`);
44
+ }
45
+ else {
46
+ appendJobLog(root, id, "Generating A/B comparison (prompt-only vs Local KG)…");
47
+ const cmp = await opCompare(root, opts, deps);
48
+ const noTests = cmp.baseline.generated_tests.length === 0 && cmp.grounded.generated_tests.length === 0;
49
+ const outputs = cmp.model_provider !== "none" && !noTests ? writeCompareReport(root, cmp, deps) : undefined;
50
+ updateJobRecord(root, id, {
51
+ status: "done",
52
+ finished_at: stamp(),
53
+ outputs: outputs
54
+ ? {
55
+ tests_path: outputs.local_kg_tests_path,
56
+ report_path: outputs.report_path,
57
+ report_json_path: outputs.report_json_path
58
+ }
59
+ : undefined
60
+ });
61
+ appendJobLog(root, id, outputs ? `Done — Local KG tests: ${outputs.local_kg_tests_path}` : "Done — no tests generated (see report).");
62
+ }
63
+ }
64
+ catch (e) {
65
+ const error = e instanceof Error ? e.message : String(e);
66
+ updateJobRecord(root, id, { status: "failed", finished_at: stamp(), error });
67
+ appendJobLog(root, id, `Failed: ${error}`);
68
+ }
69
+ finally {
70
+ setProgressReporter(null);
71
+ const rec = readJobRecord(root, id);
72
+ if (rec)
73
+ notifyJobDone(rec);
74
+ }
75
+ }
@@ -0,0 +1,117 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { workspacePaths } from "./workspace.js";
4
+ import { hashString } from "./util/hash.js";
5
+ export const LEDGER_SCHEMA_VERSION = "orangepro.local_ledger.v1";
6
+ export const LEDGER_FILE = "ledger.json";
7
+ export function ledgerPath(root) {
8
+ return join(workspacePaths(root).dir, LEDGER_FILE);
9
+ }
10
+ export function proofEdgesFor(graph, symExtId) {
11
+ return graph.edges
12
+ .filter((e) => e.relationship_type === "COVERS" && e.evidence_strength === "hard" && e.to_external_id === symExtId)
13
+ .map(edgeKey)
14
+ .sort();
15
+ }
16
+ export function reproveTarget(preEdges, postGraph, symExtId) {
17
+ const pre = new Set(preEdges);
18
+ const newEdges = proofEdgesFor(postGraph, symExtId).filter((e) => !pre.has(e));
19
+ return { closed: newEdges.length > 0, newEdges };
20
+ }
21
+ export function resolveTargetSymbol(graph, target) {
22
+ const node = graph.nodes.find((n) => n.external_id === target);
23
+ if (node?.kind === "CodeSymbol")
24
+ return node.external_id;
25
+ const directSymbols = new Set();
26
+ for (const edge of graph.edges) {
27
+ if (edge.from_external_id !== target && edge.to_external_id !== target)
28
+ continue;
29
+ if (edge.evidence_strength !== "hard")
30
+ continue;
31
+ const other = edge.from_external_id === target ? edge.to_external_id : edge.from_external_id;
32
+ if (graph.nodes.find((n) => n.external_id === other && n.kind === "CodeSymbol"))
33
+ directSymbols.add(other);
34
+ }
35
+ return directSymbols.size === 1 ? [...directSymbols][0] : null;
36
+ }
37
+ export function targetLanguage(symExtId) {
38
+ const file = symExtId.match(/^sym:(.+)#/)?.[1] ?? "";
39
+ const ext = file.split(".").pop()?.toLowerCase();
40
+ if (ext === "ts" || ext === "tsx" || ext === "js" || ext === "jsx" || ext === "mts" || ext === "cts" || ext === "mjs" || ext === "cjs")
41
+ return "typescript";
42
+ if (ext === "go")
43
+ return "go";
44
+ if (ext === "java")
45
+ return "java";
46
+ if (ext === "py")
47
+ return "python";
48
+ return ext || "unknown";
49
+ }
50
+ export function canReproveLanguage(language) {
51
+ return language === "typescript" || language === "go";
52
+ }
53
+ export function loadLedger(root) {
54
+ const path = ledgerPath(root);
55
+ if (!existsSync(path))
56
+ return { schema_version: LEDGER_SCHEMA_VERSION, records: [] };
57
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
58
+ if (parsed.schema_version !== LEDGER_SCHEMA_VERSION) {
59
+ throw new Error(`Ledger schema mismatch: found ${parsed.schema_version ?? "unknown"}, expected ${LEDGER_SCHEMA_VERSION}.`);
60
+ }
61
+ return parsed;
62
+ }
63
+ export function appendLedgerRecord(root, input) {
64
+ const path = ledgerPath(root);
65
+ const ledger = loadLedger(root);
66
+ const record = {
67
+ ...input,
68
+ run_id: input.run_id || `run:${String(ledger.records.length + 1).padStart(6, "0")}`,
69
+ evidence_ids: [...new Set(input.evidence_ids ?? [])].sort(),
70
+ pre_edge_count: input.pre_edges.length
71
+ };
72
+ ledger.records.push(record);
73
+ mkdirSync(workspacePaths(root).dir, { recursive: true });
74
+ writeFileSync(path, JSON.stringify(ledger, null, 2) + "\n", "utf8");
75
+ return { ledger_path: path, record };
76
+ }
77
+ export function ledgerStats(root) {
78
+ const path = ledgerPath(root);
79
+ const ledger = loadLedger(root);
80
+ const attempted = ledger.records.filter((r) => r.status === "reproven" || r.status === "unproven").length;
81
+ const reproven = ledger.records.filter((r) => r.status === "reproven").length;
82
+ const unproven = ledger.records.filter((r) => r.status === "unproven").length;
83
+ return {
84
+ ledger_path: path,
85
+ records: ledger.records.length,
86
+ attempted,
87
+ reproven,
88
+ unproven,
89
+ already_proven: ledger.records.filter((r) => r.status === "already_proven").length,
90
+ generated_unverifiable: ledger.records.filter((r) => r.status === "generated_unverifiable").length,
91
+ quality_adjusted_kept_rate: attempted > 0 ? Number(((reproven / attempted) * 100).toFixed(2)) : 0
92
+ };
93
+ }
94
+ function edgeKey(edge) {
95
+ return `${edge.from_external_id}->${edge.to_external_id}`;
96
+ }
97
+ /**
98
+ * Composite code-identity fingerprint for a proven target: the target file's
99
+ * manifest content hash + the symbol external id + the credited member/method.
100
+ * Both the ledger writer (`opDynamicProof`) and RTM call THIS one function, so a
101
+ * proof only re-surfaces as Proven while the code it proved is byte-identical
102
+ * (any change to the file's content — hence its hash — lapses the proof).
103
+ * Returns `undefined` when the file has no manifest hash (synthesized symbol /
104
+ * not analyzed) or the id is not a parseable `sym:<file>#<name>` → no fingerprint
105
+ * → the record is never counted as Proven.
106
+ */
107
+ export function targetFingerprint(graph, symExtId) {
108
+ const match = /^sym:(.+)#([^#]+)$/.exec(symExtId);
109
+ if (!match)
110
+ return undefined;
111
+ const [, file, symbolName] = match;
112
+ const member = symbolName.split(".").filter(Boolean).pop();
113
+ const fileHash = graph.manifest.files[file]?.hash;
114
+ if (!fileHash || !member)
115
+ return undefined;
116
+ return hashString([fileHash, symExtId, member].join("|"));
117
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Local proof-kit configuration: privacy defaults + BYOK provider resolution.
3
+ *
4
+ * Security invariant: model provider API keys are read from the environment at
5
+ * call time and are NEVER written into the workspace config, the graph, or the
6
+ * evidence pack.
7
+ */
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { join, resolve } from "node:path";
10
+ export function defaultPrivacySettings() {
11
+ return { upload_enabled: false, source_snippets_in_pack: false };
12
+ }
13
+ export const PROVIDER_ENV_FILES = [".env.provider.local", ".env.local", ".env"];
14
+ const PROVIDER_ENV_KEYS = new Set([
15
+ "OPENAI_API_KEY",
16
+ "OPENAI_BASE_URL",
17
+ "OPENAI_MODEL",
18
+ "ANTHROPIC_API_KEY",
19
+ "ANTHROPIC_BASE_URL",
20
+ "ANTHROPIC_MODEL",
21
+ "OLLAMA_BASE_URL",
22
+ "OLLAMA_MODEL",
23
+ "ORANGEPRO_PROVIDER"
24
+ ]);
25
+ function parseEnvFile(contents) {
26
+ const out = {};
27
+ for (const rawLine of contents.split(/\r?\n/)) {
28
+ const line = rawLine.trim();
29
+ if (!line || line.startsWith("#"))
30
+ continue;
31
+ const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
32
+ if (!match || !PROVIDER_ENV_KEYS.has(match[1]))
33
+ continue;
34
+ let value = match[2].trim();
35
+ if ((value.startsWith("\"") && value.endsWith("\"")) ||
36
+ (value.startsWith("'") && value.endsWith("'"))) {
37
+ value = value.slice(1, -1);
38
+ }
39
+ out[match[1]] = value;
40
+ }
41
+ return out;
42
+ }
43
+ export function loadProviderEnv(roots, env = process.env) {
44
+ const merged = {};
45
+ const seen = new Set();
46
+ for (const root of roots) {
47
+ if (!root)
48
+ continue;
49
+ const abs = resolve(root);
50
+ if (seen.has(abs))
51
+ continue;
52
+ seen.add(abs);
53
+ for (const name of PROVIDER_ENV_FILES) {
54
+ const file = join(abs, name);
55
+ if (!existsSync(file))
56
+ continue;
57
+ try {
58
+ Object.assign(merged, parseEnvFile(readFileSync(file, "utf8")));
59
+ }
60
+ catch {
61
+ /* Ignore unreadable local env files; callers will surface no-provider guidance. */
62
+ }
63
+ }
64
+ }
65
+ return { ...merged, ...env };
66
+ }
67
+ /**
68
+ * Resolve a BYOK provider from env + optional per-call override.
69
+ * Returns null when no usable provider credentials are present, so callers can
70
+ * fall back to a deterministic provider (offline/demo) or surface guidance.
71
+ */
72
+ export function resolveProviderConfig(env = process.env, override = {}) {
73
+ const requested = (override.provider || env.ORANGEPRO_PROVIDER || "").toLowerCase();
74
+ const tryOpenAI = () => {
75
+ if (!env.OPENAI_API_KEY)
76
+ return null;
77
+ return {
78
+ provider: "openai",
79
+ model: override.model || env.OPENAI_MODEL || "gpt-4.1",
80
+ baseUrl: (env.OPENAI_BASE_URL || "https://api.openai.com/v1").replace(/\/+$/, ""),
81
+ apiKey: env.OPENAI_API_KEY
82
+ };
83
+ };
84
+ const tryAnthropic = () => {
85
+ if (!env.ANTHROPIC_API_KEY)
86
+ return null;
87
+ return {
88
+ provider: "anthropic",
89
+ model: override.model || env.ANTHROPIC_MODEL || "claude-sonnet-4-6",
90
+ baseUrl: (env.ANTHROPIC_BASE_URL || "https://api.anthropic.com/v1").replace(/\/+$/, ""),
91
+ apiKey: env.ANTHROPIC_API_KEY
92
+ };
93
+ };
94
+ const tryOllama = () => {
95
+ const base = env.OLLAMA_BASE_URL;
96
+ if (!base && requested !== "ollama")
97
+ return null;
98
+ return {
99
+ provider: "ollama",
100
+ model: override.model || env.OLLAMA_MODEL || "llama3.1",
101
+ baseUrl: (base || "http://localhost:11434").replace(/\/+$/, "")
102
+ };
103
+ };
104
+ if (requested === "openai")
105
+ return tryOpenAI();
106
+ if (requested === "anthropic")
107
+ return tryAnthropic();
108
+ if (requested === "ollama")
109
+ return tryOllama();
110
+ // Auto-detect order: OpenAI-compatible, then Ollama, then Anthropic.
111
+ return tryOpenAI() || tryOllama() || tryAnthropic();
112
+ }