@compr/opscontext-mcp 2.0.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 (48) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/LICENSE +83 -0
  3. package/README.md +470 -0
  4. package/defaults/learnings.json +146 -0
  5. package/dist/activation.d.ts +48 -0
  6. package/dist/activation.js +377 -0
  7. package/dist/adapters.d.ts +101 -0
  8. package/dist/adapters.js +171 -0
  9. package/dist/agents.d.ts +137 -0
  10. package/dist/agents.js +1638 -0
  11. package/dist/audit.d.ts +23 -0
  12. package/dist/audit.js +163 -0
  13. package/dist/cache.d.ts +15 -0
  14. package/dist/cache.js +117 -0
  15. package/dist/claude-integration.d.ts +95 -0
  16. package/dist/claude-integration.js +247 -0
  17. package/dist/cli.d.ts +18 -0
  18. package/dist/cli.js +1823 -0
  19. package/dist/code-chunker.d.ts +12 -0
  20. package/dist/code-chunker.js +270 -0
  21. package/dist/collectors.d.ts +63 -0
  22. package/dist/collectors.js +617 -0
  23. package/dist/config.d.ts +73 -0
  24. package/dist/config.js +239 -0
  25. package/dist/embeddings.d.ts +36 -0
  26. package/dist/embeddings.js +124 -0
  27. package/dist/firewall.d.ts +133 -0
  28. package/dist/firewall.js +631 -0
  29. package/dist/hooks.d.ts +76 -0
  30. package/dist/hooks.js +313 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.js +1081 -0
  33. package/dist/ingest.d.ts +32 -0
  34. package/dist/ingest.js +162 -0
  35. package/dist/learnings.d.ts +108 -0
  36. package/dist/learnings.js +714 -0
  37. package/dist/license-sig.d.ts +47 -0
  38. package/dist/license-sig.js +104 -0
  39. package/dist/policy.d.ts +131 -0
  40. package/dist/policy.js +182 -0
  41. package/dist/search.d.ts +11 -0
  42. package/dist/search.js +99 -0
  43. package/dist/sessions.d.ts +46 -0
  44. package/dist/sessions.js +153 -0
  45. package/examples/adapters/notion-adapter.js +108 -0
  46. package/examples/adapters/rss-adapter.js +76 -0
  47. package/package.json +87 -0
  48. package/skills/opscontext/SKILL.md +260 -0
@@ -0,0 +1,47 @@
1
+ export declare const LICENSE_PUBLIC_KEY_PEM = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAnWMq7ITUPmC/8yx9XmpYktaWmQtXDOx6R2nqSdibq+Y=\n-----END PUBLIC KEY-----";
2
+ export declare const LICENSE_PUBKEY_FINGERPRINT = "12d0c34c917a47fbed99945d2b7fb439";
3
+ export interface SignableLicensePayload {
4
+ key: string;
5
+ email: string;
6
+ plan: string;
7
+ machineId: string;
8
+ expiresAt: string;
9
+ deltaVersion: string;
10
+ }
11
+ /**
12
+ * Stable serialization of the signable payload. MUST stay byte-identical
13
+ * to the server-side canonicalPayload() in server/src/license-sig.ts.
14
+ * Tests on each side assert a known-input → known-output to catch drift.
15
+ */
16
+ export declare function canonicalPayload(license: SignableLicensePayload): string;
17
+ export type VerifyResult = {
18
+ ok: true;
19
+ mode: "ed25519";
20
+ } | {
21
+ ok: true;
22
+ mode: "legacy-grandfathered";
23
+ warning: string;
24
+ } | {
25
+ ok: false;
26
+ reason: string;
27
+ };
28
+ /**
29
+ * Verify the Ed25519 signature on a license.
30
+ *
31
+ * Three outcomes:
32
+ * - ok=true, mode=ed25519 → signature valid, full trust
33
+ * - ok=true, mode=legacy-grandfathered → pre-Ed25519 SHA-256 hash
34
+ * (64-hex shape); grandfathered
35
+ * until the flag day so existing
36
+ * licensees don't lose access
37
+ * immediately
38
+ * - ok=false, reason=<string> → signature missing / invalid /
39
+ * tampered / wrong keypair
40
+ *
41
+ * Override the public key via CE_LICENSE_PUBLIC_KEY env var (PEM contents)
42
+ * for self-hosters running their own activation server.
43
+ */
44
+ export declare function verifyLicenseSignature(license: SignableLicensePayload & {
45
+ signature: string;
46
+ }, publicKeyPem?: string): VerifyResult;
47
+ //# sourceMappingURL=license-sig.d.ts.map
@@ -0,0 +1,104 @@
1
+ // 🔒 LOCKED [LICENSE-SIG] — 2026-06-10
2
+ // ⛔ NEVER change canonicalPayload()'s key order or field set without
3
+ // bumping a new "sig_v":2 marker AND keeping v1 verification working
4
+ // forever. The byte output of this function is what the Ed25519
5
+ // signature covers; any change breaks every license issued before
6
+ // the change.
7
+ // ⛔ NEVER ship the public key as a mutable variable. It's a constant
8
+ // that pins the client to the production activation server.
9
+ // Self-hosters override via CE_LICENSE_PUBLIC_KEY env var, which
10
+ // is the documented escape hatch.
11
+ // ⛔ NEVER reject legacy SHA-256 signatures silently. Today they're
12
+ // grandfathered with a one-line warning + audit event. The flag
13
+ // day for rejection is a SEPARATE commit, called out in CHANGELOG,
14
+ // and gives existing licensees 30+ days to reactivate.
15
+ // WHY: Audit identified that loadLicense() did NOT verify the
16
+ // signature field — anyone could write ~/.contextengine/license.json
17
+ // with plan:"enterprise" and unlock all PRO tools. Security bug +
18
+ // revenue leak. This module closes it without breaking the ~95
19
+ // weekly install users who activated before this commit landed.
20
+ // FIX: The PAIR of this file is server/src/license-sig.ts.
21
+ // `canonicalPayload()` must be byte-identical between the two.
22
+ // A test on each side asserts a known-input → known-output mapping
23
+ // to catch drift.
24
+ //
25
+ // Ed25519 license signature — verify side (client).
26
+ //
27
+ // Pairs with server/src/license-sig.ts (sign side). Public key below is
28
+ // pinned to the production activation server (api.compr.ch). Self-hosters
29
+ // override with CE_LICENSE_PUBLIC_KEY env var.
30
+ import { createPublicKey, verify } from "crypto";
31
+ // Production Ed25519 public key. Paired private key lives ONLY on the
32
+ // activation server. Public key SHA-256 fingerprint (first 32 hex chars):
33
+ // 12d0c34c917a47fbed99945d2b7fb439
34
+ // (Recompute by running on the server:
35
+ // openssl pkey -in private.pem -pubout -outform DER | openssl dgst -sha256)
36
+ export const LICENSE_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
37
+ MCowBQYDK2VwAyEAnWMq7ITUPmC/8yx9XmpYktaWmQtXDOx6R2nqSdibq+Y=
38
+ -----END PUBLIC KEY-----`;
39
+ export const LICENSE_PUBKEY_FINGERPRINT = "12d0c34c917a47fbed99945d2b7fb439";
40
+ /**
41
+ * Stable serialization of the signable payload. MUST stay byte-identical
42
+ * to the server-side canonicalPayload() in server/src/license-sig.ts.
43
+ * Tests on each side assert a known-input → known-output to catch drift.
44
+ */
45
+ export function canonicalPayload(license) {
46
+ return JSON.stringify({
47
+ key: license.key,
48
+ email: license.email,
49
+ plan: license.plan,
50
+ machineId: license.machineId,
51
+ expiresAt: license.expiresAt,
52
+ deltaVersion: license.deltaVersion,
53
+ });
54
+ }
55
+ /**
56
+ * Verify the Ed25519 signature on a license.
57
+ *
58
+ * Three outcomes:
59
+ * - ok=true, mode=ed25519 → signature valid, full trust
60
+ * - ok=true, mode=legacy-grandfathered → pre-Ed25519 SHA-256 hash
61
+ * (64-hex shape); grandfathered
62
+ * until the flag day so existing
63
+ * licensees don't lose access
64
+ * immediately
65
+ * - ok=false, reason=<string> → signature missing / invalid /
66
+ * tampered / wrong keypair
67
+ *
68
+ * Override the public key via CE_LICENSE_PUBLIC_KEY env var (PEM contents)
69
+ * for self-hosters running their own activation server.
70
+ */
71
+ export function verifyLicenseSignature(license, publicKeyPem = process.env.CE_LICENSE_PUBLIC_KEY || LICENSE_PUBLIC_KEY_PEM) {
72
+ if (!license.signature || license.signature.length === 0) {
73
+ return { ok: false, reason: "signature field missing" };
74
+ }
75
+ // Legacy detection: pre-Ed25519 signatures were SHA-256 hex (64 chars).
76
+ // Real Ed25519 signatures are 64 raw bytes → 88-char base64.
77
+ if (/^[a-f0-9]{64}$/.test(license.signature)) {
78
+ return {
79
+ ok: true,
80
+ mode: "legacy-grandfathered",
81
+ warning: "Legacy SHA-256 license signature accepted (grandfathered). Reactivate to get an Ed25519-signed license — pre-flag-day licenses will be rejected after the cutover.",
82
+ };
83
+ }
84
+ try {
85
+ const pubKey = createPublicKey(publicKeyPem);
86
+ const sigBytes = Buffer.from(license.signature, "base64");
87
+ const payload = Buffer.from(canonicalPayload(license));
88
+ const ok = verify(null, payload, pubKey, sigBytes);
89
+ if (!ok) {
90
+ return {
91
+ ok: false,
92
+ reason: "Ed25519 signature invalid (tampered license, wrong keypair, or canonical-payload drift)",
93
+ };
94
+ }
95
+ return { ok: true, mode: "ed25519" };
96
+ }
97
+ catch (e) {
98
+ return {
99
+ ok: false,
100
+ reason: `signature verify error: ${e instanceof Error ? e.message : String(e)}`,
101
+ };
102
+ }
103
+ }
104
+ //# sourceMappingURL=license-sig.js.map
@@ -0,0 +1,131 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * A regex pattern the secret scanner should match against staged diff content.
4
+ * Optional `paths` glob list scopes the pattern (e.g. JWT pattern only
5
+ * applied to docs/sessions/**\/*.md, the Apec-leak shape).
6
+ */
7
+ export declare const SecretPatternSchema: z.ZodObject<{
8
+ id: z.ZodString;
9
+ pattern: z.ZodString;
10
+ paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
+ severity: z.ZodDefault<z.ZodEnum<{
12
+ warn: "warn";
13
+ block: "block";
14
+ }>>;
15
+ description: z.ZodOptional<z.ZodString>;
16
+ }, z.core.$strip>;
17
+ export type SecretPattern = z.infer<typeof SecretPatternSchema>;
18
+ /**
19
+ * A source-tree subtree that requires a documentation section to stay current.
20
+ * Diff-aware: the gate fires only when commit touches the mapped subtree AND
21
+ * the corresponding doc section's hash is unchanged. Replaces the 4-hour
22
+ * wall-clock timer from the legacy hook.
23
+ */
24
+ export declare const DocCoverageSchema: z.ZodObject<{
25
+ paths: z.ZodArray<z.ZodString>;
26
+ requires_section: z.ZodString;
27
+ severity: z.ZodDefault<z.ZodEnum<{
28
+ warn: "warn";
29
+ block: "block";
30
+ }>>;
31
+ description: z.ZodOptional<z.ZodString>;
32
+ }, z.core.$strip>;
33
+ export type DocCoverage = z.infer<typeof DocCoverageSchema>;
34
+ /**
35
+ * A production host that requires a verification probe within N seconds of
36
+ * a git push. Encodes the "DEPLOY = VERIFY LIVE" rule from CLAUDE.md.
37
+ */
38
+ export declare const DeployVerifyHostSchema: z.ZodObject<{
39
+ host: z.ZodString;
40
+ require_probe: z.ZodString;
41
+ within_seconds: z.ZodDefault<z.ZodNumber>;
42
+ description: z.ZodOptional<z.ZodString>;
43
+ }, z.core.$strip>;
44
+ export type DeployVerifyHost = z.infer<typeof DeployVerifyHostSchema>;
45
+ /**
46
+ * A documented escape hatch for the hook. Beats undocumented `touch` /
47
+ * `--no-verify` workarounds. Bypass token requires a reason and lives in
48
+ * the audit log.
49
+ */
50
+ export declare const BypassTokenSchema: z.ZodObject<{
51
+ id: z.ZodString;
52
+ ttl_seconds: z.ZodDefault<z.ZodNumber>;
53
+ requires_reason_min_length: z.ZodDefault<z.ZodNumber>;
54
+ description: z.ZodOptional<z.ZodString>;
55
+ }, z.core.$strip>;
56
+ export type BypassToken = z.infer<typeof BypassTokenSchema>;
57
+ /**
58
+ * The full policy document — schema version 1.
59
+ */
60
+ export declare const PolicySchema: z.ZodObject<{
61
+ version: z.ZodLiteral<1>;
62
+ extends: z.ZodOptional<z.ZodString>;
63
+ secret_patterns: z.ZodDefault<z.ZodArray<z.ZodObject<{
64
+ id: z.ZodString;
65
+ pattern: z.ZodString;
66
+ paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
67
+ severity: z.ZodDefault<z.ZodEnum<{
68
+ warn: "warn";
69
+ block: "block";
70
+ }>>;
71
+ description: z.ZodOptional<z.ZodString>;
72
+ }, z.core.$strip>>>;
73
+ doc_coverage: z.ZodDefault<z.ZodArray<z.ZodObject<{
74
+ paths: z.ZodArray<z.ZodString>;
75
+ requires_section: z.ZodString;
76
+ severity: z.ZodDefault<z.ZodEnum<{
77
+ warn: "warn";
78
+ block: "block";
79
+ }>>;
80
+ description: z.ZodOptional<z.ZodString>;
81
+ }, z.core.$strip>>>;
82
+ deploy_verify_hosts: z.ZodDefault<z.ZodArray<z.ZodObject<{
83
+ host: z.ZodString;
84
+ require_probe: z.ZodString;
85
+ within_seconds: z.ZodDefault<z.ZodNumber>;
86
+ description: z.ZodOptional<z.ZodString>;
87
+ }, z.core.$strip>>>;
88
+ bypass_tokens: z.ZodDefault<z.ZodArray<z.ZodObject<{
89
+ id: z.ZodString;
90
+ ttl_seconds: z.ZodDefault<z.ZodNumber>;
91
+ requires_reason_min_length: z.ZodDefault<z.ZodNumber>;
92
+ description: z.ZodOptional<z.ZodString>;
93
+ }, z.core.$strip>>>;
94
+ }, z.core.$strip>;
95
+ export type Policy = z.infer<typeof PolicySchema>;
96
+ /**
97
+ * Validate a raw parsed object against the policy schema. Returns either
98
+ * a validated Policy or a structured list of field-level errors.
99
+ */
100
+ export type ValidationResult = {
101
+ ok: true;
102
+ policy: Policy;
103
+ } | {
104
+ ok: false;
105
+ errors: Array<{
106
+ path: string;
107
+ message: string;
108
+ }>;
109
+ };
110
+ export declare function validatePolicy(raw: unknown): ValidationResult;
111
+ /**
112
+ * Parse policy file contents. Currently supports JSON only. YAML support
113
+ * is on the roadmap — purely an ergonomic addition, no runtime difference.
114
+ */
115
+ export declare function parsePolicy(contents: string): ValidationResult;
116
+ /**
117
+ * Load the repo-local policy from .contextengine/policy.json. Returns null
118
+ * when no policy file exists (repos without explicit policy still work —
119
+ * hooks fall back to built-in defaults).
120
+ *
121
+ * Returns ValidationResult so consumers can surface schema errors to the
122
+ * user instead of crashing on a malformed file.
123
+ */
124
+ export declare function loadRepoPolicy(repoRoot: string): ValidationResult | null;
125
+ export declare function repoPolicyPath(repoRoot: string): string;
126
+ export declare function formatPolicySummary(policy: Policy): string;
127
+ export declare function formatValidationErrors(errors: Array<{
128
+ path: string;
129
+ message: string;
130
+ }>): string;
131
+ //# sourceMappingURL=policy.d.ts.map
package/dist/policy.js ADDED
@@ -0,0 +1,182 @@
1
+ // 🔒 LOCKED [POLICY-CONTRACT] — 2026-06-10
2
+ // ⛔ NEVER bump `version: 1` without a migration path that keeps v1
3
+ // policies loadable. Org policy URLs in the wild will pin a version;
4
+ // breaking the loader silently breaks remote policy distribution.
5
+ // ⛔ NEVER add NEW required fields to existing schemas — only optional
6
+ // fields with defaults. Old policies must still validate.
7
+ // WHY: This is the contract that hooks, CI templates, and the future
8
+ // signed-policy-distribution layer will all consume. Schema breakage
9
+ // here cascades into every consumer. The audit's hook-redesign
10
+ // proposal hinges on this single declarative contract replacing 329
11
+ // LOC of inline bash.
12
+ // FIX: To evolve, add `version: 2` schema alongside, dispatch in
13
+ // `parsePolicy()` based on the version field, keep `validatePolicy()`
14
+ // backward-compatible.
15
+ //
16
+ // Declarative policy contract — .contextengine/policy.json at repo root.
17
+ //
18
+ // Hooks (pre-commit, CC PreToolUse, CI templates) consume this single file
19
+ // instead of carrying inline bash. Reviewable in PR, portable across IDE +
20
+ // git + CI layers, signable as an org-distributed bundle later.
21
+ import { z } from "zod";
22
+ import { existsSync, readFileSync } from "fs";
23
+ import { join } from "path";
24
+ // ---------------------------------------------------------------------------
25
+ // Schemas
26
+ // ---------------------------------------------------------------------------
27
+ /**
28
+ * A regex pattern the secret scanner should match against staged diff content.
29
+ * Optional `paths` glob list scopes the pattern (e.g. JWT pattern only
30
+ * applied to docs/sessions/**\/*.md, the Apec-leak shape).
31
+ */
32
+ export const SecretPatternSchema = z.object({
33
+ id: z.string().min(1).describe("Stable identifier for audit-log attribution"),
34
+ pattern: z.string().min(1).describe("ERE regex matched against added lines"),
35
+ paths: z.array(z.string()).optional().describe("Glob patterns scoping the rule; omit for all files"),
36
+ severity: z.enum(["block", "warn"]).default("block"),
37
+ description: z.string().optional(),
38
+ });
39
+ /**
40
+ * A source-tree subtree that requires a documentation section to stay current.
41
+ * Diff-aware: the gate fires only when commit touches the mapped subtree AND
42
+ * the corresponding doc section's hash is unchanged. Replaces the 4-hour
43
+ * wall-clock timer from the legacy hook.
44
+ */
45
+ export const DocCoverageSchema = z.object({
46
+ paths: z.array(z.string()).min(1).describe("Source-tree globs that this rule covers"),
47
+ requires_section: z.string().min(1).describe("Doc path with anchor, e.g. SKILLS.md#protocol-firewall"),
48
+ severity: z.enum(["block", "warn"]).default("block"),
49
+ description: z.string().optional(),
50
+ });
51
+ /**
52
+ * A production host that requires a verification probe within N seconds of
53
+ * a git push. Encodes the "DEPLOY = VERIFY LIVE" rule from CLAUDE.md.
54
+ */
55
+ export const DeployVerifyHostSchema = z.object({
56
+ host: z.string().min(1),
57
+ require_probe: z.string().min(1).describe("Shell command run to verify, e.g. curl -sf https://host/healthz"),
58
+ within_seconds: z.number().int().positive().default(60),
59
+ description: z.string().optional(),
60
+ });
61
+ /**
62
+ * A documented escape hatch for the hook. Beats undocumented `touch` /
63
+ * `--no-verify` workarounds. Bypass token requires a reason and lives in
64
+ * the audit log.
65
+ */
66
+ export const BypassTokenSchema = z.object({
67
+ id: z.string().min(1).describe("Stable identifier for audit-log attribution"),
68
+ ttl_seconds: z.number().int().positive().default(300).describe("How long after issuance the token is valid"),
69
+ requires_reason_min_length: z.number().int().min(0).default(20),
70
+ description: z.string().optional(),
71
+ });
72
+ /**
73
+ * The full policy document — schema version 1.
74
+ */
75
+ export const PolicySchema = z.object({
76
+ version: z.literal(1).describe("Policy schema version. Pin to 1 — bumps require a migration path."),
77
+ extends: z
78
+ .string()
79
+ .url()
80
+ .optional()
81
+ .describe("Org policy URL (HTTPS or git). Signed-bundle distribution is a P1 #5 follow-up."),
82
+ secret_patterns: z.array(SecretPatternSchema).default([]),
83
+ doc_coverage: z.array(DocCoverageSchema).default([]),
84
+ deploy_verify_hosts: z.array(DeployVerifyHostSchema).default([]),
85
+ bypass_tokens: z.array(BypassTokenSchema).default([]),
86
+ });
87
+ export function validatePolicy(raw) {
88
+ const result = PolicySchema.safeParse(raw);
89
+ if (result.success)
90
+ return { ok: true, policy: result.data };
91
+ return {
92
+ ok: false,
93
+ errors: result.error.issues.map((i) => ({
94
+ path: i.path.length ? i.path.join(".") : "(root)",
95
+ message: i.message,
96
+ })),
97
+ };
98
+ }
99
+ /**
100
+ * Parse policy file contents. Currently supports JSON only. YAML support
101
+ * is on the roadmap — purely an ergonomic addition, no runtime difference.
102
+ */
103
+ export function parsePolicy(contents) {
104
+ let raw;
105
+ try {
106
+ raw = JSON.parse(contents);
107
+ }
108
+ catch (e) {
109
+ return {
110
+ ok: false,
111
+ errors: [{ path: "(root)", message: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` }],
112
+ };
113
+ }
114
+ return validatePolicy(raw);
115
+ }
116
+ /**
117
+ * Load the repo-local policy from .contextengine/policy.json. Returns null
118
+ * when no policy file exists (repos without explicit policy still work —
119
+ * hooks fall back to built-in defaults).
120
+ *
121
+ * Returns ValidationResult so consumers can surface schema errors to the
122
+ * user instead of crashing on a malformed file.
123
+ */
124
+ export function loadRepoPolicy(repoRoot) {
125
+ const path = repoPolicyPath(repoRoot);
126
+ if (!existsSync(path))
127
+ return null;
128
+ try {
129
+ const contents = readFileSync(path, "utf-8");
130
+ return parsePolicy(contents);
131
+ }
132
+ catch (e) {
133
+ return {
134
+ ok: false,
135
+ errors: [{ path: "(file)", message: `Cannot read ${path}: ${e instanceof Error ? e.message : String(e)}` }],
136
+ };
137
+ }
138
+ }
139
+ export function repoPolicyPath(repoRoot) {
140
+ return join(repoRoot, ".contextengine", "policy.json");
141
+ }
142
+ // ---------------------------------------------------------------------------
143
+ // Pretty-print
144
+ // ---------------------------------------------------------------------------
145
+ export function formatPolicySummary(policy) {
146
+ const lines = [];
147
+ lines.push(`# ContextEngine policy (v${policy.version})`);
148
+ if (policy.extends) {
149
+ lines.push(`Extends: ${policy.extends} (signed bundle resolution: not yet implemented — P1 #5)`);
150
+ }
151
+ lines.push("");
152
+ lines.push(`Secret patterns: ${policy.secret_patterns.length}`);
153
+ for (const p of policy.secret_patterns) {
154
+ const scope = p.paths?.length ? p.paths.join(", ") : "(all files)";
155
+ lines.push(` - [${p.severity}] ${p.id} → scoped to ${scope}`);
156
+ }
157
+ lines.push("");
158
+ lines.push(`Doc coverage rules: ${policy.doc_coverage.length}`);
159
+ for (const c of policy.doc_coverage) {
160
+ lines.push(` - [${c.severity}] ${c.paths.join(", ")} → ${c.requires_section}`);
161
+ }
162
+ lines.push("");
163
+ lines.push(`Deploy-verify hosts: ${policy.deploy_verify_hosts.length}`);
164
+ for (const h of policy.deploy_verify_hosts) {
165
+ lines.push(` - ${h.host} → probe within ${h.within_seconds}s: ${h.require_probe}`);
166
+ }
167
+ lines.push("");
168
+ lines.push(`Bypass tokens: ${policy.bypass_tokens.length}`);
169
+ for (const b of policy.bypass_tokens) {
170
+ lines.push(` - ${b.id} → TTL ${b.ttl_seconds}s, reason ≥ ${b.requires_reason_min_length} chars`);
171
+ }
172
+ return lines.join("\n");
173
+ }
174
+ export function formatValidationErrors(errors) {
175
+ const lines = [];
176
+ lines.push(`❌ Policy validation failed — ${errors.length} error(s):`);
177
+ for (const e of errors) {
178
+ lines.push(` • ${e.path}: ${e.message}`);
179
+ }
180
+ return lines.join("\n");
181
+ }
182
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1,11 @@
1
+ import { Chunk } from "./ingest.js";
2
+ export interface SearchResult {
3
+ chunk: Chunk;
4
+ score: number;
5
+ }
6
+ /**
7
+ * Search chunks by BM25 keyword relevance.
8
+ * Returns top-k results sorted by score descending.
9
+ */
10
+ export declare function searchChunks(chunks: Chunk[], query: string, topK?: number): SearchResult[];
11
+ //# sourceMappingURL=search.d.ts.map
package/dist/search.js ADDED
@@ -0,0 +1,99 @@
1
+ // LOCKED — verified March 3 2026 — BM25 keyword search with IDF + temporal decay + lock detection
2
+ // DO NOT RE-AUDIT — scoring weights are IP-protected trade secrets
3
+ /**
4
+ * BM25-style keyword search over chunks.
5
+ *
6
+ * v1.10: Upgraded from naive term-overlap to proper BM25 scoring with:
7
+ * - IDF (inverse document frequency) — rare terms score higher
8
+ * - Document length normalization — short focused chunks aren't penalized
9
+ * - Configurable k1 (term frequency saturation) and b (length penalty)
10
+ *
11
+ * Inspired by OpenClaw's FTS5/BM25 approach but pure JS (no SQLite dep).
12
+ */
13
+ // BM25 parameters
14
+ const K1 = 1.5; // Term frequency saturation (1.2-2.0 typical)
15
+ const B = 0.75; // Length normalization factor (0 = no normalization, 1 = full)
16
+ /** Normalize and tokenize a string */
17
+ function tokenize(text) {
18
+ return text
19
+ .toLowerCase()
20
+ .replace(/[^a-z0-9\s_\-./]/g, " ")
21
+ .split(/\s+/)
22
+ .filter((t) => t.length > 1);
23
+ }
24
+ /** Count term occurrences in text */
25
+ function termFrequency(tokens, term) {
26
+ return tokens.filter((t) => t === term || t.includes(term)).length;
27
+ }
28
+ /**
29
+ * Pre-compute IDF values for query terms across the corpus.
30
+ * IDF = log((N - n + 0.5) / (n + 0.5) + 1) where N = total docs, n = docs containing term
31
+ */
32
+ function computeIDF(chunks, queryTokens) {
33
+ const N = chunks.length;
34
+ const idf = new Map();
35
+ for (const term of queryTokens) {
36
+ let docCount = 0;
37
+ for (const chunk of chunks) {
38
+ const text = (chunk.content + " " + chunk.section).toLowerCase();
39
+ if (text.includes(term)) {
40
+ docCount++;
41
+ }
42
+ }
43
+ // BM25 IDF formula
44
+ const val = Math.log((N - docCount + 0.5) / (docCount + 0.5) + 1);
45
+ idf.set(term, val);
46
+ }
47
+ return idf;
48
+ }
49
+ /**
50
+ * Score a chunk against query tokens using BM25.
51
+ */
52
+ function bm25Score(chunk, queryTokens, idf, avgDl) {
53
+ const text = (chunk.content + " " + chunk.section).toLowerCase();
54
+ const docTokens = tokenize(text);
55
+ const dl = docTokens.length;
56
+ let score = 0;
57
+ for (const term of queryTokens) {
58
+ const tf = termFrequency(docTokens, term);
59
+ if (tf === 0)
60
+ continue;
61
+ const termIdf = idf.get(term) || 0;
62
+ // BM25 TF component: tf * (k1 + 1) / (tf + k1 * (1 - b + b * dl / avgDl))
63
+ const tfNorm = (tf * (K1 + 1)) / (tf + K1 * (1 - B + B * (dl / avgDl)));
64
+ score += termIdf * tfNorm;
65
+ }
66
+ // Bonus for matching multiple distinct query terms (proximity signal)
67
+ const distinctMatches = queryTokens.filter((t) => text.includes(t)).length;
68
+ if (distinctMatches > 1) {
69
+ score *= 1 + distinctMatches * 0.15;
70
+ }
71
+ return score;
72
+ }
73
+ function escapeRegex(s) {
74
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
75
+ }
76
+ /**
77
+ * Search chunks by BM25 keyword relevance.
78
+ * Returns top-k results sorted by score descending.
79
+ */
80
+ export function searchChunks(chunks, query, topK = 10) {
81
+ const queryTokens = tokenize(query);
82
+ if (queryTokens.length === 0)
83
+ return [];
84
+ // Pre-compute IDF and average document length
85
+ const idf = computeIDF(chunks, queryTokens);
86
+ const avgDl = chunks.reduce((sum, c) => {
87
+ return sum + tokenize((c.content + " " + c.section).toLowerCase()).length;
88
+ }, 0) / Math.max(chunks.length, 1);
89
+ const scored = [];
90
+ for (const chunk of chunks) {
91
+ const score = bm25Score(chunk, queryTokens, idf, avgDl);
92
+ if (score > 0) {
93
+ scored.push({ chunk, score });
94
+ }
95
+ }
96
+ scored.sort((a, b) => b.score - a.score);
97
+ return scored.slice(0, topK);
98
+ }
99
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1,46 @@
1
+ export interface SessionEntry {
2
+ key: string;
3
+ value: string;
4
+ timestamp: string;
5
+ }
6
+ export interface Session {
7
+ name: string;
8
+ created: string;
9
+ updated: string;
10
+ entries: SessionEntry[];
11
+ }
12
+ /**
13
+ * Save or update a key-value pair in a named session.
14
+ */
15
+ export declare function saveSession(name: string, key: string, value: string): Session;
16
+ /**
17
+ * Load a session by name.
18
+ */
19
+ export declare function loadSession(name: string): Session | null;
20
+ /**
21
+ * List all saved sessions.
22
+ */
23
+ export declare function listSessions(): Array<{
24
+ name: string;
25
+ entries: number;
26
+ created: string;
27
+ updated: string;
28
+ }>;
29
+ /**
30
+ * Delete a session.
31
+ */
32
+ export declare function deleteSession(name: string): boolean;
33
+ /**
34
+ * Format a session for display.
35
+ */
36
+ export declare function formatSession(session: Session): string;
37
+ /**
38
+ * Format session list for display.
39
+ */
40
+ export declare function formatSessionList(sessions: Array<{
41
+ name: string;
42
+ entries: number;
43
+ created: string;
44
+ updated: string;
45
+ }>): string;
46
+ //# sourceMappingURL=sessions.d.ts.map