pi-orchestrator-config 4.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +808 -0
- package/agents/api-documenter.md +32 -0
- package/agents/bash-expert.md +53 -0
- package/agents/code-reviewer-docs.md +145 -0
- package/agents/code-reviewer-guidelines.md +97 -0
- package/agents/code-reviewer-quality.md +142 -0
- package/agents/code-reviewer-security.md +107 -0
- package/agents/code-reviewer-spec.md +164 -0
- package/agents/debugger.md +63 -0
- package/agents/docker-expert.md +40 -0
- package/agents/docs-fetcher.md +48 -0
- package/agents/git-expert.md +63 -0
- package/agents/github-expert.md +83 -0
- package/agents/go-expert.md +62 -0
- package/agents/issue-reviewer-feasibility.md +60 -0
- package/agents/issue-reviewer-scope.md +63 -0
- package/agents/issue-reviewer-spec.md +77 -0
- package/agents/java-expert.md +31 -0
- package/agents/jenkins-expert.md +38 -0
- package/agents/kubernetes-expert.md +40 -0
- package/agents/planner.md +42 -0
- package/agents/python-expert.md +61 -0
- package/agents/reviewer.md +26 -0
- package/agents/scout.md +40 -0
- package/agents/security-auditor.md +254 -0
- package/agents/technical-documentation-writer.md +37 -0
- package/agents/test-automator.md +55 -0
- package/agents/test-runner.md +47 -0
- package/agents/ts-expert.md +23 -0
- package/agents/worker.md +30 -0
- package/extensions/acpx-provider/index.ts +104 -0
- package/extensions/acpx-provider/load-runtime.ts +99 -0
- package/extensions/acpx-provider/runtime-models.ts +40 -0
- package/extensions/cli-provider/agents/claude.ts +128 -0
- package/extensions/cli-provider/agents/cursor.ts +84 -0
- package/extensions/cli-provider/agents/gemini.ts +100 -0
- package/extensions/cli-provider/discover.ts +77 -0
- package/extensions/cli-provider/index.ts +57 -0
- package/extensions/cli-provider/parsers.ts +304 -0
- package/extensions/cli-provider/providers.ts +58 -0
- package/extensions/cli-provider/runner.ts +225 -0
- package/extensions/cli-provider/runtime-models.ts +32 -0
- package/extensions/cli-provider/session-reaper.ts +122 -0
- package/extensions/cli-provider/sessions.ts +533 -0
- package/extensions/cli-provider/shared/discover-cache.ts +235 -0
- package/extensions/cli-provider/types.ts +48 -0
- package/extensions/coms/coms-net-server.ts +1642 -0
- package/extensions/coms/coms-net-wrapper.ts +521 -0
- package/extensions/coms/coms-net.ts +1589 -0
- package/extensions/coms/coms-p2p.ts +1879 -0
- package/extensions/coms/coms-shared.ts +510 -0
- package/extensions/coms/coms-wrapper.ts +138 -0
- package/extensions/coms/index.ts +20 -0
- package/extensions/coms/themeMap.ts +147 -0
- package/extensions/image-gen/image-gen.ts +332 -0
- package/extensions/image-gen/index.ts +14 -0
- package/extensions/orchestrator/agents.ts +134 -0
- package/extensions/orchestrator/ask-user.ts +293 -0
- package/extensions/orchestrator/async-agents.ts +1037 -0
- package/extensions/orchestrator/async-capability.ts +129 -0
- package/extensions/orchestrator/async-runner.ts +324 -0
- package/extensions/orchestrator/async-status-parse.ts +31 -0
- package/extensions/orchestrator/async-status-ui.ts +201 -0
- package/extensions/orchestrator/async-wait.ts +36 -0
- package/extensions/orchestrator/btw.ts +210 -0
- package/extensions/orchestrator/cron-status-format.ts +126 -0
- package/extensions/orchestrator/cron-status-ui.ts +132 -0
- package/extensions/orchestrator/cron.ts +730 -0
- package/extensions/orchestrator/dreaming.ts +355 -0
- package/extensions/orchestrator/enforcement-helpers.ts +530 -0
- package/extensions/orchestrator/enforcement-rules.ts +329 -0
- package/extensions/orchestrator/enforcement.ts +828 -0
- package/extensions/orchestrator/extended-autocomplete.ts +593 -0
- package/extensions/orchestrator/git-helpers.ts +420 -0
- package/extensions/orchestrator/github-autocomplete.ts +250 -0
- package/extensions/orchestrator/icons.ts +17 -0
- package/extensions/orchestrator/index.ts +100 -0
- package/extensions/orchestrator/memory-embeddings.ts +281 -0
- package/extensions/orchestrator/memory-promotion.ts +449 -0
- package/extensions/orchestrator/memory-provenance.ts +80 -0
- package/extensions/orchestrator/memory-query-class.ts +90 -0
- package/extensions/orchestrator/memory-scoring.ts +434 -0
- package/extensions/orchestrator/memory-tools.ts +883 -0
- package/extensions/orchestrator/memory-tree.ts +212 -0
- package/extensions/orchestrator/nvim.ts +169 -0
- package/extensions/orchestrator/overlay-dashboard-utils.ts +25 -0
- package/extensions/orchestrator/overlay-dashboard.ts +512 -0
- package/extensions/orchestrator/pi-config-review-state.ts +303 -0
- package/extensions/orchestrator/preference-extractor.ts +106 -0
- package/extensions/orchestrator/project-settings.ts +468 -0
- package/extensions/orchestrator/promotion-queue.ts +311 -0
- package/extensions/orchestrator/resolve-agent-model.ts +32 -0
- package/extensions/orchestrator/review-ui.ts +204 -0
- package/extensions/orchestrator/rule-placeholders.ts +12 -0
- package/extensions/orchestrator/rules.ts +481 -0
- package/extensions/orchestrator/session-search.ts +243 -0
- package/extensions/orchestrator/session-validation.ts +374 -0
- package/extensions/orchestrator/situation-report.ts +370 -0
- package/extensions/orchestrator/status-bar.ts +31 -0
- package/extensions/orchestrator/status-line.ts +192 -0
- package/extensions/orchestrator/status.ts +110 -0
- package/extensions/orchestrator/subagent-tool.ts +1748 -0
- package/extensions/orchestrator/task-lifecycle.ts +88 -0
- package/extensions/orchestrator/utils.ts +227 -0
- package/extensions/pidash/index.ts +15 -0
- package/extensions/pidash/pidash.ts +941 -0
- package/extensions/pidiff/index.ts +13 -0
- package/extensions/pidiff/pidiff.ts +369 -0
- package/extensions/providers/acpx-driver.ts +479 -0
- package/extensions/providers/built-in-drivers.ts +52 -0
- package/extensions/providers/claude-driver.ts +319 -0
- package/extensions/providers/cursor-acpx-driver.ts +398 -0
- package/extensions/providers/cursor-cli-driver.ts +484 -0
- package/extensions/providers/gemini-driver.ts +302 -0
- package/extensions/providers/index.ts +513 -0
- package/extensions/shared/build-system-prompt.ts +57 -0
- package/extensions/shared/create-runtime-provider.ts +190 -0
- package/extensions/shared/daemon-manager.ts +248 -0
- package/extensions/shared/file-logger.ts +115 -0
- package/extensions/shared/managed-refresh.ts +211 -0
- package/extensions/shared/provider-driver.ts +295 -0
- package/extensions/shared/provider-errors.ts +305 -0
- package/extensions/shared/provider-registry.ts +388 -0
- package/extensions/shared/stream-builder.ts +275 -0
- package/extensions/shared/types.ts +20 -0
- package/extensions/shared/ui/README.md +10 -0
- package/extensions/shared/ui/badge.tsx +52 -0
- package/extensions/shared/ui/button.tsx +58 -0
- package/extensions/shared/ui/collapsible.tsx +21 -0
- package/extensions/shared/ui/dropdown-menu.tsx +266 -0
- package/extensions/shared/ui/input.tsx +20 -0
- package/extensions/shared/ui/popover.tsx +114 -0
- package/extensions/shared/ui/resizable.tsx +48 -0
- package/extensions/shared/ui/scroll-area.tsx +53 -0
- package/extensions/shared/ui/select.tsx +201 -0
- package/extensions/shared/ui/separator.tsx +25 -0
- package/extensions/shared/ui/tooltip.tsx +64 -0
- package/extensions/shared/ws-client.ts +74 -0
- package/package.json +59 -0
- package/prompts/coderabbit-rate-limit.md +95 -0
- package/prompts/create-coms-feature-manager.md +186 -0
- package/prompts/create-skill.md +128 -0
- package/prompts/domain-model.md +109 -0
- package/prompts/external-ai.md +665 -0
- package/prompts/implement-and-review.md +25 -0
- package/prompts/implement.md +26 -0
- package/prompts/issue-review.md +256 -0
- package/prompts/pr-review.md +753 -0
- package/prompts/query-db.md +154 -0
- package/prompts/refine-review.md +208 -0
- package/prompts/release.md +235 -0
- package/prompts/remember.md +33 -0
- package/prompts/review-handler-status.md +27 -0
- package/prompts/review-handler.md +677 -0
- package/prompts/review-local.md +159 -0
- package/prompts/scout-and-plan.md +21 -0
- package/rules/00-orchestrator-core.md +42 -0
- package/rules/05-issue-first-workflow.md +119 -0
- package/rules/10-agent-routing.md +58 -0
- package/rules/15-mcp-launchpad.md +46 -0
- package/rules/20-code-review-loop.md +234 -0
- package/rules/25-documentation-updates.md +15 -0
- package/rules/30-prompt-templates.md +19 -0
- package/rules/35-memory.md +240 -0
- package/rules/40-critical-rules.md +124 -0
- package/rules/45-file-preview.md +29 -0
- package/rules/50-agent-bug-reporting.md +90 -0
- package/rules/55-coms-protocol.md +63 -0
- package/rules/60-task-tracking.md +66 -0
- package/skills/conflict-resolver/SKILL.md +213 -0
- package/skills/skill-creator/SKILL.md +85 -0
- package/skills/systematic-debugging/SKILL.md +353 -0
- package/skills/test-driven-development/SKILL.md +335 -0
- package/skills/update-agents-md/SKILL.md +282 -0
- package/templates/coms-feature-manager-prompt.md +313 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orchestrator Extension for pi
|
|
3
|
+
*
|
|
4
|
+
* Bundles:
|
|
5
|
+
* - Subagent tool (based on pi's subagent example, with package agent discovery)
|
|
6
|
+
* - Enforcement handlers (python/pip, git protection, dangerous commands)
|
|
7
|
+
* - Rule injection & memory loading (before_agent_start)
|
|
8
|
+
* - Slash commands (/btw, /async-status, /dream-auto)
|
|
9
|
+
* - Notifications and status line
|
|
10
|
+
* - Session validation (required tools check)
|
|
11
|
+
* - Async agent infrastructure
|
|
12
|
+
* - ask_user tool
|
|
13
|
+
* - Memory dreaming (background consolidation)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { registerAskUser } from "./ask-user.js";
|
|
18
|
+
import { registerAsyncAgents } from "./async-agents.js";
|
|
19
|
+
import { registerBtw } from "./btw.js";
|
|
20
|
+
import { registerDreaming } from "./dreaming.js";
|
|
21
|
+
import { registerEnforcement } from "./enforcement.js";
|
|
22
|
+
import { registerProjectSettings } from "./project-settings.js";
|
|
23
|
+
import { registerRules } from "./rules.js";
|
|
24
|
+
import { registerSessionValidation } from "./session-validation.js";
|
|
25
|
+
import { registerStatusLine } from "./status-line.js";
|
|
26
|
+
import { registerSubagentTool } from "./subagent-tool.js";
|
|
27
|
+
import { registerGithubAutocomplete } from "./github-autocomplete.js";
|
|
28
|
+
import { registerExtendedAutocomplete } from "./extended-autocomplete.js";
|
|
29
|
+
import { registerCron } from "./cron.js";
|
|
30
|
+
import { registerStatus } from "./status.js";
|
|
31
|
+
import { registerNvim } from "./nvim.js";
|
|
32
|
+
import { registerPreferenceExtractor } from "./preference-extractor.js";
|
|
33
|
+
import { registerMemoryTools } from "./memory-tools.js";
|
|
34
|
+
import { registerReviewUI } from "./review-ui.js";
|
|
35
|
+
import { registerSessionSearch } from "./session-search.js";
|
|
36
|
+
import { ensureGitSshTimeout, isRunningInContainer, terminalNotify } from "./utils.js";
|
|
37
|
+
|
|
38
|
+
const IN_CONTAINER = isRunningInContainer();
|
|
39
|
+
ensureGitSshTimeout();
|
|
40
|
+
|
|
41
|
+
// Shared command handler registry — pidash uses this to execute commands from the browser
|
|
42
|
+
export const commandHandlerRegistry = new Map<string, (args: string, ctx: any) => Promise<void>>();
|
|
43
|
+
|
|
44
|
+
// Latest ExtensionCommandContext captured from any command handler.
|
|
45
|
+
// Commands provide the full context (switchSession, newSession, etc.)
|
|
46
|
+
// while session_start only provides basic ExtensionContext.
|
|
47
|
+
export let latestCommandCtx: any = null;
|
|
48
|
+
|
|
49
|
+
export default function (pi: ExtensionAPI) {
|
|
50
|
+
// Wrap registerCommand to capture all handler functions AND command context.
|
|
51
|
+
// Any command the user runs upgrades latestCommandCtx to a real ExtensionCommandContext
|
|
52
|
+
// which has switchSession()/newSession() methods.
|
|
53
|
+
const originalRegisterCommand = pi.registerCommand.bind(pi);
|
|
54
|
+
pi.registerCommand = (name: string, options: any) => {
|
|
55
|
+
if (options?.handler) {
|
|
56
|
+
const origHandler = options.handler;
|
|
57
|
+
options.handler = async (args: string, ctx: any) => {
|
|
58
|
+
latestCommandCtx = ctx;
|
|
59
|
+
pi.events.emit("pidash:command-ctx", ctx);
|
|
60
|
+
return origHandler(args, ctx);
|
|
61
|
+
};
|
|
62
|
+
commandHandlerRegistry.set(name, options.handler);
|
|
63
|
+
// Notify pidash extension about new command handler
|
|
64
|
+
pi.events.emit("pidash:register-command", { name, handler: options.handler });
|
|
65
|
+
}
|
|
66
|
+
return originalRegisterCommand(name, options);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Replay all registered command handlers when pidash requests them
|
|
70
|
+
// (handles extension load order — pidash may load after orchestrator)
|
|
71
|
+
pi.events.on("pidash:request-commands", () => {
|
|
72
|
+
for (const [name, handler] of commandHandlerRegistry) {
|
|
73
|
+
pi.events.emit("pidash:register-command", { name, handler });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Extended autocomplete must register FIRST — it wraps registerCommand
|
|
78
|
+
// to inject getArgumentCompletions before other modules register their commands.
|
|
79
|
+
registerExtendedAutocomplete(pi);
|
|
80
|
+
|
|
81
|
+
registerAskUser(pi, terminalNotify);
|
|
82
|
+
const { spawnAsyncAgent, killAsyncAgent, getAsyncJobs } = registerAsyncAgents(pi, terminalNotify);
|
|
83
|
+
registerSubagentTool(pi, spawnAsyncAgent, killAsyncAgent);
|
|
84
|
+
registerProjectSettings(pi);
|
|
85
|
+
registerEnforcement(pi, IN_CONTAINER);
|
|
86
|
+
registerReviewUI(pi);
|
|
87
|
+
registerRules(pi, getAsyncJobs);
|
|
88
|
+
|
|
89
|
+
registerStatusLine(pi, IN_CONTAINER, terminalNotify);
|
|
90
|
+
registerBtw(pi);
|
|
91
|
+
registerDreaming(pi, spawnAsyncAgent);
|
|
92
|
+
const { getCronTasks } = registerCron(pi, spawnAsyncAgent);
|
|
93
|
+
registerSessionValidation(pi);
|
|
94
|
+
registerGithubAutocomplete(pi);
|
|
95
|
+
registerStatus(pi, IN_CONTAINER, getAsyncJobs, getCronTasks);
|
|
96
|
+
registerNvim(pi);
|
|
97
|
+
registerPreferenceExtractor(pi);
|
|
98
|
+
registerMemoryTools(pi);
|
|
99
|
+
registerSessionSearch(pi);
|
|
100
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-embeddings.ts — Vector embedding support for memory search
|
|
3
|
+
*
|
|
4
|
+
* Embeds memory entries and search queries using Xenova/bge-small-en-v1.5 via
|
|
5
|
+
* @huggingface/transformers (in-process ONNX, no Python, no subprocess).
|
|
6
|
+
* Stores embeddings in .pi/memory/embeddings.json.
|
|
7
|
+
*
|
|
8
|
+
* Falls back gracefully when the model is unavailable — callers always get a
|
|
9
|
+
* result (possibly empty) and never throw.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
|
|
16
|
+
const EMBEDDING_DIM = 384;
|
|
17
|
+
export const EMBEDDING_POOLING = "mean";
|
|
18
|
+
|
|
19
|
+
// Lazy-loaded pipeline — initialized once per process
|
|
20
|
+
let pipelineInstance: any = null;
|
|
21
|
+
let pipelineLoading: Promise<any> | null = null;
|
|
22
|
+
let modelUnavailable = false;
|
|
23
|
+
|
|
24
|
+
// In-memory embedding cache for queries
|
|
25
|
+
const queryCache = new Map<string, number[]>();
|
|
26
|
+
|
|
27
|
+
interface EmbeddingStore {
|
|
28
|
+
model: string;
|
|
29
|
+
dim: number;
|
|
30
|
+
pooling?: string;
|
|
31
|
+
entries: Record<string, number[]>; // text hash → vector
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getStorePath(cwd: string): string {
|
|
35
|
+
return join(cwd, ".pi", "memory", "embeddings.json");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadStore(cwd: string): EmbeddingStore {
|
|
39
|
+
const storePath = getStorePath(cwd);
|
|
40
|
+
if (existsSync(storePath)) {
|
|
41
|
+
try {
|
|
42
|
+
const store = JSON.parse(readFileSync(storePath, "utf-8")) as EmbeddingStore;
|
|
43
|
+
// Invalidate cache when pooling strategy changes — CLS and mean vectors are incompatible
|
|
44
|
+
if (store.pooling !== EMBEDDING_POOLING) {
|
|
45
|
+
console.debug(`[memory-embeddings] pooling changed (${store.pooling ?? "cls"} → ${EMBEDDING_POOLING}), clearing ${Object.keys(store.entries).length} cached embeddings`);
|
|
46
|
+
store.entries = {};
|
|
47
|
+
store.pooling = EMBEDDING_POOLING;
|
|
48
|
+
queryCache.clear();
|
|
49
|
+
saveStore(cwd, store);
|
|
50
|
+
}
|
|
51
|
+
return store;
|
|
52
|
+
} catch {
|
|
53
|
+
// Corrupted file — start fresh
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { model: "Xenova/bge-small-en-v1.5", dim: EMBEDDING_DIM, pooling: EMBEDDING_POOLING, entries: {} };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function saveStore(cwd: string, store: EmbeddingStore): void {
|
|
60
|
+
try {
|
|
61
|
+
const dir = join(cwd, ".pi", "memory");
|
|
62
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
63
|
+
const target = getStorePath(cwd);
|
|
64
|
+
const tmp = target + ".tmp";
|
|
65
|
+
writeFileSync(tmp, JSON.stringify(store), "utf-8");
|
|
66
|
+
renameSync(tmp, target);
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
console.debug("[memory-embeddings] save failed:", err?.message?.slice(0, 100));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** SHA-256 hash for embedding keys — includes category to avoid cross-category collisions */
|
|
73
|
+
function embeddingKey(text: string, category?: string): string {
|
|
74
|
+
const input = category ? `[${category}] ${text}` : text;
|
|
75
|
+
return createHash("sha256").update(input).digest("hex").slice(0, 16);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get or initialize the embedding pipeline.
|
|
80
|
+
* Lazy-loaded on first use, cached for process lifetime.
|
|
81
|
+
*/
|
|
82
|
+
async function getPipeline(): Promise<any> {
|
|
83
|
+
if (modelUnavailable) return null;
|
|
84
|
+
if (pipelineInstance) return pipelineInstance;
|
|
85
|
+
if (pipelineLoading) return pipelineLoading;
|
|
86
|
+
|
|
87
|
+
pipelineLoading = (async () => {
|
|
88
|
+
try {
|
|
89
|
+
const { pipeline } = await import("@huggingface/transformers");
|
|
90
|
+
const extractor = await pipeline("feature-extraction", "Xenova/bge-small-en-v1.5", {
|
|
91
|
+
quantized: true,
|
|
92
|
+
});
|
|
93
|
+
pipelineInstance = extractor;
|
|
94
|
+
return extractor;
|
|
95
|
+
} catch (err: any) {
|
|
96
|
+
console.debug("[memory-embeddings] model init failed:", err?.message?.slice(0, 100));
|
|
97
|
+
// Don't permanently disable — allow retry on next call
|
|
98
|
+
return null;
|
|
99
|
+
} finally {
|
|
100
|
+
pipelineLoading = null;
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
|
|
104
|
+
return pipelineLoading;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Embed texts using the in-process ONNX model.
|
|
109
|
+
* Returns array of vectors, or null if model is unavailable.
|
|
110
|
+
*/
|
|
111
|
+
async function embed(texts: string[]): Promise<number[][] | null> {
|
|
112
|
+
if (texts.length === 0) return null;
|
|
113
|
+
|
|
114
|
+
const extractor = await getPipeline();
|
|
115
|
+
if (!extractor) return null;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// Mean pooling produces more discriminative embeddings for sentence-to-sentence
|
|
119
|
+
// similarity (dedup) vs CLS which over-weights structural patterns.
|
|
120
|
+
const result = await extractor(texts, { pooling: EMBEDDING_POOLING, normalize: true });
|
|
121
|
+
const vectors: number[][] = [];
|
|
122
|
+
for (let i = 0; i < texts.length; i++) {
|
|
123
|
+
vectors.push(Array.from(result[i].data as Float32Array));
|
|
124
|
+
}
|
|
125
|
+
return vectors;
|
|
126
|
+
} catch (err: any) {
|
|
127
|
+
console.debug("[memory-embeddings] embed failed:", err?.message?.slice(0, 100));
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Embed a single query text with caching.
|
|
134
|
+
*/
|
|
135
|
+
async function embedQuery(query: string): Promise<number[] | null> {
|
|
136
|
+
const cached = queryCache.get(query);
|
|
137
|
+
if (cached) return cached;
|
|
138
|
+
|
|
139
|
+
const vectors = await embed([query]);
|
|
140
|
+
if (!vectors || vectors.length === 0) return null;
|
|
141
|
+
|
|
142
|
+
queryCache.set(query, vectors[0]);
|
|
143
|
+
// Cap cache size
|
|
144
|
+
if (queryCache.size > 200) {
|
|
145
|
+
const firstKey = queryCache.keys().next().value;
|
|
146
|
+
if (firstKey !== undefined) queryCache.delete(firstKey);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return vectors[0];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function cosineSimilarity(a: number[], b: number[]): number {
|
|
153
|
+
const len = Math.min(a.length, b.length);
|
|
154
|
+
if (len === 0) return 0;
|
|
155
|
+
let dot = 0, normA = 0, normB = 0;
|
|
156
|
+
for (let i = 0; i < len; i++) {
|
|
157
|
+
dot += a[i] * b[i];
|
|
158
|
+
normA += a[i] * a[i];
|
|
159
|
+
normB += b[i] * b[i];
|
|
160
|
+
}
|
|
161
|
+
if (normA === 0 || normB === 0) return 0;
|
|
162
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Initialize the embedding model (call at session start).
|
|
169
|
+
* Downloads the model on first run (~50MB), loads ONNX (~2.7s).
|
|
170
|
+
* Subsequent calls are instant (cached in process memory).
|
|
171
|
+
*/
|
|
172
|
+
export async function initEmbeddings(): Promise<boolean> {
|
|
173
|
+
const pipeline = await getPipeline();
|
|
174
|
+
return pipeline !== null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Embed a single memory entry and store it. Called on memory_add.
|
|
179
|
+
* No-op if model is unavailable.
|
|
180
|
+
*/
|
|
181
|
+
export async function embedEntry(cwd: string, text: string, category?: string): Promise<void> {
|
|
182
|
+
try {
|
|
183
|
+
const store = loadStore(cwd);
|
|
184
|
+
const hash = embeddingKey(text, category);
|
|
185
|
+
if (store.entries[hash]) return; // Already embedded
|
|
186
|
+
|
|
187
|
+
const vectors = await embed([text]);
|
|
188
|
+
if (!vectors || vectors.length === 0) return;
|
|
189
|
+
|
|
190
|
+
store.entries[hash] = vectors[0];
|
|
191
|
+
saveStore(cwd, store);
|
|
192
|
+
} catch (err: any) {
|
|
193
|
+
console.debug("[memory-embeddings] embedEntry failed:", err?.message?.slice(0, 100));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Remove an entry's embedding from the store. Called on memory_remove.
|
|
199
|
+
*/
|
|
200
|
+
export function removeEmbedding(cwd: string, text: string, category?: string): void {
|
|
201
|
+
try {
|
|
202
|
+
const store = loadStore(cwd);
|
|
203
|
+
const hash = embeddingKey(text, category);
|
|
204
|
+
if (store.entries[hash]) {
|
|
205
|
+
delete store.entries[hash];
|
|
206
|
+
saveStore(cwd, store);
|
|
207
|
+
}
|
|
208
|
+
} catch (err: any) {
|
|
209
|
+
console.debug("[memory-embeddings] removeEmbedding failed:", err?.message?.slice(0, 100));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Search memory entries by vector similarity.
|
|
215
|
+
* Returns entries sorted by similarity score (highest first).
|
|
216
|
+
* Returns empty array if model is unavailable or no embeddings exist.
|
|
217
|
+
*/
|
|
218
|
+
export async function vectorSearch(
|
|
219
|
+
cwd: string,
|
|
220
|
+
query: string,
|
|
221
|
+
entries: { text: string; category: string; pinned: boolean }[],
|
|
222
|
+
topK: number = 20,
|
|
223
|
+
): Promise<{ text: string; category: string; pinned: boolean; similarity: number }[]> {
|
|
224
|
+
const store = loadStore(cwd);
|
|
225
|
+
if (Object.keys(store.entries).length === 0) return [];
|
|
226
|
+
|
|
227
|
+
const queryVec = await embedQuery(query);
|
|
228
|
+
if (!queryVec) return [];
|
|
229
|
+
|
|
230
|
+
// Score all entries that have embeddings
|
|
231
|
+
const scored: { text: string; category: string; pinned: boolean; similarity: number }[] = [];
|
|
232
|
+
for (const entry of entries) {
|
|
233
|
+
const hash = embeddingKey(entry.text, entry.category);
|
|
234
|
+
const entryVec = store.entries[hash];
|
|
235
|
+
if (!entryVec) continue;
|
|
236
|
+
const similarity = cosineSimilarity(queryVec, entryVec);
|
|
237
|
+
scored.push({ ...entry, similarity });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
scored.sort((a, b) => b.similarity - a.similarity);
|
|
241
|
+
return scored.slice(0, topK);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Embed all entries that don't have embeddings yet.
|
|
246
|
+
* Used for initial migration when vector search is first enabled.
|
|
247
|
+
* Returns count of newly embedded entries.
|
|
248
|
+
*/
|
|
249
|
+
export async function embedMissing(
|
|
250
|
+
cwd: string,
|
|
251
|
+
entries: { text: string; category: string }[],
|
|
252
|
+
): Promise<number> {
|
|
253
|
+
const store = loadStore(cwd);
|
|
254
|
+
const missing = entries.filter(e => !store.entries[embeddingKey(e.text, e.category)]);
|
|
255
|
+
if (missing.length === 0) return 0;
|
|
256
|
+
|
|
257
|
+
// Batch embed all missing entries
|
|
258
|
+
const texts = missing.map(e => e.text);
|
|
259
|
+
const vectors = await embed(texts);
|
|
260
|
+
if (!vectors || vectors.length !== texts.length) return 0;
|
|
261
|
+
|
|
262
|
+
let stored = 0;
|
|
263
|
+
for (let i = 0; i < missing.length; i++) {
|
|
264
|
+
const vec = vectors[i];
|
|
265
|
+
// Validate vector dimensions — skip corrupted results
|
|
266
|
+
if (!Array.isArray(vec) || vec.length !== EMBEDDING_DIM) continue;
|
|
267
|
+
store.entries[embeddingKey(missing[i].text, missing[i].category)] = vec;
|
|
268
|
+
stored++;
|
|
269
|
+
}
|
|
270
|
+
saveStore(cwd, store);
|
|
271
|
+
return stored;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check if vector search is available (model can load).
|
|
276
|
+
*/
|
|
277
|
+
export async function isVectorSearchAvailable(): Promise<boolean> {
|
|
278
|
+
if (modelUnavailable) return false;
|
|
279
|
+
const pipeline = await getPipeline();
|
|
280
|
+
return pipeline !== null;
|
|
281
|
+
}
|