@kontextmind/kxm 0.6.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.
- package/.claude-plugin/marketplace.json +19 -0
- package/.kxm/README.md +14 -0
- package/.kxm/assets/README.md +5 -0
- package/.kxm/assets/retrospectives/README.md +5 -0
- package/.kxm/config/README.md +5 -0
- package/.kxm/config/agents.json +43 -0
- package/.kxm/config/env.example +56 -0
- package/.kxm/config/update.example.yaml +9 -0
- package/.kxm/config/workflows/fix.json +160 -0
- package/.kxm/config/workflows/jira-development.json +116 -0
- package/.kxm/config/workflows/provenance-quorum.json +150 -0
- package/.kxm/config/workflows/v04-dogfood.json +72 -0
- package/CHANGELOG.md +465 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/SECURITY.md +72 -0
- package/docs/README.md +48 -0
- package/docs/agent-communication-envelopes-and-gates.md +553 -0
- package/docs/architecture.md +242 -0
- package/docs/assignment-runner.md +241 -0
- package/docs/configuration.md +361 -0
- package/docs/continuous-improvement.md +114 -0
- package/docs/getting-started.md +253 -0
- package/docs/kxm-handbook.md +1090 -0
- package/docs/operations.md +205 -0
- package/docs/provenance-gates.md +291 -0
- package/docs/skills.md +45 -0
- package/docs/templates/README.md +95 -0
- package/docs/templates/adr.md +88 -0
- package/docs/templates/architecture.md +120 -0
- package/docs/templates/bug-fix.md +109 -0
- package/docs/templates/feature.md +108 -0
- package/docs/templates/handoff.md +72 -0
- package/docs/templates/postmortem.md +77 -0
- package/docs/templates/research.md +100 -0
- package/docs/templates/review.md +85 -0
- package/docs/templates/runbook.md +73 -0
- package/docs/templates/test-plan.md +87 -0
- package/docs/templates/test-report.md +72 -0
- package/docs/test-matrix.md +121 -0
- package/docs/troubleshooting.md +249 -0
- package/docs/vnext/README.md +62 -0
- package/docs/vnext/architecture.md +185 -0
- package/docs/vnext/effects-and-recovery.md +172 -0
- package/docs/vnext/lifecycles.md +235 -0
- package/docs/vnext/migration.md +220 -0
- package/docs/vnext/routing.md +184 -0
- package/docs/vnext/synchronization.md +172 -0
- package/docs/vnext/terminology.md +240 -0
- package/docs/vnext/validation.md +335 -0
- package/docs/webhook-workflows.md +240 -0
- package/docs/workflow-guide.md +1150 -0
- package/examples/README.md +102 -0
- package/examples/provenance-workflow.json +40 -0
- package/examples/requester.ts +30 -0
- package/examples/reviewer-agent.ts +29 -0
- package/examples/roundtrip.ts +46 -0
- package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
- package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
- package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
- package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
- package/examples/vnext/.kxm/agents/planner.yaml +13 -0
- package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
- package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
- package/examples/vnext/.kxm/gates.yaml +8 -0
- package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
- package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
- package/examples/vnext/.kxm/models/implementation.yaml +14 -0
- package/examples/vnext/.kxm/models/primary.yaml +17 -0
- package/examples/vnext/.kxm/prices.yaml +111 -0
- package/examples/vnext/.kxm/project/env.yaml +7 -0
- package/examples/vnext/.kxm/project.yaml +32 -0
- package/examples/vnext/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/.kxm/workflows/default.yaml +92 -0
- package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
- package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
- package/examples/vnext/README.md +53 -0
- package/examples/vnext/records/assignment-result-recorded.json +63 -0
- package/examples/vnext/records/assignment-result.json +46 -0
- package/examples/vnext/records/context-candidate.json +42 -0
- package/examples/vnext/records/delivery-manifest.json +66 -0
- package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
- package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
- package/examples/vnext/records/run-created.json +54 -0
- package/examples/vnext/records/sync-event.json +65 -0
- package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
- package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
- package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
- package/examples/workflow-signal.ts +63 -0
- package/package.json +129 -0
- package/plugins/kxm/.claude-plugin/plugin.json +73 -0
- package/plugins/kxm/.mcp.json +19 -0
- package/plugins/kxm/README.md +93 -0
- package/plugins/kxm/dist/cli.js +42853 -0
- package/plugins/kxm/dist/client.js +416 -0
- package/plugins/kxm/dist/core.js +1823 -0
- package/plugins/kxm/dist/extension.js +3797 -0
- package/plugins/kxm/dist/mcp-server.js +17104 -0
- package/plugins/kxm/dist/runtime.js +23361 -0
- package/plugins/kxm/dist/server.js +13640 -0
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
- package/plugins/kxm/package.json +12 -0
- package/plugins/kxm/skills/kxm/SKILL.md +97 -0
- package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
- package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
- package/plugins/kxm/src/arbiter.ts +355 -0
- package/plugins/kxm/src/artifacts-exist.ts +62 -0
- package/plugins/kxm/src/autocomplete.ts +236 -0
- package/plugins/kxm/src/cli.ts +3707 -0
- package/plugins/kxm/src/client.ts +614 -0
- package/plugins/kxm/src/commands.ts +1063 -0
- package/plugins/kxm/src/config.ts +290 -0
- package/plugins/kxm/src/context/providers.ts +101 -0
- package/plugins/kxm/src/context-packet.ts +332 -0
- package/plugins/kxm/src/context.ts +499 -0
- package/plugins/kxm/src/core.ts +6 -0
- package/plugins/kxm/src/database.ts +563 -0
- package/plugins/kxm/src/diagnostics.ts +184 -0
- package/plugins/kxm/src/envelope.ts +118 -0
- package/plugins/kxm/src/extension.ts +895 -0
- package/plugins/kxm/src/external-effects.ts +299 -0
- package/plugins/kxm/src/github-watch.ts +255 -0
- package/plugins/kxm/src/hub-binding.ts +160 -0
- package/plugins/kxm/src/hub.ts +2502 -0
- package/plugins/kxm/src/improve.ts +383 -0
- package/plugins/kxm/src/inbox.ts +10 -0
- package/plugins/kxm/src/kxm-install-kind.ts +113 -0
- package/plugins/kxm/src/kxm-update-config.ts +39 -0
- package/plugins/kxm/src/kxm-update.ts +238 -0
- package/plugins/kxm/src/local-snapshot.ts +406 -0
- package/plugins/kxm/src/logger.ts +198 -0
- package/plugins/kxm/src/mcp-server.ts +143 -0
- package/plugins/kxm/src/memory.ts +385 -0
- package/plugins/kxm/src/nous-pi.ts +287 -0
- package/plugins/kxm/src/nous-provider.ts +729 -0
- package/plugins/kxm/src/price-calc.ts +87 -0
- package/plugins/kxm/src/prices.ts +121 -0
- package/plugins/kxm/src/protocol.ts +172 -0
- package/plugins/kxm/src/recovery.ts +211 -0
- package/plugins/kxm/src/redact.ts +26 -0
- package/plugins/kxm/src/retrospective.ts +400 -0
- package/plugins/kxm/src/routing.ts +830 -0
- package/plugins/kxm/src/runtime.ts +9 -0
- package/plugins/kxm/src/server.ts +117 -0
- package/plugins/kxm/src/session-work.ts +571 -0
- package/plugins/kxm/src/session.ts +184 -0
- package/plugins/kxm/src/skills.ts +535 -0
- package/plugins/kxm/src/state.ts +326 -0
- package/plugins/kxm/src/store.ts +637 -0
- package/plugins/kxm/src/studio-layout.ts +268 -0
- package/plugins/kxm/src/suggest.ts +162 -0
- package/plugins/kxm/src/task-manager.ts +244 -0
- package/plugins/kxm/src/telemetry.ts +116 -0
- package/plugins/kxm/src/tui.ts +1046 -0
- package/plugins/kxm/src/vnext-bindings.ts +403 -0
- package/plugins/kxm/src/vnext-config.ts +1646 -0
- package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
- package/plugins/kxm/src/vnext-engine-command.ts +533 -0
- package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
- package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
- package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
- package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
- package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
- package/plugins/kxm/src/vnext-engine.ts +2458 -0
- package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
- package/plugins/kxm/src/vnext-harness.ts +1142 -0
- package/plugins/kxm/src/vnext-init.ts +430 -0
- package/plugins/kxm/src/vnext-migrate.ts +1848 -0
- package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
- package/plugins/kxm/src/vnext-permission.ts +936 -0
- package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
- package/plugins/kxm/src/vnext-repair.ts +1094 -0
- package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
- package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
- package/plugins/kxm/src/vnext-runtime.ts +663 -0
- package/plugins/kxm/src/vnext-template.ts +247 -0
- package/plugins/kxm/src/wiki.ts +313 -0
- package/plugins/kxm/src/workflow.ts +1548 -0
- package/schemas/vnext/README.md +46 -0
- package/schemas/vnext/agent.schema.json +40 -0
- package/schemas/vnext/assignment-result.schema.json +66 -0
- package/schemas/vnext/backup-manifest.schema.json +89 -0
- package/schemas/vnext/candidate.schema.json +109 -0
- package/schemas/vnext/common.schema.json +422 -0
- package/schemas/vnext/context-candidate.schema.json +76 -0
- package/schemas/vnext/context-packet.schema.json +192 -0
- package/schemas/vnext/delivery-manifest.schema.json +159 -0
- package/schemas/vnext/environment.schema.json +66 -0
- package/schemas/vnext/gate-registry.schema.json +109 -0
- package/schemas/vnext/handoff-manifest.schema.json +146 -0
- package/schemas/vnext/init-operation.schema.json +61 -0
- package/schemas/vnext/local-repository-bindings.schema.json +30 -0
- package/schemas/vnext/memory-record.schema.json +45 -0
- package/schemas/vnext/migration-decision.schema.json +26 -0
- package/schemas/vnext/migration-plan.schema.json +123 -0
- package/schemas/vnext/migration-receipt.schema.json +52 -0
- package/schemas/vnext/model.schema.json +42 -0
- package/schemas/vnext/permission-diff.schema.json +57 -0
- package/schemas/vnext/prices.schema.json +115 -0
- package/schemas/vnext/project.schema.json +85 -0
- package/schemas/vnext/repository.schema.json +24 -0
- package/schemas/vnext/run-event.schema.json +460 -0
- package/schemas/vnext/session-brief.schema.json +153 -0
- package/schemas/vnext/sync-event.schema.json +234 -0
- package/schemas/vnext/template-provenance.schema.json +38 -0
- package/schemas/vnext/workflow.schema.json +248 -0
- package/scripts/assignment-run.d.mts +354 -0
- package/scripts/assignment-run.mjs +4451 -0
- package/scripts/build-runtime.mjs +56 -0
- package/scripts/check-generated.mjs +77 -0
- package/scripts/check-versions.mjs +34 -0
- package/scripts/emit-codex-artifacts.d.mts +9 -0
- package/scripts/emit-codex-artifacts.mjs +91 -0
- package/scripts/harness-run.d.mts +83 -0
- package/scripts/harness-run.mjs +2095 -0
- package/scripts/kxm-hub.mjs +105 -0
- package/scripts/kxm-publish-npm.mjs +327 -0
- package/scripts/kxm-release-github.mjs +472 -0
- package/scripts/kxm-runtime-supervisor.mjs +7 -0
- package/scripts/kxm-worker.mjs +1127 -0
- package/scripts/kxm.mjs +27 -0
- package/scripts/roster-policy.d.mts +20 -0
- package/scripts/roster-policy.mjs +161 -0
- package/scripts/smoke-multi-pi.mjs +479 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
4
|
+
import { parse, stringify } from "yaml";
|
|
5
|
+
import { redactSecrets } from "./redact.ts";
|
|
6
|
+
|
|
7
|
+
export const MEMORY_SCHEMA = "kxm.memory.v1" as const;
|
|
8
|
+
export const MEMORY_BRIEF_SCHEMA = "kxm.memory-brief.v1" as const;
|
|
9
|
+
export const MEMORY_MARKER_START = "<!-- kxm:memory:start -->";
|
|
10
|
+
export const MEMORY_MARKER_END = "<!-- kxm:memory:end -->";
|
|
11
|
+
|
|
12
|
+
export type MemoryScope = "agent" | "project" | "run" | "operator";
|
|
13
|
+
export type MemoryAuthority = "instruction" | "evidence" | "promoted";
|
|
14
|
+
export type MemoryConfidence = "verified" | "probable" | "uncertain";
|
|
15
|
+
export type MemoryLifecycle = "active" | "deprecated" | "superseded";
|
|
16
|
+
|
|
17
|
+
const VALID_SCOPES = new Set<MemoryScope>(["agent", "project", "run", "operator"]);
|
|
18
|
+
const VALID_AUTHORITIES = new Set<MemoryAuthority>(["instruction", "evidence", "promoted"]);
|
|
19
|
+
const VALID_CONFIDENCES = new Set<MemoryConfidence>(["verified", "probable", "uncertain"]);
|
|
20
|
+
const VALID_LIFECYCLES = new Set<MemoryLifecycle>(["active", "deprecated", "superseded"]);
|
|
21
|
+
|
|
22
|
+
const BANNED_CONTROL_PLANE_FIELDS = new Set([
|
|
23
|
+
"permissions",
|
|
24
|
+
"tools",
|
|
25
|
+
"allow",
|
|
26
|
+
"deny",
|
|
27
|
+
"grants",
|
|
28
|
+
"approval",
|
|
29
|
+
"policy",
|
|
30
|
+
"scopes",
|
|
31
|
+
"credentials",
|
|
32
|
+
"secrets",
|
|
33
|
+
"token",
|
|
34
|
+
"apiKey",
|
|
35
|
+
"password",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
export interface MemoryProvenance {
|
|
39
|
+
sourceType: string;
|
|
40
|
+
sourceRef?: string;
|
|
41
|
+
runId?: string;
|
|
42
|
+
timestamp?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface MemoryRecord {
|
|
46
|
+
schema: typeof MEMORY_SCHEMA;
|
|
47
|
+
id: string;
|
|
48
|
+
scope: MemoryScope;
|
|
49
|
+
kind: string;
|
|
50
|
+
summary: string;
|
|
51
|
+
provenance: MemoryProvenance;
|
|
52
|
+
authority: MemoryAuthority;
|
|
53
|
+
confidence: MemoryConfidence;
|
|
54
|
+
lifecycle: MemoryLifecycle;
|
|
55
|
+
evidenceRefs: string[];
|
|
56
|
+
body?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface MemoryBrief {
|
|
60
|
+
schema: typeof MEMORY_BRIEF_SCHEMA;
|
|
61
|
+
generatedAt: string;
|
|
62
|
+
count: number;
|
|
63
|
+
facts: MemoryRecord[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseFrontmatter(content: string): { frontmatter: string; body: string } {
|
|
67
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
68
|
+
if (!match) {
|
|
69
|
+
throw new Error("memory file must contain YAML frontmatter enclosed in '---'");
|
|
70
|
+
}
|
|
71
|
+
return { frontmatter: match[1] ?? "", body: match[2]?.trim() ?? "" };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function parseMemoryRecord(raw: string, filename = "memory.md"): MemoryRecord {
|
|
75
|
+
const { frontmatter, body } = parseFrontmatter(raw);
|
|
76
|
+
const data = parse(frontmatter) as Record<string, unknown> | null;
|
|
77
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
78
|
+
throw new Error(`invalid YAML frontmatter in ${filename}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 1. Control-plane rejection
|
|
82
|
+
for (const field of BANNED_CONTROL_PLANE_FIELDS) {
|
|
83
|
+
if (field in data) {
|
|
84
|
+
throw new Error(`memory record ${filename} may not carry control-plane field '${field}'`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. Schema check
|
|
89
|
+
if (data.schema !== MEMORY_SCHEMA) {
|
|
90
|
+
throw new Error(`memory record ${filename} has unsupported schema: expected ${MEMORY_SCHEMA}, got ${String(data.schema)}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 3. ID validation
|
|
94
|
+
const id = typeof data.id === "string" ? data.id.trim() : "";
|
|
95
|
+
if (!id || !/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/i.test(id)) {
|
|
96
|
+
throw new Error(`memory record ${filename} has invalid id: ${String(data.id)}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 4. Scope validation
|
|
100
|
+
const scope = data.scope as MemoryScope;
|
|
101
|
+
if (!VALID_SCOPES.has(scope)) {
|
|
102
|
+
throw new Error(`memory record ${filename} has invalid scope: must be one of agent, project, run, operator`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 5. Kind validation
|
|
106
|
+
const kind = typeof data.kind === "string" ? data.kind.trim() : "";
|
|
107
|
+
if (!kind) {
|
|
108
|
+
throw new Error(`memory record ${filename} is missing kind`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 6. Summary validation & secret redaction
|
|
112
|
+
const rawSummary = typeof data.summary === "string" ? data.summary.trim() : "";
|
|
113
|
+
if (!rawSummary) {
|
|
114
|
+
throw new Error(`memory record ${filename} is missing summary`);
|
|
115
|
+
}
|
|
116
|
+
const summary = redactSecrets(rawSummary);
|
|
117
|
+
|
|
118
|
+
// 7. Provenance validation & secret redaction
|
|
119
|
+
const rawProv = data.provenance as Record<string, unknown> | undefined;
|
|
120
|
+
if (!rawProv || typeof rawProv !== "object" || typeof rawProv.sourceType !== "string" || !rawProv.sourceType.trim()) {
|
|
121
|
+
throw new Error(`memory record ${filename} is missing provenance.sourceType`);
|
|
122
|
+
}
|
|
123
|
+
const provenance: MemoryProvenance = {
|
|
124
|
+
sourceType: rawProv.sourceType.trim(),
|
|
125
|
+
...(typeof rawProv.sourceRef === "string" ? { sourceRef: redactSecrets(rawProv.sourceRef.trim()) } : {}),
|
|
126
|
+
...(typeof rawProv.runId === "string" ? { runId: rawProv.runId.trim() } : {}),
|
|
127
|
+
...(typeof rawProv.timestamp === "string" ? { timestamp: rawProv.timestamp.trim() } : {}),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// 8. Authority validation
|
|
131
|
+
const authority = data.authority as MemoryAuthority;
|
|
132
|
+
if (!VALID_AUTHORITIES.has(authority)) {
|
|
133
|
+
throw new Error(`memory record ${filename} has invalid authority: ${String(data.authority)}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 9. Confidence validation
|
|
137
|
+
const confidence = data.confidence as MemoryConfidence;
|
|
138
|
+
if (!VALID_CONFIDENCES.has(confidence)) {
|
|
139
|
+
throw new Error(`memory record ${filename} has invalid confidence: ${String(data.confidence)}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 10. Lifecycle validation
|
|
143
|
+
const lifecycle = data.lifecycle as MemoryLifecycle;
|
|
144
|
+
if (!VALID_LIFECYCLES.has(lifecycle)) {
|
|
145
|
+
throw new Error(`memory record ${filename} has invalid lifecycle: ${String(data.lifecycle)}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 11. EvidenceRefs validation
|
|
149
|
+
const rawRefs = Array.isArray(data.evidenceRefs) ? data.evidenceRefs : [];
|
|
150
|
+
const evidenceRefs = rawRefs.map(String);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
schema: MEMORY_SCHEMA,
|
|
154
|
+
id,
|
|
155
|
+
scope,
|
|
156
|
+
kind,
|
|
157
|
+
summary,
|
|
158
|
+
provenance,
|
|
159
|
+
authority,
|
|
160
|
+
confidence,
|
|
161
|
+
lifecycle,
|
|
162
|
+
evidenceRefs,
|
|
163
|
+
...(body ? { body } : {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function formatMemoryRecord(record: MemoryRecord): string {
|
|
168
|
+
const frontmatter: Record<string, unknown> = {
|
|
169
|
+
schema: record.schema,
|
|
170
|
+
id: record.id,
|
|
171
|
+
scope: record.scope,
|
|
172
|
+
kind: record.kind,
|
|
173
|
+
summary: record.summary,
|
|
174
|
+
provenance: record.provenance,
|
|
175
|
+
authority: record.authority,
|
|
176
|
+
confidence: record.confidence,
|
|
177
|
+
lifecycle: record.lifecycle,
|
|
178
|
+
evidenceRefs: record.evidenceRefs,
|
|
179
|
+
};
|
|
180
|
+
const yamlText = stringify(frontmatter).trim();
|
|
181
|
+
const bodyText = record.body?.trim() ?? "";
|
|
182
|
+
return bodyText ? `---\n${yamlText}\n---\n\n${bodyText}\n` : `---\n${yamlText}\n---\n`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function memoryDirectories(repoRoot: string): { memoryDir: string; candidatesDir: string } {
|
|
186
|
+
const root = resolve(repoRoot);
|
|
187
|
+
const memoryDir = join(root, ".kxm", "memory");
|
|
188
|
+
const candidatesDir = join(memoryDir, "candidates");
|
|
189
|
+
return { memoryDir, candidatesDir };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function loadAuthoredMemory(repoRoot: string): MemoryRecord[] {
|
|
193
|
+
const { memoryDir } = memoryDirectories(repoRoot);
|
|
194
|
+
if (!existsSync(memoryDir)) return [];
|
|
195
|
+
|
|
196
|
+
const records: MemoryRecord[] = [];
|
|
197
|
+
const entries = readdirSync(memoryDir, { withFileTypes: true });
|
|
198
|
+
for (const entry of entries) {
|
|
199
|
+
if (entry.isFile() && extname(entry.name) === ".md") {
|
|
200
|
+
const fullPath = join(memoryDir, entry.name);
|
|
201
|
+
try {
|
|
202
|
+
const text = readFileSync(fullPath, "utf8");
|
|
203
|
+
const record = parseMemoryRecord(text, entry.name);
|
|
204
|
+
if (record.lifecycle === "active") {
|
|
205
|
+
records.push(record);
|
|
206
|
+
}
|
|
207
|
+
} catch (error) {
|
|
208
|
+
// Log or rethrow depending on strictness
|
|
209
|
+
throw new Error(`failed to parse authored memory file ${entry.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return records.sort((left, right) => left.id.localeCompare(right.id));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function loadCandidateMemory(repoRoot: string): MemoryRecord[] {
|
|
218
|
+
const { candidatesDir } = memoryDirectories(repoRoot);
|
|
219
|
+
if (!existsSync(candidatesDir)) return [];
|
|
220
|
+
|
|
221
|
+
const records: MemoryRecord[] = [];
|
|
222
|
+
const entries = readdirSync(candidatesDir, { withFileTypes: true });
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
if (entry.isFile() && extname(entry.name) === ".md") {
|
|
225
|
+
const fullPath = join(candidatesDir, entry.name);
|
|
226
|
+
try {
|
|
227
|
+
const text = readFileSync(fullPath, "utf8");
|
|
228
|
+
const record = parseMemoryRecord(text, entry.name);
|
|
229
|
+
records.push(record);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
throw new Error(`failed to parse candidate memory file ${entry.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return records.sort((left, right) => left.id.localeCompare(right.id));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function createMemoryNote(
|
|
240
|
+
repoRoot: string,
|
|
241
|
+
summary: string,
|
|
242
|
+
options: {
|
|
243
|
+
scope?: MemoryScope | undefined;
|
|
244
|
+
kind?: string | undefined;
|
|
245
|
+
body?: string | undefined;
|
|
246
|
+
sourceRef?: string | undefined;
|
|
247
|
+
runId?: string | undefined;
|
|
248
|
+
} = {},
|
|
249
|
+
): { record: MemoryRecord; path: string } {
|
|
250
|
+
const { candidatesDir } = memoryDirectories(repoRoot);
|
|
251
|
+
mkdirSync(candidatesDir, { recursive: true });
|
|
252
|
+
|
|
253
|
+
const scope: MemoryScope = options.scope ?? "project";
|
|
254
|
+
if (!VALID_SCOPES.has(scope)) {
|
|
255
|
+
throw new Error(`invalid memory note scope: ${scope}; must be one of agent, project, run, operator`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const hex = randomUUID().replaceAll("-", "").slice(0, 12);
|
|
259
|
+
const id = `cand_${scope}_${hex}`;
|
|
260
|
+
const record: MemoryRecord = {
|
|
261
|
+
schema: MEMORY_SCHEMA,
|
|
262
|
+
id,
|
|
263
|
+
scope,
|
|
264
|
+
kind: options.kind ?? "learning",
|
|
265
|
+
summary: redactSecrets(summary.trim()),
|
|
266
|
+
provenance: {
|
|
267
|
+
sourceType: "command",
|
|
268
|
+
sourceRef: options.sourceRef ? redactSecrets(options.sourceRef) : "kxm memory note",
|
|
269
|
+
...(options.runId ? { runId: options.runId } : {}),
|
|
270
|
+
timestamp: new Date().toISOString(),
|
|
271
|
+
},
|
|
272
|
+
authority: "evidence",
|
|
273
|
+
confidence: "probable",
|
|
274
|
+
lifecycle: "active",
|
|
275
|
+
evidenceRefs: [],
|
|
276
|
+
...(options.body ? { body: options.body.trim() } : {}),
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const targetPath = join(candidatesDir, `${id}.md`);
|
|
280
|
+
writeFileSync(targetPath, formatMemoryRecord(record), "utf8");
|
|
281
|
+
return { record, path: targetPath };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function generateMemoryBrief(repoRoot: string): MemoryBrief {
|
|
285
|
+
const facts = loadAuthoredMemory(repoRoot);
|
|
286
|
+
return {
|
|
287
|
+
schema: MEMORY_BRIEF_SCHEMA,
|
|
288
|
+
generatedAt: new Date().toISOString(),
|
|
289
|
+
count: facts.length,
|
|
290
|
+
facts,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function formatMemoryBriefText(brief: MemoryBrief): string {
|
|
295
|
+
if (brief.facts.length === 0) {
|
|
296
|
+
return "No active project memory facts.";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const lines: string[] = [`Project memory (${brief.facts.length} active fact${brief.facts.length === 1 ? "" : "s"}):`, ""];
|
|
300
|
+
for (const fact of brief.facts) {
|
|
301
|
+
lines.push(`• [${fact.scope}] [${fact.kind}] ${fact.summary} (${fact.id})`);
|
|
302
|
+
}
|
|
303
|
+
return lines.join("\n");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function formatHarnessMemoryBlock(records: MemoryRecord[]): string {
|
|
307
|
+
const contentLines: string[] = [MEMORY_MARKER_START, "## Project memory (read-only projection)", ""];
|
|
308
|
+
if (records.length === 0) {
|
|
309
|
+
contentLines.push("*No active project memory.*");
|
|
310
|
+
} else {
|
|
311
|
+
for (const record of records) {
|
|
312
|
+
contentLines.push(`- **[${record.scope}]** [${record.kind}] ${record.summary} (\`${record.id}\`)`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
contentLines.push(MEMORY_MARKER_END);
|
|
316
|
+
return contentLines.join("\n");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function updateHarnessDocument(filePath: string, block: string, defaultHeader: string): boolean {
|
|
320
|
+
let original = "";
|
|
321
|
+
if (existsSync(filePath)) {
|
|
322
|
+
original = readFileSync(filePath, "utf8");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let updated = "";
|
|
326
|
+
if (original.includes(MEMORY_MARKER_START) && original.includes(MEMORY_MARKER_END)) {
|
|
327
|
+
const startIdx = original.indexOf(MEMORY_MARKER_START);
|
|
328
|
+
const endIdx = original.indexOf(MEMORY_MARKER_END) + MEMORY_MARKER_END.length;
|
|
329
|
+
updated = original.slice(0, startIdx) + block + original.slice(endIdx);
|
|
330
|
+
} else if (original.length > 0) {
|
|
331
|
+
// If "## Do not" exists, insert before it; else append to end
|
|
332
|
+
const doNotIdx = original.indexOf("## Do not");
|
|
333
|
+
if (doNotIdx !== -1) {
|
|
334
|
+
updated = `${original.slice(0, doNotIdx).trimEnd()}\n\n${block}\n\n${original.slice(doNotIdx)}`;
|
|
335
|
+
} else {
|
|
336
|
+
updated = `${original.trimEnd()}\n\n${block}\n`;
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
updated = `${defaultHeader.trimEnd()}\n\n${block}\n`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (updated !== original) {
|
|
343
|
+
writeFileSync(filePath, updated, "utf8");
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function syncHarnessMemory(repoRoot: string): { updated: string[]; created: string[] } {
|
|
350
|
+
const root = resolve(repoRoot);
|
|
351
|
+
const records = loadAuthoredMemory(root);
|
|
352
|
+
const block = formatHarnessMemoryBlock(records);
|
|
353
|
+
|
|
354
|
+
const updated: string[] = [];
|
|
355
|
+
const created: string[] = [];
|
|
356
|
+
|
|
357
|
+
// 1. AGENTS.md
|
|
358
|
+
const agentsPath = join(root, "AGENTS.md");
|
|
359
|
+
const agentsHeader = "# AGENTS\n\nFollow project instructions.\n";
|
|
360
|
+
const agentsExisted = existsSync(agentsPath);
|
|
361
|
+
if (updateHarnessDocument(agentsPath, block, agentsHeader)) {
|
|
362
|
+
if (agentsExisted) updated.push("AGENTS.md");
|
|
363
|
+
else created.push("AGENTS.md");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 2. CLAUDE.md
|
|
367
|
+
const claudePath = join(root, "CLAUDE.md");
|
|
368
|
+
const claudeHeader = `# KXM (Claude)\n\nFollow [\`AGENTS.md\`](AGENTS.md). Official phase tracking:\n[\`plans/implementation-plan.md\`](plans/implementation-plan.md#tracking-working-tree-not-a-release).\n\nYou are the **planner / architecture critic** unless the human explicitly asks\nyou to implement. Default writer is native Grok CLI (\`grok --model grok-4.6\`).\nIf \`grok\` is logged out, fail closed; do not fall back to Pi. Your reviews are\nartifacts, not hub \`peer-reply\` evidence.\n`;
|
|
369
|
+
const claudeExisted = existsSync(claudePath);
|
|
370
|
+
if (updateHarnessDocument(claudePath, block, claudeHeader)) {
|
|
371
|
+
if (claudeExisted) updated.push("CLAUDE.md");
|
|
372
|
+
else created.push("CLAUDE.md");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// 3. GEMINI.md
|
|
376
|
+
const geminiPath = join(root, "GEMINI.md");
|
|
377
|
+
const geminiHeader = `# KXM (Gemini / Antigravity)\n\nFollow [\`AGENTS.md\`](AGENTS.md). Official phase tracking:\n[\`plans/implementation-plan.md\`](plans/implementation-plan.md#tracking-working-tree-not-a-release).\n\nGoogle / Antigravity CLI (\`agy\`) is an admitted native writer and experiment edit route\nfor Gemini models. Starting rotation remains Grok.\n`;
|
|
378
|
+
const geminiExisted = existsSync(geminiPath);
|
|
379
|
+
if (updateHarnessDocument(geminiPath, block, geminiHeader)) {
|
|
380
|
+
if (geminiExisted) updated.push("GEMINI.md");
|
|
381
|
+
else created.push("GEMINI.md");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return { updated, created };
|
|
385
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy Pi registerProvider glue for opt-in Nous routes.
|
|
3
|
+
* No @earendil-works/pi-ai import. Direct auth is NOUS_API_KEY only.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
NOUS_DIRECT_BASE_URL,
|
|
10
|
+
NOUS_DIRECT_ID,
|
|
11
|
+
NOUS_PROXY_DEFAULT_BASE_URL,
|
|
12
|
+
NOUS_PROXY_ID,
|
|
13
|
+
NOUS_PROXY_PLACEHOLDER_KEY,
|
|
14
|
+
buildModelConfigs,
|
|
15
|
+
fetchNousModels,
|
|
16
|
+
guidanceFor,
|
|
17
|
+
isLoopbackUrl,
|
|
18
|
+
modelsUrl,
|
|
19
|
+
parseCatalogPin,
|
|
20
|
+
parseNousEnv,
|
|
21
|
+
sanitizeNousText,
|
|
22
|
+
type BuiltModel,
|
|
23
|
+
type CatalogLoad,
|
|
24
|
+
type DiscoveryOutcome,
|
|
25
|
+
type DiscoveryProvenance,
|
|
26
|
+
type GuidanceMessage,
|
|
27
|
+
type NousCatalogPin,
|
|
28
|
+
type NousEnvParse,
|
|
29
|
+
type NousProviderKind,
|
|
30
|
+
} from "./nous-provider.ts";
|
|
31
|
+
|
|
32
|
+
export interface NousProviderRegistration {
|
|
33
|
+
id: string;
|
|
34
|
+
registered: number;
|
|
35
|
+
skipped: number;
|
|
36
|
+
error?: string;
|
|
37
|
+
reason?: string;
|
|
38
|
+
provenance?: DiscoveryProvenance;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface NousRegistrationReport {
|
|
42
|
+
optedIn: boolean;
|
|
43
|
+
direct: NousProviderRegistration;
|
|
44
|
+
proxy: NousProviderRegistration;
|
|
45
|
+
guidance: GuidanceMessage[];
|
|
46
|
+
registeredProviders: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface NousRegisterDeps {
|
|
50
|
+
env?: NodeJS.Dict<string>;
|
|
51
|
+
fetch?: typeof fetch;
|
|
52
|
+
readFile?: (path: string) => Promise<string>;
|
|
53
|
+
nowMs?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type LegacyPi = Pick<ExtensionAPI, "registerProvider">;
|
|
57
|
+
|
|
58
|
+
const emptySide = (id: string): NousProviderRegistration => ({ id, registered: 0, skipped: 0 });
|
|
59
|
+
|
|
60
|
+
export function emptyNousReport(): NousRegistrationReport {
|
|
61
|
+
return {
|
|
62
|
+
optedIn: false,
|
|
63
|
+
direct: emptySide(NOUS_DIRECT_ID),
|
|
64
|
+
proxy: emptySide(NOUS_PROXY_ID),
|
|
65
|
+
guidance: [],
|
|
66
|
+
registeredProviders: [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Factory helper: unset stays synchronous; invalid tokens fail closed without
|
|
72
|
+
* fetch; opted-in routes discover then register before Pi lists models.
|
|
73
|
+
*/
|
|
74
|
+
export function nousFactoryWork(
|
|
75
|
+
pi: ExtensionAPI,
|
|
76
|
+
onReport: (report: NousRegistrationReport) => void,
|
|
77
|
+
deps: NousRegisterDeps = {},
|
|
78
|
+
): void | Promise<void> {
|
|
79
|
+
const env = deps.env ?? process.env;
|
|
80
|
+
const parsed = parseNousEnv(env);
|
|
81
|
+
if (parsed.status === "unset") {
|
|
82
|
+
onReport(emptyNousReport());
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (parsed.status === "invalid") {
|
|
86
|
+
onReport({
|
|
87
|
+
...emptyNousReport(),
|
|
88
|
+
optedIn: true,
|
|
89
|
+
guidance: guidanceFor({ parsed }),
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
return registerNousProviders(pi, deps).then(onReport);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function registerNousProviders(
|
|
97
|
+
pi: LegacyPi,
|
|
98
|
+
deps: NousRegisterDeps = {},
|
|
99
|
+
): Promise<NousRegistrationReport> {
|
|
100
|
+
const env = deps.env ?? process.env;
|
|
101
|
+
const parsed = parseNousEnv(env);
|
|
102
|
+
if (parsed.status === "unset") return emptyNousReport();
|
|
103
|
+
if (parsed.status === "invalid") {
|
|
104
|
+
return { ...emptyNousReport(), optedIn: true, guidance: guidanceFor({ parsed }) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const report = emptyNousReport();
|
|
108
|
+
report.optedIn = true;
|
|
109
|
+
let pin: NousCatalogPin | undefined;
|
|
110
|
+
let catalogError: string | undefined;
|
|
111
|
+
if (parsed.catalogFile) {
|
|
112
|
+
const loaded = await loadCatalog(parsed.catalogFile, deps);
|
|
113
|
+
if (loaded.ok) pin = loaded.pin;
|
|
114
|
+
else catalogError = loaded.reason;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const skipped: Array<{ id: string; reason: string }> = [];
|
|
118
|
+
let directDiscovery: DiscoveryOutcome | undefined;
|
|
119
|
+
let proxyDiscovery: DiscoveryOutcome | undefined;
|
|
120
|
+
let proxyUrlInvalid = false;
|
|
121
|
+
const directHasKey = hasDirectKey(env);
|
|
122
|
+
|
|
123
|
+
if (parsed.providers.includes("direct")) {
|
|
124
|
+
const result = await registerRoute({
|
|
125
|
+
pi,
|
|
126
|
+
route: "direct",
|
|
127
|
+
env,
|
|
128
|
+
parsed,
|
|
129
|
+
pin,
|
|
130
|
+
hasKey: directHasKey,
|
|
131
|
+
...(deps.fetch ? { fetchImpl: deps.fetch } : {}),
|
|
132
|
+
...(deps.nowMs !== undefined ? { nowMs: deps.nowMs } : {}),
|
|
133
|
+
});
|
|
134
|
+
report.direct = result.side;
|
|
135
|
+
directDiscovery = result.discovery;
|
|
136
|
+
skipped.push(...result.skipped);
|
|
137
|
+
if (result.registered) report.registeredProviders.push(NOUS_DIRECT_ID);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (parsed.providers.includes("proxy")) {
|
|
141
|
+
if (!isLoopbackUrl(parsed.proxyUrl)) {
|
|
142
|
+
proxyUrlInvalid = true;
|
|
143
|
+
report.proxy = {
|
|
144
|
+
id: NOUS_PROXY_ID,
|
|
145
|
+
registered: 0,
|
|
146
|
+
skipped: 0,
|
|
147
|
+
error: "non-loopback",
|
|
148
|
+
reason: "proxy URL is not loopback http(s)",
|
|
149
|
+
};
|
|
150
|
+
} else {
|
|
151
|
+
const result = await registerRoute({
|
|
152
|
+
pi,
|
|
153
|
+
route: "proxy",
|
|
154
|
+
env,
|
|
155
|
+
parsed,
|
|
156
|
+
pin,
|
|
157
|
+
hasKey: true,
|
|
158
|
+
...(deps.fetch ? { fetchImpl: deps.fetch } : {}),
|
|
159
|
+
...(deps.nowMs !== undefined ? { nowMs: deps.nowMs } : {}),
|
|
160
|
+
});
|
|
161
|
+
report.proxy = result.side;
|
|
162
|
+
proxyDiscovery = result.discovery;
|
|
163
|
+
skipped.push(...result.skipped);
|
|
164
|
+
if (result.registered) report.registeredProviders.push(NOUS_PROXY_ID);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
report.guidance = guidanceFor({
|
|
169
|
+
parsed,
|
|
170
|
+
directHasKey,
|
|
171
|
+
proxyUrlInvalid,
|
|
172
|
+
skipped,
|
|
173
|
+
...(directDiscovery ? { directDiscovery } : {}),
|
|
174
|
+
...(proxyDiscovery ? { proxyDiscovery } : {}),
|
|
175
|
+
...(catalogError ? { catalogError } : {}),
|
|
176
|
+
});
|
|
177
|
+
return report;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function registerRoute(input: {
|
|
181
|
+
pi: LegacyPi;
|
|
182
|
+
route: NousProviderKind;
|
|
183
|
+
env: NodeJS.Dict<string>;
|
|
184
|
+
parsed: Extract<NousEnvParse, { status: "ready" }>;
|
|
185
|
+
pin: NousCatalogPin | undefined;
|
|
186
|
+
fetchImpl?: typeof fetch;
|
|
187
|
+
hasKey: boolean;
|
|
188
|
+
nowMs?: number;
|
|
189
|
+
}): Promise<{
|
|
190
|
+
side: NousProviderRegistration;
|
|
191
|
+
discovery?: DiscoveryOutcome;
|
|
192
|
+
skipped: Array<{ id: string; reason: string }>;
|
|
193
|
+
registered: boolean;
|
|
194
|
+
}> {
|
|
195
|
+
const providerId = input.route === "direct" ? NOUS_DIRECT_ID : NOUS_PROXY_ID;
|
|
196
|
+
const baseUrl = input.route === "direct" ? NOUS_DIRECT_BASE_URL : input.parsed.proxyUrl;
|
|
197
|
+
const side: NousProviderRegistration = { id: providerId, registered: 0, skipped: 0 };
|
|
198
|
+
|
|
199
|
+
if (input.route === "direct" && !input.hasKey) {
|
|
200
|
+
registerLegacy(input.pi, providerId, baseUrl, "$NOUS_API_KEY", []);
|
|
201
|
+
return { side, skipped: [], registered: true };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const authorization = input.route === "direct"
|
|
205
|
+
? input.env.NOUS_API_KEY
|
|
206
|
+
: NOUS_PROXY_PLACEHOLDER_KEY;
|
|
207
|
+
|
|
208
|
+
const discovery = await fetchNousModels({
|
|
209
|
+
url: modelsUrl(baseUrl),
|
|
210
|
+
timeoutMs: input.parsed.timeoutMs,
|
|
211
|
+
...(authorization ? { authorization } : {}),
|
|
212
|
+
...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}),
|
|
213
|
+
...(input.nowMs !== undefined ? { nowMs: input.nowMs } : {}),
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (!discovery.ok) {
|
|
217
|
+
side.error = discovery.error;
|
|
218
|
+
side.reason = discovery.reason;
|
|
219
|
+
registerLegacy(input.pi, providerId, baseUrl, apiKeyRef(input.route), []);
|
|
220
|
+
return { side, discovery, skipped: [], registered: true };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (discovery.provenance) side.provenance = discovery.provenance;
|
|
224
|
+
const built = buildModelConfigs(discovery.models, input.pin, input.route);
|
|
225
|
+
side.registered = built.registered.length;
|
|
226
|
+
side.skipped = built.skipped.length;
|
|
227
|
+
registerLegacy(input.pi, providerId, baseUrl, apiKeyRef(input.route), built.registered);
|
|
228
|
+
return { side, discovery, skipped: built.skipped, registered: true };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function apiKeyRef(route: NousProviderKind): string {
|
|
232
|
+
return route === "direct" ? "$NOUS_API_KEY" : NOUS_PROXY_PLACEHOLDER_KEY;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function registerLegacy(
|
|
236
|
+
pi: LegacyPi,
|
|
237
|
+
id: string,
|
|
238
|
+
baseUrl: string,
|
|
239
|
+
apiKey: string,
|
|
240
|
+
models: BuiltModel[],
|
|
241
|
+
): void {
|
|
242
|
+
if (typeof pi.registerProvider !== "function") {
|
|
243
|
+
throw new Error("Pi registerProvider is unavailable");
|
|
244
|
+
}
|
|
245
|
+
pi.registerProvider(id, {
|
|
246
|
+
name: id === NOUS_DIRECT_ID ? "Nous" : "Nous subscription proxy",
|
|
247
|
+
baseUrl,
|
|
248
|
+
apiKey,
|
|
249
|
+
api: "openai-completions",
|
|
250
|
+
authHeader: true,
|
|
251
|
+
models: models.map((model) => ({
|
|
252
|
+
id: model.id,
|
|
253
|
+
name: model.name,
|
|
254
|
+
reasoning: model.reasoning === true,
|
|
255
|
+
input: model.input && model.input.length > 0 ? model.input : ["text"],
|
|
256
|
+
// Upper-bound rates only: Pi cost.tiers uses strict > and can underquote.
|
|
257
|
+
cost: {
|
|
258
|
+
input: model.cost.input,
|
|
259
|
+
output: model.cost.output,
|
|
260
|
+
cacheRead: model.cost.cacheRead,
|
|
261
|
+
cacheWrite: model.cost.cacheWrite,
|
|
262
|
+
},
|
|
263
|
+
contextWindow: model.contextWindow,
|
|
264
|
+
maxTokens: model.maxTokens,
|
|
265
|
+
})),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function hasDirectKey(env: NodeJS.Dict<string>): boolean {
|
|
270
|
+
const key = env.NOUS_API_KEY;
|
|
271
|
+
return typeof key === "string" && key.trim().length > 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function loadCatalog(path: string, deps: NousRegisterDeps): Promise<CatalogLoad> {
|
|
275
|
+
try {
|
|
276
|
+
const read = deps.readFile ?? ((filePath: string) => readFile(filePath, "utf8"));
|
|
277
|
+
const text = await read(path);
|
|
278
|
+
return parseCatalogPin(JSON.parse(text) as unknown, deps.nowMs ?? Date.now());
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (error instanceof SyntaxError) {
|
|
281
|
+
return { ok: false, reason: "catalog file is not valid JSON" };
|
|
282
|
+
}
|
|
283
|
+
return { ok: false, reason: sanitizeNousText(error instanceof Error ? error.message : "catalog file could not be read") };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export const NOUS_PROXY_DEFAULT = NOUS_PROXY_DEFAULT_BASE_URL;
|