@algosuite/vo-mcp 0.2.0-beta.20 → 0.2.0-beta.21

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/dist/index.js CHANGED
@@ -293,6 +293,123 @@ var init_credential_store = __esm({
293
293
  }
294
294
  });
295
295
 
296
+ // src/tools/memory/safe-memory-file.ts
297
+ import { resolve, sep } from "node:path";
298
+ function isSafeMemoryFileName(fileName) {
299
+ return fileName.length <= 200 && fileName.trim() === fileName && !fileName.includes("/") && !fileName.includes("\\") && !fileName.includes(":") && SAFE_MEMORY_FILE_RE.test(fileName);
300
+ }
301
+ function resolveMemoryFilePath(memoryDir, fileName) {
302
+ if (!isSafeMemoryFileName(fileName)) {
303
+ throw new Error(`unsafe memory file_name: ${fileName.slice(0, 80)}`);
304
+ }
305
+ const root = resolve(memoryDir);
306
+ const filePath = resolve(root, fileName);
307
+ const rootPrefix = root.endsWith(sep) ? root : `${root}${sep}`;
308
+ if (filePath !== root && !filePath.startsWith(rootPrefix)) {
309
+ throw new Error(`memory file path escapes memory directory: ${fileName.slice(0, 80)}`);
310
+ }
311
+ return filePath;
312
+ }
313
+ var SAFE_MEMORY_FILE_RE;
314
+ var init_safe_memory_file = __esm({
315
+ "src/tools/memory/safe-memory-file.ts"() {
316
+ "use strict";
317
+ SAFE_MEMORY_FILE_RE = /^[A-Za-z0-9][A-Za-z0-9._ -]*\.md$/i;
318
+ }
319
+ });
320
+
321
+ // src/tools/memory/memory-knowledge-bridge.ts
322
+ var memory_knowledge_bridge_exports = {};
323
+ __export(memory_knowledge_bridge_exports, {
324
+ extractMemoryTitle: () => extractMemoryTitle,
325
+ upsertMemoryFilesAsKnowledge: () => upsertMemoryFilesAsKnowledge
326
+ });
327
+ import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync7 } from "node:fs";
328
+ function extractMemoryTitle(fileName, content) {
329
+ const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
330
+ if (frontmatter) {
331
+ const description23 = frontmatter[1].match(/^description:\s*(.+)$/m);
332
+ if (description23 && description23[1].trim()) return description23[1].trim().slice(0, 200);
333
+ }
334
+ const heading = content.match(/^#\s+(.+)$/m);
335
+ if (heading && heading[1].trim()) return heading[1].trim().slice(0, 200);
336
+ return fileName;
337
+ }
338
+ async function upsertMemoryFilesAsKnowledge(options) {
339
+ const { controlPlaneUrl, token, memoryDir, fetchFn } = options;
340
+ let files;
341
+ try {
342
+ if (!existsSync5(memoryDir)) {
343
+ return { attempted: 0, upserted: 0, failed: 0, failures: [] };
344
+ }
345
+ files = readdirSync4(memoryDir).filter(
346
+ (f) => f.endsWith(".md") && f.toUpperCase() !== "MEMORY.MD"
347
+ );
348
+ } catch (err) {
349
+ return {
350
+ attempted: 0,
351
+ upserted: 0,
352
+ failed: 1,
353
+ failures: [`memory dir scan: ${err instanceof Error ? err.message : String(err)}`]
354
+ };
355
+ }
356
+ let upserted = 0;
357
+ const failures = [];
358
+ for (const fileName of files) {
359
+ try {
360
+ const content = readFileSync7(resolveMemoryFilePath(memoryDir, fileName), "utf8");
361
+ if (content.length > CONTENT_HARD_LIMIT) {
362
+ failures.push(`${fileName}: ${content.length} chars exceeds the ${CONTENT_HARD_LIMIT} server limit \u2014 split the memory file`);
363
+ continue;
364
+ }
365
+ const title = extractMemoryTitle(fileName, content);
366
+ const base = {
367
+ knowledge_class: "memory",
368
+ source_path: `memory/${fileName}`,
369
+ title,
370
+ content
371
+ };
372
+ const post = (body) => fetchFn(`${controlPlaneUrl}/api/v1/knowledge/private`, {
373
+ method: "POST",
374
+ headers: {
375
+ authorization: `Bearer ${token}`,
376
+ "content-type": "application/json"
377
+ },
378
+ body: JSON.stringify(body)
379
+ });
380
+ let response = await post({
381
+ ...base,
382
+ provenance: { written_by: "memory-bridge", source_kind: "operator_memory" }
383
+ });
384
+ if (response.status === 400) {
385
+ response = await post(base);
386
+ }
387
+ if (response.status >= 200 && response.status < 300) {
388
+ upserted += 1;
389
+ } else {
390
+ const text = await response.text();
391
+ failures.push(`${fileName}: HTTP ${response.status} ${text.slice(0, 80)}`);
392
+ }
393
+ } catch (err) {
394
+ failures.push(`${fileName}: ${err instanceof Error ? err.message : String(err)}`);
395
+ }
396
+ }
397
+ return {
398
+ attempted: files.length,
399
+ upserted,
400
+ failed: failures.length,
401
+ failures: failures.slice(0, 5)
402
+ };
403
+ }
404
+ var CONTENT_HARD_LIMIT;
405
+ var init_memory_knowledge_bridge = __esm({
406
+ "src/tools/memory/memory-knowledge-bridge.ts"() {
407
+ "use strict";
408
+ init_safe_memory_file();
409
+ CONTENT_HARD_LIMIT = 5e5;
410
+ }
411
+ });
412
+
296
413
  // src/server.ts
297
414
  import { randomUUID as randomUUID2 } from "node:crypto";
298
415
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -4943,28 +5060,8 @@ async function handleConciergeDispatch(deps, rawInput, _signal) {
4943
5060
  // src/tools/memory/sync-config.ts
4944
5061
  import { homedir as homedir5 } from "node:os";
4945
5062
  import { join as join7 } from "node:path";
4946
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, readdirSync as readdirSync4 } from "node:fs";
4947
-
4948
- // src/tools/memory/safe-memory-file.ts
4949
- import { resolve, sep } from "node:path";
4950
- var SAFE_MEMORY_FILE_RE = /^[A-Za-z0-9][A-Za-z0-9._ -]*\.md$/i;
4951
- function isSafeMemoryFileName(fileName) {
4952
- return fileName.length <= 200 && fileName.trim() === fileName && !fileName.includes("/") && !fileName.includes("\\") && !fileName.includes(":") && SAFE_MEMORY_FILE_RE.test(fileName);
4953
- }
4954
- function resolveMemoryFilePath(memoryDir, fileName) {
4955
- if (!isSafeMemoryFileName(fileName)) {
4956
- throw new Error(`unsafe memory file_name: ${fileName.slice(0, 80)}`);
4957
- }
4958
- const root = resolve(memoryDir);
4959
- const filePath = resolve(root, fileName);
4960
- const rootPrefix = root.endsWith(sep) ? root : `${root}${sep}`;
4961
- if (filePath !== root && !filePath.startsWith(rootPrefix)) {
4962
- throw new Error(`memory file path escapes memory directory: ${fileName.slice(0, 80)}`);
4963
- }
4964
- return filePath;
4965
- }
4966
-
4967
- // src/tools/memory/sync-config.ts
5063
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync3, readdirSync as readdirSync5 } from "node:fs";
5064
+ init_safe_memory_file();
4968
5065
  var TOOL_NAME22 = "vo_sync_config";
4969
5066
  var inputSchema22 = {
4970
5067
  type: "object",
@@ -4991,7 +5088,7 @@ function isToolInput22(v) {
4991
5088
  return true;
4992
5089
  }
4993
5090
  function deriveProjectSlug(cwd) {
4994
- return cwd.replace(/\\/g, "/").replace(/\/+$/g, "").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
5091
+ return cwd.replace(/([^:\\/])[\\/]+$/, "$1").replace(/\\/g, "/").replace(/^([a-zA-Z]):/, (_m, drive) => `${drive.toUpperCase()}:`).replace(/[^a-zA-Z0-9]/g, "-");
4995
5092
  }
4996
5093
  function getMemoryDir(cwd) {
4997
5094
  const slug = deriveProjectSlug(cwd);
@@ -5026,12 +5123,12 @@ async function pullMemory(controlPlaneUrl, token, memoryDir, fetchFn) {
5026
5123
  return { pulled: data.entries.length, files };
5027
5124
  }
5028
5125
  async function pushMemory(controlPlaneUrl, token, memoryDir, sessionId, fetchFn) {
5029
- if (!existsSync5(memoryDir)) {
5126
+ if (!existsSync6(memoryDir)) {
5030
5127
  return { pushed: 0, created: 0, updated: 0 };
5031
5128
  }
5032
- const localFiles = readdirSync4(memoryDir).filter((f) => f.endsWith(".md")).map((f) => ({
5129
+ const localFiles = readdirSync5(memoryDir).filter((f) => f.endsWith(".md")).map((f) => ({
5033
5130
  file_name: f,
5034
- content: readFileSync7(resolveMemoryFilePath(memoryDir, f), "utf8"),
5131
+ content: readFileSync8(resolveMemoryFilePath(memoryDir, f), "utf8"),
5035
5132
  entry_type: f === "MEMORY.md" ? "index" : "topic"
5036
5133
  }));
5037
5134
  if (localFiles.length === 0) {
@@ -5136,13 +5233,32 @@ async function runMemorySync(action, cwd, sessionId, fetchFn = globalThis.fetch)
5136
5233
  return { synced: true, action: "pull", pulled: result2.pulled, files: result2.files, memory_dir: memoryDir };
5137
5234
  }
5138
5235
  const result = await pushMemory(baseUrl, token, memoryDir, sessionId, fetchFn);
5236
+ let bridge = { upserted: 0, failed: 0, failures: [] };
5237
+ try {
5238
+ const { upsertMemoryFilesAsKnowledge: upsertMemoryFilesAsKnowledge2 } = await Promise.resolve().then(() => (init_memory_knowledge_bridge(), memory_knowledge_bridge_exports));
5239
+ bridge = await upsertMemoryFilesAsKnowledge2({
5240
+ controlPlaneUrl: baseUrl,
5241
+ token,
5242
+ memoryDir,
5243
+ fetchFn
5244
+ });
5245
+ } catch (err) {
5246
+ bridge = {
5247
+ upserted: 0,
5248
+ failed: 1,
5249
+ failures: [`bridge unavailable: ${err instanceof Error ? err.message : String(err)}`]
5250
+ };
5251
+ }
5139
5252
  return {
5140
5253
  synced: true,
5141
5254
  action: "push",
5142
5255
  pushed: result.pushed,
5143
5256
  created: result.created,
5144
5257
  updated: result.updated,
5145
- memory_dir: memoryDir
5258
+ memory_dir: memoryDir,
5259
+ knowledge_upserted: bridge.upserted,
5260
+ knowledge_failed: bridge.failed,
5261
+ ...bridge.failed > 0 ? { knowledge_failures: bridge.failures } : {}
5146
5262
  };
5147
5263
  } catch (err) {
5148
5264
  const message = err instanceof Error ? err.message : String(err);
@@ -5164,6 +5280,7 @@ async function handleSyncConfig(deps, rawInput, _signal, fetchFn = globalThis.fe
5164
5280
  // src/tools/memory/private-knowledge.ts
5165
5281
  var UPSERT_TOOL_NAME = "vo_private_knowledge_upsert";
5166
5282
  var CONTEXT_TOOL_NAME = "vo_private_knowledge_context";
5283
+ var INVALIDATE_TOOL_NAME = "vo_private_knowledge_invalidate";
5167
5284
  var KNOWLEDGE_CLASSES = ["memory", "skill", "doctrine", "hook", "command"];
5168
5285
  var PRECISION_CHAR_BUDGET = 12e3;
5169
5286
  var upsertInputSchema = {
@@ -5187,8 +5304,18 @@ var contextInputSchema = {
5187
5304
  required: ["query"],
5188
5305
  additionalProperties: false
5189
5306
  };
5307
+ var invalidateInputSchema = {
5308
+ type: "object",
5309
+ properties: {
5310
+ knowledge_class: { type: "string", enum: KNOWLEDGE_CLASSES },
5311
+ source_path: { type: "string", minLength: 1, maxLength: 400, description: "Stable private source identifier of the entry to invalidate \u2014 must match the source_path used at upsert." }
5312
+ },
5313
+ required: ["knowledge_class", "source_path"],
5314
+ additionalProperties: false
5315
+ };
5190
5316
  var upsertDescription = "Uploads or refreshes the authenticated operator\u2019s private cloud knowledge. Works for Claude, Codex, Cursor, and cowork clients via the same vo-mcp login credential. Returns metadata only, not raw stored content. PRECISION DISCIPLINE: keep each entry tight and focused (~1-3 pages) with a descriptive retrieval-friendly title \u2014 retrieval surfaces whole entries, so small dense entries beat bulk dumps. Split large corpora into focused entries, then run a retrieval self-test via vo_private_knowledge_context before relying on the knowledge.";
5191
5317
  var contextDescription = "Retrieves prompt-ready private knowledge context for the authenticated operator. Returns snippets/context only; no raw corpus download. Also the retrieval self-test surface: after upserting critical knowledge, query for it here and confirm the entry surfaces before trusting it in downstream work.";
5318
+ var invalidateDescription = `Soft-deletes one private-knowledge entry for the authenticated operator: closes the live entry\u2019s validity window (bi-temporal) so it stops surfacing in retrieval. Never destroys data \u2014 invalidated versions remain queryable server-side via include_invalidated. Identify the entry by the same { knowledge_class, source_path } used at upsert; a not_found response means no live entry matches. After invalidating, self-test via ${CONTEXT_TOOL_NAME} to confirm the entry no longer surfaces.`;
5192
5319
  function isKnowledgeClass(value) {
5193
5320
  return typeof value === "string" && KNOWLEDGE_CLASSES.includes(value);
5194
5321
  }
@@ -5197,6 +5324,11 @@ function isUpsertInput(value) {
5197
5324
  const input = value;
5198
5325
  return isKnowledgeClass(input["knowledge_class"]) && typeof input["source_path"] === "string" && typeof input["title"] === "string" && typeof input["content"] === "string";
5199
5326
  }
5327
+ function isInvalidateInput(value) {
5328
+ if (typeof value !== "object" || value === null) return false;
5329
+ const input = value;
5330
+ return isKnowledgeClass(input["knowledge_class"]) && typeof input["source_path"] === "string";
5331
+ }
5200
5332
  function isContextInput(value) {
5201
5333
  if (typeof value !== "object" || value === null) return false;
5202
5334
  const input = value;
@@ -5230,7 +5362,12 @@ async function callPrivateKnowledge(path3, body, fetchFn) {
5230
5362
  body: JSON.stringify(body)
5231
5363
  });
5232
5364
  const text = await response.text();
5233
- const parsed = text ? JSON.parse(text) : null;
5365
+ let parsed;
5366
+ try {
5367
+ parsed = text ? JSON.parse(text) : null;
5368
+ } catch {
5369
+ parsed = null;
5370
+ }
5234
5371
  if (response.status < 200 || response.status >= 300) {
5235
5372
  return { ok: false, status: response.status, response: parsed ?? text };
5236
5373
  }
@@ -5251,6 +5388,13 @@ async function handlePrivateKnowledgeUpsert(_deps, rawInput, _signal, fetchFn =
5251
5388
  }
5252
5389
  return jsonContent(envelope);
5253
5390
  }
5391
+ async function handlePrivateKnowledgeInvalidate(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
5392
+ if (!isInvalidateInput(rawInput)) {
5393
+ throw invalidParams(INVALIDATE_TOOL_NAME, "expected { knowledge_class, source_path }.");
5394
+ }
5395
+ const payload = await callPrivateKnowledge("/api/v1/knowledge/private/invalidate", rawInput, fetchFn);
5396
+ return jsonContent({ tool: INVALIDATE_TOOL_NAME, schema_version: 1, payload });
5397
+ }
5254
5398
  async function handlePrivateKnowledgeContext(_deps, rawInput, _signal, fetchFn = globalThis.fetch) {
5255
5399
  if (!isContextInput(rawInput)) {
5256
5400
  throw invalidParams(CONTEXT_TOOL_NAME, "expected { query, optional limit, optional knowledge_class }.");
@@ -5402,11 +5546,11 @@ async function handleHqWhiteboardRead(_deps, rawInput, signal) {
5402
5546
  }
5403
5547
 
5404
5548
  // src/tools/skills/skill-corpus.ts
5405
- import { existsSync as existsSync6, statSync as statSync5 } from "node:fs";
5549
+ import { existsSync as existsSync7, statSync as statSync5 } from "node:fs";
5406
5550
  import { dirname as dirname5, isAbsolute, join as join9, resolve as resolve2 } from "node:path";
5407
5551
 
5408
5552
  // ../skill-registry/src/loader.ts
5409
- import { readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync4 } from "node:fs";
5553
+ import { readdirSync as readdirSync6, readFileSync as readFileSync9, statSync as statSync4 } from "node:fs";
5410
5554
  import { join as join8 } from "node:path";
5411
5555
  var InvalidSkillFrontmatterError = class extends Error {
5412
5556
  constructor(skillFile, reason) {
@@ -5457,7 +5601,7 @@ ${FRONTMATTER_DELIMITER}
5457
5601
  return { name, description: description23, body };
5458
5602
  }
5459
5603
  function loadSkillsFromDir(skillsDir) {
5460
- const entries = readdirSync5(skillsDir);
5604
+ const entries = readdirSync6(skillsDir);
5461
5605
  const skills = [];
5462
5606
  for (const entry of entries) {
5463
5607
  const entryPath = join8(skillsDir, entry);
@@ -5471,7 +5615,7 @@ function loadSkillsFromDir(skillsDir) {
5471
5615
  const skillFile = join8(entryPath, "SKILL.md");
5472
5616
  let raw;
5473
5617
  try {
5474
- raw = readFileSync8(skillFile, "utf8");
5618
+ raw = readFileSync9(skillFile, "utf8");
5475
5619
  } catch {
5476
5620
  continue;
5477
5621
  }
@@ -5512,12 +5656,12 @@ function resolveSkillsDir(env = process.env, startDir = process.cwd()) {
5512
5656
  const override = env.VO_SKILLS_DIR;
5513
5657
  if (typeof override === "string" && override.length > 0) {
5514
5658
  const abs = isAbsolute(override) ? override : resolve2(startDir, override);
5515
- return existsSync6(abs) && statSync5(abs).isDirectory() ? abs : null;
5659
+ return existsSync7(abs) && statSync5(abs).isDirectory() ? abs : null;
5516
5660
  }
5517
5661
  let dir = resolve2(startDir);
5518
5662
  for (let i = 0; i < MAX_WALK_UP_LEVELS; i += 1) {
5519
5663
  const candidate = join9(dir, ".claude", "skills");
5520
- if (existsSync6(candidate) && statSync5(candidate).isDirectory()) return candidate;
5664
+ if (existsSync7(candidate) && statSync5(candidate).isDirectory()) return candidate;
5521
5665
  const parent = dirname5(dir);
5522
5666
  if (parent === dir) break;
5523
5667
  dir = parent;
@@ -5785,6 +5929,14 @@ function buildToolRegistry() {
5785
5929
  },
5786
5930
  handler: handlePrivateKnowledgeContext
5787
5931
  },
5932
+ [INVALIDATE_TOOL_NAME]: {
5933
+ definition: {
5934
+ name: INVALIDATE_TOOL_NAME,
5935
+ description: invalidateDescription,
5936
+ inputSchema: invalidateInputSchema
5937
+ },
5938
+ handler: handlePrivateKnowledgeInvalidate
5939
+ },
5788
5940
  [POST_TOOL_NAME]: {
5789
5941
  definition: {
5790
5942
  name: POST_TOOL_NAME,