@intent-systems/nexus 2026.1.5-3 → 2026.1.5-4

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 (39) hide show
  1. package/dist/capabilities/detector.js +214 -0
  2. package/dist/capabilities/registry.js +98 -0
  3. package/dist/channels/location.js +44 -0
  4. package/dist/channels/web/index.js +2 -0
  5. package/dist/control-plane/broker/broker.js +969 -0
  6. package/dist/control-plane/compaction.js +284 -0
  7. package/dist/control-plane/factory.js +31 -0
  8. package/dist/control-plane/index.js +10 -0
  9. package/dist/control-plane/odu/agents.js +187 -0
  10. package/dist/control-plane/odu/interaction-tools.js +196 -0
  11. package/dist/control-plane/odu/prompt-loader.js +95 -0
  12. package/dist/control-plane/odu/runtime.js +467 -0
  13. package/dist/control-plane/odu/types.js +6 -0
  14. package/dist/control-plane/odu-control-plane.js +314 -0
  15. package/dist/control-plane/single-agent.js +249 -0
  16. package/dist/control-plane/types.js +11 -0
  17. package/dist/credentials/store.js +323 -0
  18. package/dist/logging/redact.js +109 -0
  19. package/dist/markdown/fences.js +58 -0
  20. package/dist/memory/embeddings.js +146 -0
  21. package/dist/memory/index.js +382 -0
  22. package/dist/memory/internal.js +163 -0
  23. package/dist/pairing/pairing-store.js +194 -0
  24. package/dist/plugins/cli.js +42 -0
  25. package/dist/plugins/discovery.js +253 -0
  26. package/dist/plugins/install.js +181 -0
  27. package/dist/plugins/loader.js +290 -0
  28. package/dist/plugins/registry.js +105 -0
  29. package/dist/plugins/status.js +29 -0
  30. package/dist/plugins/tools.js +39 -0
  31. package/dist/plugins/types.js +1 -0
  32. package/dist/routing/resolve-route.js +144 -0
  33. package/dist/routing/session-key.js +63 -0
  34. package/dist/utils/provider-utils.js +28 -0
  35. package/package.json +4 -29
  36. package/patches/@mariozechner__pi-ai.patch +215 -0
  37. package/patches/playwright-core@1.57.0.patch +13 -0
  38. package/patches/qrcode-terminal.patch +12 -0
  39. package/scripts/postinstall.js +202 -0
@@ -0,0 +1,382 @@
1
+ import fs from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ import chokidar from "chokidar";
5
+ import { resolveAgentDir, resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
6
+ import { resolveMemorySearchConfig } from "../agents/memory-search.js";
7
+ import { resolveUserPath, truncateUtf16Safe } from "../utils.js";
8
+ import { createEmbeddingProvider, } from "./embeddings.js";
9
+ import { buildFileEntry, chunkMarkdown, cosineSimilarity, ensureDir, hashText, isMemoryPath, listMemoryFiles, normalizeRelPath, parseEmbedding, } from "./internal.js";
10
+ const require = createRequire(import.meta.url);
11
+ function requireNodeSqlite() {
12
+ const onWarning = (warning) => {
13
+ if (warning.name === "ExperimentalWarning" &&
14
+ warning.message?.includes("SQLite is an experimental feature")) {
15
+ return;
16
+ }
17
+ process.stderr.write(`${warning.stack ?? warning.toString()}\n`);
18
+ };
19
+ process.on("warning", onWarning);
20
+ try {
21
+ return require("node:sqlite");
22
+ }
23
+ finally {
24
+ process.off("warning", onWarning);
25
+ }
26
+ }
27
+ const META_KEY = "memory_index_meta_v1";
28
+ const SNIPPET_MAX_CHARS = 700;
29
+ const INDEX_CACHE = new Map();
30
+ export class MemoryIndexManager {
31
+ cacheKey;
32
+ cfg;
33
+ agentId;
34
+ workspaceDir;
35
+ settings;
36
+ provider;
37
+ requestedProvider;
38
+ fallbackReason;
39
+ db;
40
+ watcher = null;
41
+ watchTimer = null;
42
+ intervalTimer = null;
43
+ closed = false;
44
+ dirty = false;
45
+ sessionWarm = new Set();
46
+ syncing = null;
47
+ static async get(params) {
48
+ const { cfg, agentId } = params;
49
+ const settings = resolveMemorySearchConfig(cfg, agentId);
50
+ if (!settings)
51
+ return null;
52
+ const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
53
+ const key = `${agentId}:${workspaceDir}:${JSON.stringify(settings)}`;
54
+ const existing = INDEX_CACHE.get(key);
55
+ if (existing)
56
+ return existing;
57
+ const providerResult = await createEmbeddingProvider({
58
+ config: cfg,
59
+ agentDir: resolveAgentDir(cfg, agentId),
60
+ provider: settings.provider,
61
+ remote: settings.remote,
62
+ model: settings.model,
63
+ fallback: settings.fallback,
64
+ local: settings.local,
65
+ });
66
+ const manager = new MemoryIndexManager({
67
+ cacheKey: key,
68
+ cfg,
69
+ agentId,
70
+ workspaceDir,
71
+ settings,
72
+ providerResult,
73
+ });
74
+ INDEX_CACHE.set(key, manager);
75
+ return manager;
76
+ }
77
+ constructor(params) {
78
+ this.cacheKey = params.cacheKey;
79
+ this.cfg = params.cfg;
80
+ this.agentId = params.agentId;
81
+ this.workspaceDir = params.workspaceDir;
82
+ this.settings = params.settings;
83
+ this.provider = params.providerResult.provider;
84
+ this.requestedProvider = params.providerResult.requestedProvider;
85
+ this.fallbackReason = params.providerResult.fallbackReason;
86
+ this.db = this.openDatabase();
87
+ this.ensureSchema();
88
+ this.ensureWatcher();
89
+ this.ensureIntervalSync();
90
+ this.dirty = true;
91
+ }
92
+ async warmSession(sessionKey) {
93
+ if (!this.settings.sync.onSessionStart)
94
+ return;
95
+ const key = sessionKey?.trim() || "";
96
+ if (key && this.sessionWarm.has(key))
97
+ return;
98
+ await this.sync({ reason: "session-start" });
99
+ if (key)
100
+ this.sessionWarm.add(key);
101
+ }
102
+ async search(query, opts) {
103
+ await this.warmSession(opts?.sessionKey);
104
+ if (this.settings.sync.onSearch && this.dirty) {
105
+ await this.sync({ reason: "search" });
106
+ }
107
+ const cleaned = query.trim();
108
+ if (!cleaned)
109
+ return [];
110
+ const queryVec = await this.provider.embedQuery(cleaned);
111
+ if (queryVec.length === 0)
112
+ return [];
113
+ const candidates = this.listChunks();
114
+ const scored = candidates
115
+ .map((chunk) => ({
116
+ chunk,
117
+ score: cosineSimilarity(queryVec, chunk.embedding),
118
+ }))
119
+ .filter((entry) => Number.isFinite(entry.score));
120
+ const minScore = opts?.minScore ?? this.settings.query.minScore;
121
+ const maxResults = opts?.maxResults ?? this.settings.query.maxResults;
122
+ return scored
123
+ .filter((entry) => entry.score >= minScore)
124
+ .sort((a, b) => b.score - a.score)
125
+ .slice(0, maxResults)
126
+ .map((entry) => ({
127
+ path: entry.chunk.path,
128
+ startLine: entry.chunk.startLine,
129
+ endLine: entry.chunk.endLine,
130
+ score: entry.score,
131
+ snippet: truncateUtf16Safe(entry.chunk.text, SNIPPET_MAX_CHARS),
132
+ }));
133
+ }
134
+ async readFile(params) {
135
+ const relPath = normalizeRelPath(params.relPath);
136
+ if (!relPath)
137
+ return { text: "", error: "Invalid path" };
138
+ if (!isMemoryPath(relPath)) {
139
+ return { text: "", error: "Path must be MEMORY.md or memory/*.md" };
140
+ }
141
+ const absPath = path.join(this.workspaceDir, relPath);
142
+ try {
143
+ const content = await fs.readFile(absPath, "utf-8");
144
+ const lines = content.split("\n");
145
+ const start = Math.max(1, params.from ?? 1);
146
+ const count = Math.max(1, params.lines ?? lines.length);
147
+ const slice = lines.slice(start - 1, start - 1 + count);
148
+ return { text: slice.join("\n"), path: relPath };
149
+ }
150
+ catch (err) {
151
+ const message = err instanceof Error ? err.message : String(err);
152
+ return { text: "", error: message };
153
+ }
154
+ }
155
+ status() {
156
+ const files = this.db.prepare(`SELECT COUNT(*) as c FROM files`).get();
157
+ const chunks = this.db.prepare(`SELECT COUNT(*) as c FROM chunks`).get();
158
+ return {
159
+ files: files?.c ?? 0,
160
+ chunks: chunks?.c ?? 0,
161
+ dirty: this.dirty,
162
+ workspaceDir: this.workspaceDir,
163
+ dbPath: this.settings.store.path,
164
+ provider: this.provider.id,
165
+ model: this.provider.model,
166
+ requestedProvider: this.requestedProvider,
167
+ fallback: this.fallbackReason ? { from: "local", reason: this.fallbackReason } : undefined,
168
+ };
169
+ }
170
+ async close() {
171
+ if (this.closed)
172
+ return;
173
+ this.closed = true;
174
+ if (this.watchTimer) {
175
+ clearTimeout(this.watchTimer);
176
+ this.watchTimer = null;
177
+ }
178
+ if (this.intervalTimer) {
179
+ clearInterval(this.intervalTimer);
180
+ this.intervalTimer = null;
181
+ }
182
+ if (this.watcher) {
183
+ await this.watcher.close();
184
+ this.watcher = null;
185
+ }
186
+ this.db.close();
187
+ INDEX_CACHE.delete(this.cacheKey);
188
+ }
189
+ openDatabase() {
190
+ const dbPath = resolveUserPath(this.settings.store.path);
191
+ const dir = path.dirname(dbPath);
192
+ ensureDir(dir);
193
+ const { DatabaseSync } = requireNodeSqlite();
194
+ return new DatabaseSync(dbPath);
195
+ }
196
+ ensureSchema() {
197
+ this.db.exec(`
198
+ CREATE TABLE IF NOT EXISTS meta (
199
+ key TEXT PRIMARY KEY,
200
+ value TEXT NOT NULL
201
+ );
202
+ `);
203
+ this.db.exec(`
204
+ CREATE TABLE IF NOT EXISTS files (
205
+ path TEXT PRIMARY KEY,
206
+ hash TEXT NOT NULL,
207
+ mtime INTEGER NOT NULL,
208
+ size INTEGER NOT NULL
209
+ );
210
+ `);
211
+ this.db.exec(`
212
+ CREATE TABLE IF NOT EXISTS chunks (
213
+ id TEXT PRIMARY KEY,
214
+ path TEXT NOT NULL,
215
+ start_line INTEGER NOT NULL,
216
+ end_line INTEGER NOT NULL,
217
+ hash TEXT NOT NULL,
218
+ model TEXT NOT NULL,
219
+ text TEXT NOT NULL,
220
+ embedding TEXT NOT NULL,
221
+ updated_at INTEGER NOT NULL
222
+ );
223
+ `);
224
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_chunks_path ON chunks(path);`);
225
+ }
226
+ ensureWatcher() {
227
+ if (!this.settings.sync.watch || this.watcher)
228
+ return;
229
+ const watchPaths = [
230
+ path.join(this.workspaceDir, "MEMORY.md"),
231
+ path.join(this.workspaceDir, "memory"),
232
+ ];
233
+ this.watcher = chokidar.watch(watchPaths, {
234
+ ignoreInitial: true,
235
+ awaitWriteFinish: {
236
+ stabilityThreshold: this.settings.sync.watchDebounceMs,
237
+ pollInterval: 100,
238
+ },
239
+ });
240
+ const markDirty = () => {
241
+ this.dirty = true;
242
+ this.scheduleWatchSync();
243
+ };
244
+ this.watcher.on("add", markDirty);
245
+ this.watcher.on("change", markDirty);
246
+ this.watcher.on("unlink", markDirty);
247
+ }
248
+ ensureIntervalSync() {
249
+ const minutes = this.settings.sync.intervalMinutes;
250
+ if (!minutes || minutes <= 0 || this.intervalTimer)
251
+ return;
252
+ const ms = minutes * 60 * 1000;
253
+ this.intervalTimer = setInterval(() => {
254
+ void this.sync({ reason: "interval" });
255
+ }, ms);
256
+ }
257
+ scheduleWatchSync() {
258
+ if (!this.settings.sync.watch)
259
+ return;
260
+ if (this.watchTimer)
261
+ clearTimeout(this.watchTimer);
262
+ this.watchTimer = setTimeout(() => {
263
+ this.watchTimer = null;
264
+ void this.sync({ reason: "watch" });
265
+ }, this.settings.sync.watchDebounceMs);
266
+ }
267
+ listChunks() {
268
+ const rows = this.db
269
+ .prepare(`SELECT path, start_line, end_line, text, embedding FROM chunks WHERE model = ?`)
270
+ .all(this.provider.model);
271
+ return rows.map((row) => ({
272
+ path: row.path,
273
+ startLine: row.start_line,
274
+ endLine: row.end_line,
275
+ text: row.text,
276
+ embedding: parseEmbedding(row.embedding),
277
+ }));
278
+ }
279
+ async sync(params) {
280
+ if (this.syncing)
281
+ return this.syncing;
282
+ this.syncing = this.runSync(params).finally(() => {
283
+ this.syncing = null;
284
+ });
285
+ return this.syncing;
286
+ }
287
+ async runSync(params) {
288
+ const meta = this.readMeta();
289
+ const needsFullReindex = params?.force ||
290
+ !meta ||
291
+ meta.model !== this.provider.model ||
292
+ meta.provider !== this.provider.id ||
293
+ meta.chunkTokens !== this.settings.chunking.tokens ||
294
+ meta.chunkOverlap !== this.settings.chunking.overlap;
295
+ if (needsFullReindex) {
296
+ this.resetIndex();
297
+ }
298
+ const files = await listMemoryFiles(this.workspaceDir);
299
+ const fileEntries = await Promise.all(files.map(async (file) => buildFileEntry(file, this.workspaceDir)));
300
+ const activePaths = new Set(fileEntries.map((entry) => entry.path));
301
+ for (const entry of fileEntries) {
302
+ const record = this.db.prepare(`SELECT hash FROM files WHERE path = ?`).get(entry.path);
303
+ if (!needsFullReindex && record?.hash === entry.hash) {
304
+ continue;
305
+ }
306
+ await this.indexFile(entry);
307
+ }
308
+ const staleRows = this.db.prepare(`SELECT path FROM files`).all();
309
+ for (const stale of staleRows) {
310
+ if (activePaths.has(stale.path))
311
+ continue;
312
+ this.db.prepare(`DELETE FROM files WHERE path = ?`).run(stale.path);
313
+ this.db.prepare(`DELETE FROM chunks WHERE path = ?`).run(stale.path);
314
+ }
315
+ this.writeMeta({
316
+ model: this.provider.model,
317
+ provider: this.provider.id,
318
+ chunkTokens: this.settings.chunking.tokens,
319
+ chunkOverlap: this.settings.chunking.overlap,
320
+ });
321
+ this.dirty = false;
322
+ }
323
+ resetIndex() {
324
+ this.db.exec(`DELETE FROM files`);
325
+ this.db.exec(`DELETE FROM chunks`);
326
+ }
327
+ readMeta() {
328
+ const row = this.db.prepare(`SELECT value FROM meta WHERE key = ?`).get(META_KEY);
329
+ if (!row?.value)
330
+ return null;
331
+ try {
332
+ return JSON.parse(row.value);
333
+ }
334
+ catch {
335
+ return null;
336
+ }
337
+ }
338
+ writeMeta(meta) {
339
+ const value = JSON.stringify(meta);
340
+ this.db
341
+ .prepare(`INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value`)
342
+ .run(META_KEY, value);
343
+ }
344
+ async indexFile(entry) {
345
+ const content = await fs.readFile(entry.absPath, "utf-8");
346
+ const chunks = chunkMarkdown(content, this.settings.chunking);
347
+ const embeddings = await this.provider.embedBatch(chunks.map((chunk) => chunk.text));
348
+ const now = Date.now();
349
+ this.db.prepare(`DELETE FROM chunks WHERE path = ?`).run(entry.path);
350
+ for (let i = 0; i < chunks.length; i += 1) {
351
+ const chunk = chunks[i];
352
+ const embedding = embeddings[i] ?? [];
353
+ if (!chunk)
354
+ continue;
355
+ const id = hashText(`${entry.path}:${chunk.startLine}:${chunk.endLine}:${chunk.hash}:${this.provider.model}`);
356
+ this.db
357
+ .prepare(`INSERT INTO chunks (id, path, start_line, end_line, hash, model, text, embedding, updated_at)
358
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
359
+ ON CONFLICT(id) DO UPDATE SET
360
+ hash=excluded.hash,
361
+ model=excluded.model,
362
+ text=excluded.text,
363
+ embedding=excluded.embedding,
364
+ updated_at=excluded.updated_at`)
365
+ .run(id, entry.path, chunk.startLine, chunk.endLine, chunk.hash, this.provider.model, chunk.text, JSON.stringify(embedding), now);
366
+ }
367
+ this.db
368
+ .prepare(`INSERT INTO files (path, hash, mtime, size) VALUES (?, ?, ?, ?)
369
+ ON CONFLICT(path) DO UPDATE SET hash=excluded.hash, mtime=excluded.mtime, size=excluded.size`)
370
+ .run(entry.path, entry.hash, entry.mtimeMs, entry.size);
371
+ }
372
+ }
373
+ export async function getMemorySearchManager(params) {
374
+ try {
375
+ const manager = await MemoryIndexManager.get(params);
376
+ return { manager };
377
+ }
378
+ catch (err) {
379
+ const message = err instanceof Error ? err.message : String(err);
380
+ return { manager: null, error: message };
381
+ }
382
+ }
@@ -0,0 +1,163 @@
1
+ import crypto from "node:crypto";
2
+ import fsSync from "node:fs";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ export function ensureDir(dir) {
6
+ try {
7
+ fsSync.mkdirSync(dir, { recursive: true });
8
+ }
9
+ catch { }
10
+ return dir;
11
+ }
12
+ export function normalizeRelPath(value) {
13
+ const trimmed = value.trim().replace(/^[./]+/, "");
14
+ return trimmed.replace(/\\/g, "/");
15
+ }
16
+ export function isMemoryPath(relPath) {
17
+ const normalized = normalizeRelPath(relPath);
18
+ if (!normalized)
19
+ return false;
20
+ if (normalized === "MEMORY.md" || normalized === "memory.md")
21
+ return true;
22
+ return normalized.startsWith("memory/");
23
+ }
24
+ async function exists(filePath) {
25
+ try {
26
+ await fs.access(filePath);
27
+ return true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ async function walkDir(dir, files) {
34
+ const entries = await fs.readdir(dir, { withFileTypes: true });
35
+ for (const entry of entries) {
36
+ const full = path.join(dir, entry.name);
37
+ if (entry.isDirectory()) {
38
+ await walkDir(full, files);
39
+ continue;
40
+ }
41
+ if (!entry.isFile())
42
+ continue;
43
+ if (!entry.name.endsWith(".md"))
44
+ continue;
45
+ files.push(full);
46
+ }
47
+ }
48
+ export async function listMemoryFiles(workspaceDir) {
49
+ const result = [];
50
+ const memoryFile = path.join(workspaceDir, "MEMORY.md");
51
+ const altMemoryFile = path.join(workspaceDir, "memory.md");
52
+ if (await exists(memoryFile))
53
+ result.push(memoryFile);
54
+ if (await exists(altMemoryFile))
55
+ result.push(altMemoryFile);
56
+ const memoryDir = path.join(workspaceDir, "memory");
57
+ if (await exists(memoryDir)) {
58
+ await walkDir(memoryDir, result);
59
+ }
60
+ return result;
61
+ }
62
+ export function hashText(value) {
63
+ return crypto.createHash("sha256").update(value).digest("hex");
64
+ }
65
+ export async function buildFileEntry(absPath, workspaceDir) {
66
+ const stat = await fs.stat(absPath);
67
+ const content = await fs.readFile(absPath, "utf-8");
68
+ const hash = hashText(content);
69
+ return {
70
+ path: path.relative(workspaceDir, absPath).replace(/\\/g, "/"),
71
+ absPath,
72
+ mtimeMs: stat.mtimeMs,
73
+ size: stat.size,
74
+ hash,
75
+ };
76
+ }
77
+ export function chunkMarkdown(content, chunking) {
78
+ const lines = content.split("\n");
79
+ if (lines.length === 0)
80
+ return [];
81
+ const maxChars = Math.max(32, chunking.tokens * 4);
82
+ const overlapChars = Math.max(0, chunking.overlap * 4);
83
+ const chunks = [];
84
+ let current = [];
85
+ let currentChars = 0;
86
+ const flush = () => {
87
+ if (current.length === 0)
88
+ return;
89
+ const firstEntry = current[0];
90
+ const lastEntry = current[current.length - 1];
91
+ if (!firstEntry || !lastEntry)
92
+ return;
93
+ const text = current.map((entry) => entry.line).join("\n");
94
+ const startLine = firstEntry.lineNo;
95
+ const endLine = lastEntry.lineNo;
96
+ chunks.push({
97
+ startLine,
98
+ endLine,
99
+ text,
100
+ hash: hashText(text),
101
+ });
102
+ };
103
+ const carryOverlap = () => {
104
+ if (overlapChars <= 0 || current.length === 0) {
105
+ current = [];
106
+ currentChars = 0;
107
+ return;
108
+ }
109
+ let acc = 0;
110
+ const kept = [];
111
+ for (let i = current.length - 1; i >= 0; i -= 1) {
112
+ const entry = current[i];
113
+ if (!entry)
114
+ continue;
115
+ acc += entry.line.length + 1;
116
+ kept.unshift(entry);
117
+ if (acc >= overlapChars)
118
+ break;
119
+ }
120
+ current = kept;
121
+ currentChars = kept.reduce((sum, entry) => sum + entry.line.length + 1, 0);
122
+ };
123
+ for (let i = 0; i < lines.length; i += 1) {
124
+ const line = lines[i] ?? "";
125
+ const lineNo = i + 1;
126
+ const lineSize = line.length + 1;
127
+ if (currentChars + lineSize > maxChars && current.length > 0) {
128
+ flush();
129
+ carryOverlap();
130
+ }
131
+ current.push({ line, lineNo });
132
+ currentChars += lineSize;
133
+ }
134
+ flush();
135
+ return chunks;
136
+ }
137
+ export function parseEmbedding(raw) {
138
+ try {
139
+ const parsed = JSON.parse(raw);
140
+ return Array.isArray(parsed) ? parsed : [];
141
+ }
142
+ catch {
143
+ return [];
144
+ }
145
+ }
146
+ export function cosineSimilarity(a, b) {
147
+ if (a.length === 0 || b.length === 0)
148
+ return 0;
149
+ const len = Math.min(a.length, b.length);
150
+ let dot = 0;
151
+ let normA = 0;
152
+ let normB = 0;
153
+ for (let i = 0; i < len; i += 1) {
154
+ const av = a[i] ?? 0;
155
+ const bv = b[i] ?? 0;
156
+ dot += av * bv;
157
+ normA += av * av;
158
+ normB += bv * bv;
159
+ }
160
+ if (normA === 0 || normB === 0)
161
+ return 0;
162
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
163
+ }