oh-memos-mcp 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +44 -27
- package/CHANGELOG.md +145 -0
- package/README.md +260 -226
- package/dist/api-client.d.ts +0 -0
- package/dist/api-client.js +0 -0
- package/dist/canvas-format.d.ts +101 -0
- package/dist/canvas-format.js +259 -0
- package/dist/canvas-store.d.ts +62 -0
- package/dist/canvas-store.js +187 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +44 -9
- package/dist/cube-manager.d.ts +0 -0
- package/dist/cube-manager.js +11 -0
- package/dist/formatters.d.ts +0 -0
- package/dist/formatters.js +0 -0
- package/dist/graph-provenance.d.ts +34 -0
- package/dist/graph-provenance.js +175 -0
- package/dist/graphify-import.d.ts +72 -0
- package/dist/graphify-import.js +194 -0
- package/dist/handlers/admin.d.ts +1 -0
- package/dist/handlers/admin.js +18 -0
- package/dist/handlers/arg-contract.d.ts +0 -0
- package/dist/handlers/arg-contract.js +0 -0
- package/dist/handlers/calendar.d.ts +0 -0
- package/dist/handlers/calendar.js +0 -0
- package/dist/handlers/canvas.d.ts +40 -0
- package/dist/handlers/canvas.js +334 -0
- package/dist/handlers/graph.d.ts +8 -0
- package/dist/handlers/graph.js +66 -14
- package/dist/handlers/index.d.ts +0 -0
- package/dist/handlers/index.js +28 -5
- package/dist/handlers/memory.d.ts +0 -0
- package/dist/handlers/memory.js +33 -2
- package/dist/handlers/search.d.ts +0 -0
- package/dist/handlers/search.js +56 -15
- package/dist/handlers/skill.d.ts +6 -0
- package/dist/handlers/skill.js +123 -0
- package/dist/handlers/think.d.ts +0 -0
- package/dist/handlers/think.js +0 -0
- package/dist/handlers/utils.d.ts +0 -0
- package/dist/handlers/utils.js +0 -0
- package/dist/handlers/wiki-export.d.ts +0 -0
- package/dist/handlers/wiki-export.js +7 -9
- package/dist/handlers/wiki-import-relations.d.ts +11 -0
- package/dist/handlers/wiki-import-relations.js +32 -0
- package/dist/handlers/wiki-import.d.ts +19 -0
- package/dist/handlers/wiki-import.js +382 -0
- package/dist/index.d.ts +0 -0
- package/dist/index.js +0 -0
- package/dist/keyword-enhancer.d.ts +0 -0
- package/dist/keyword-enhancer.js +0 -0
- package/dist/memory-analysis.d.ts +0 -0
- package/dist/memory-analysis.js +0 -0
- package/dist/memory-quality.d.ts +10 -0
- package/dist/memory-quality.js +57 -0
- package/dist/memory-write-response.d.ts +8 -0
- package/dist/memory-write-response.js +15 -0
- package/dist/models.d.ts +0 -0
- package/dist/models.js +0 -0
- package/dist/policy.d.ts +8 -0
- package/dist/policy.js +9 -0
- package/dist/providers/lite-embedding.d.ts +31 -0
- package/dist/providers/lite-embedding.js +60 -0
- package/dist/providers/local-jsonl-provider.d.ts +28 -0
- package/dist/providers/local-jsonl-provider.js +225 -0
- package/dist/providers/memory-provider.d.ts +21 -0
- package/dist/providers/memory-provider.js +2 -0
- package/dist/providers/provider-factory.d.ts +11 -0
- package/dist/providers/provider-factory.js +27 -0
- package/dist/query-processing.d.ts +0 -0
- package/dist/query-processing.js +0 -0
- package/dist/server.d.ts +0 -0
- package/dist/server.js +13 -3
- package/dist/skill-candidate.d.ts +11 -0
- package/dist/skill-candidate.js +56 -0
- package/dist/skill-lifecycle.d.ts +15 -0
- package/dist/skill-lifecycle.js +51 -0
- package/dist/tools-registry.d.ts +130 -7
- package/dist/tools-registry.js +93 -31
- package/dist/types.d.ts +0 -0
- package/dist/types.js +0 -0
- package/dist/wiki-import-format.d.ts +47 -0
- package/dist/wiki-import-format.js +113 -0
- package/dist/wiki-import-safety.d.ts +25 -0
- package/dist/wiki-import-safety.js +31 -0
- package/dist/wiki-relations.d.ts +44 -0
- package/dist/wiki-relations.js +88 -0
- package/package.json +5 -2
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provenance helpers shared by memory-graph responses and the Graphify
|
|
3
|
+
* node-link importer.
|
|
4
|
+
*
|
|
5
|
+
* Graphify calls the categorical edge field `confidence` while oh-memos
|
|
6
|
+
* memory records historically use `confidence`, `source` and `sources` for
|
|
7
|
+
* several different purposes. This module deliberately normalizes at the
|
|
8
|
+
* boundary instead of changing old Neo4j properties in place.
|
|
9
|
+
*/
|
|
10
|
+
export const EVIDENCE_KINDS = ["EXTRACTED", "INFERRED", "AMBIGUOUS", "UNKNOWN"];
|
|
11
|
+
function asRecord(value) {
|
|
12
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
13
|
+
? value
|
|
14
|
+
: {};
|
|
15
|
+
}
|
|
16
|
+
function firstString(record, keys) {
|
|
17
|
+
for (const key of keys) {
|
|
18
|
+
const value = record[key];
|
|
19
|
+
if (typeof value === "string" && value.trim())
|
|
20
|
+
return value.trim();
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
function normalizeKind(value) {
|
|
25
|
+
if (typeof value !== "string")
|
|
26
|
+
return "UNKNOWN";
|
|
27
|
+
const normalized = value.trim().toUpperCase();
|
|
28
|
+
return EVIDENCE_KINDS.includes(normalized)
|
|
29
|
+
? normalized
|
|
30
|
+
: "UNKNOWN";
|
|
31
|
+
}
|
|
32
|
+
function normalizeScore(...values) {
|
|
33
|
+
for (const value of values) {
|
|
34
|
+
if (typeof value === "string" && EVIDENCE_KINDS.includes(value.trim().toUpperCase())) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const score = Number(value);
|
|
38
|
+
if (!Number.isFinite(score))
|
|
39
|
+
continue;
|
|
40
|
+
return Math.max(0, Math.min(1, score));
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
function addRef(refs, value) {
|
|
45
|
+
if (typeof value !== "string")
|
|
46
|
+
return;
|
|
47
|
+
const ref = value.trim();
|
|
48
|
+
if (ref && !refs.includes(ref))
|
|
49
|
+
refs.push(ref);
|
|
50
|
+
}
|
|
51
|
+
function collectRefs(record, sourceFile, sourceLocation) {
|
|
52
|
+
const refs = [];
|
|
53
|
+
const rawRefs = record.evidence_refs ?? record.evidenceRefs ?? record.sources;
|
|
54
|
+
if (Array.isArray(rawRefs)) {
|
|
55
|
+
for (const item of rawRefs) {
|
|
56
|
+
if (typeof item === "string") {
|
|
57
|
+
addRef(refs, item);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const nested = asRecord(item);
|
|
61
|
+
const nestedFile = firstString(nested, ["source_file", "sourceFile", "file", "path"]);
|
|
62
|
+
const nestedLocation = firstString(nested, ["source_location", "sourceLocation", "location", "line"]);
|
|
63
|
+
if (nestedFile && nestedLocation)
|
|
64
|
+
addRef(refs, `${nestedFile}:${nestedLocation}`);
|
|
65
|
+
else
|
|
66
|
+
addRef(refs, nestedFile ?? firstString(nested, ["ref", "uri", "source_ref"]));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
addRef(refs, record.source_ref);
|
|
71
|
+
addRef(refs, record.sourceRef);
|
|
72
|
+
// An explicit evidence_refs/source_ref is authoritative. Only synthesize
|
|
73
|
+
// a file anchor when legacy data has no explicit reference at all; this
|
|
74
|
+
// keeps output compact while retaining useful provenance for old records.
|
|
75
|
+
if (refs.length === 0) {
|
|
76
|
+
if (sourceFile && sourceLocation)
|
|
77
|
+
addRef(refs, `${sourceFile}:${sourceLocation}`);
|
|
78
|
+
else if (sourceFile)
|
|
79
|
+
addRef(refs, sourceFile);
|
|
80
|
+
}
|
|
81
|
+
return refs;
|
|
82
|
+
}
|
|
83
|
+
/** Normalize old memory/Neo4j/Graphify fields without making provenance claims. */
|
|
84
|
+
export function normalizeProvenance(value) {
|
|
85
|
+
const record = asRecord(value);
|
|
86
|
+
const nested = asRecord(record.provenance);
|
|
87
|
+
const merged = { ...nested, ...record };
|
|
88
|
+
const sourceFile = firstString(merged, ["source_file", "sourceFile"]);
|
|
89
|
+
const sourceLocation = firstString(merged, ["source_location", "sourceLocation"]);
|
|
90
|
+
const sourceRef = firstString(merged, ["source_ref", "sourceRef"]);
|
|
91
|
+
const kind = normalizeKind(merged.evidence_kind
|
|
92
|
+
?? merged.evidenceKind
|
|
93
|
+
?? (typeof merged.confidence === "string" ? merged.confidence : undefined)
|
|
94
|
+
?? merged.kind
|
|
95
|
+
?? merged.provenance_kind);
|
|
96
|
+
const score = normalizeScore(merged.confidence_score, merged.confidence, merged.weight);
|
|
97
|
+
const result = {
|
|
98
|
+
evidence_kind: kind,
|
|
99
|
+
evidence_refs: collectRefs(merged, sourceFile, sourceLocation),
|
|
100
|
+
};
|
|
101
|
+
if (score !== undefined)
|
|
102
|
+
result.confidence_score = score;
|
|
103
|
+
if (sourceFile)
|
|
104
|
+
result.source_file = sourceFile;
|
|
105
|
+
if (sourceLocation)
|
|
106
|
+
result.source_location = sourceLocation;
|
|
107
|
+
if (sourceRef)
|
|
108
|
+
result.source_ref = sourceRef;
|
|
109
|
+
const extractorVersion = firstString(merged, ["extractor_version", "extractorVersion"]);
|
|
110
|
+
const lastVerifiedAt = firstString(merged, ["last_verified_at", "lastVerifiedAt"]);
|
|
111
|
+
if (extractorVersion)
|
|
112
|
+
result.extractor_version = extractorVersion;
|
|
113
|
+
if (lastVerifiedAt)
|
|
114
|
+
result.last_verified_at = lastVerifiedAt;
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
/** Render a compact, deterministic explanation suitable for MCP Markdown. */
|
|
118
|
+
export function formatProvenance(value) {
|
|
119
|
+
const provenance = normalizeProvenance(value);
|
|
120
|
+
const parts = [`evidence=${provenance.evidence_kind}`];
|
|
121
|
+
if (provenance.confidence_score !== undefined) {
|
|
122
|
+
parts.push(`confidence=${provenance.confidence_score.toFixed(2)}`);
|
|
123
|
+
}
|
|
124
|
+
const source = provenance.source_file
|
|
125
|
+
? provenance.source_location
|
|
126
|
+
? `${provenance.source_file}:${provenance.source_location}`
|
|
127
|
+
: provenance.source_file
|
|
128
|
+
: provenance.source_ref;
|
|
129
|
+
if (source)
|
|
130
|
+
parts.push(`source=${source}`);
|
|
131
|
+
if (provenance.evidence_refs.length > 0)
|
|
132
|
+
parts.push(`refs=${provenance.evidence_refs.join(",")}`);
|
|
133
|
+
if (provenance.extractor_version)
|
|
134
|
+
parts.push(`extractor=${provenance.extractor_version}`);
|
|
135
|
+
if (provenance.last_verified_at)
|
|
136
|
+
parts.push(`verified=${provenance.last_verified_at}`);
|
|
137
|
+
return parts.join("; ");
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Normalize a project-relative path for a stable code-symbol identity.
|
|
141
|
+
* Absolute paths and traversal segments are rejected before they can enter a
|
|
142
|
+
* graph ID or an import plan.
|
|
143
|
+
*/
|
|
144
|
+
export function normalizeProjectRelativePath(value) {
|
|
145
|
+
const raw = value.trim().replaceAll("\\", "/");
|
|
146
|
+
if (!raw || raw.startsWith("/") || /^[A-Za-z]:\//.test(raw) || raw.startsWith("//")) {
|
|
147
|
+
throw new Error("source path must be project-relative, not absolute");
|
|
148
|
+
}
|
|
149
|
+
const parts = [];
|
|
150
|
+
for (const part of raw.split("/")) {
|
|
151
|
+
if (!part || part === ".")
|
|
152
|
+
continue;
|
|
153
|
+
if (part === "..")
|
|
154
|
+
throw new Error("source path must be project-relative and must not contain traversal segments");
|
|
155
|
+
parts.push(part);
|
|
156
|
+
}
|
|
157
|
+
if (parts.length === 0)
|
|
158
|
+
throw new Error("source path must be a non-empty project-relative path");
|
|
159
|
+
return parts.join("/");
|
|
160
|
+
}
|
|
161
|
+
function slug(value) {
|
|
162
|
+
const normalized = value.normalize("NFKC").toLocaleLowerCase("en-US");
|
|
163
|
+
return normalized
|
|
164
|
+
.replace(/[^\p{L}\p{N}]+/gu, "_")
|
|
165
|
+
.replace(/^_+|_+$/g, "") || "unknown";
|
|
166
|
+
}
|
|
167
|
+
/** Stable, portable ID for a code node imported from an external graph. */
|
|
168
|
+
export function stableCodeNodeId(projectRelativePath, language, symbol, signature) {
|
|
169
|
+
const path = normalizeProjectRelativePath(projectRelativePath);
|
|
170
|
+
const parts = [path, language, symbol];
|
|
171
|
+
if (signature?.trim())
|
|
172
|
+
parts.push(signature);
|
|
173
|
+
return `code:${parts.map(slug).join(":")}`;
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=graph-provenance.js.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure Graphify node-link importer boundary.
|
|
3
|
+
*
|
|
4
|
+
* This module intentionally produces a dry-run plan only. A later adapter
|
|
5
|
+
* may apply the plan to Neo4j, but parsing and safety checks must be testable
|
|
6
|
+
* without a database or a filesystem write.
|
|
7
|
+
*/
|
|
8
|
+
import { type NormalizedProvenance } from "./graph-provenance.js";
|
|
9
|
+
export interface GraphifyNode {
|
|
10
|
+
id: string;
|
|
11
|
+
label?: string;
|
|
12
|
+
file_type?: string;
|
|
13
|
+
language?: string;
|
|
14
|
+
source_file?: string;
|
|
15
|
+
source_location?: string;
|
|
16
|
+
community?: number | string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface GraphifyLink {
|
|
20
|
+
source?: string;
|
|
21
|
+
target?: string;
|
|
22
|
+
_src?: string;
|
|
23
|
+
_tgt?: string;
|
|
24
|
+
relation?: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
confidence?: string | number;
|
|
27
|
+
confidence_score?: number;
|
|
28
|
+
source_file?: string;
|
|
29
|
+
source_location?: string;
|
|
30
|
+
weight?: number;
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
export interface GraphImportOptions {
|
|
34
|
+
projectKey?: string;
|
|
35
|
+
maxNodes?: number;
|
|
36
|
+
maxEdges?: number;
|
|
37
|
+
}
|
|
38
|
+
export interface GraphImportNode {
|
|
39
|
+
external_id: string;
|
|
40
|
+
stable_id: string;
|
|
41
|
+
label: string;
|
|
42
|
+
node_type: "CODE_SYMBOL" | "DOCUMENT" | "CODE_NODE";
|
|
43
|
+
file_type?: string;
|
|
44
|
+
source_file?: string;
|
|
45
|
+
source_location?: string;
|
|
46
|
+
community?: number | string;
|
|
47
|
+
provenance: NormalizedProvenance;
|
|
48
|
+
}
|
|
49
|
+
export interface GraphImportEdge {
|
|
50
|
+
source_id: string;
|
|
51
|
+
target_id: string;
|
|
52
|
+
relation: string;
|
|
53
|
+
weight?: number;
|
|
54
|
+
provenance: NormalizedProvenance;
|
|
55
|
+
}
|
|
56
|
+
export interface GraphImportPlan {
|
|
57
|
+
version: "graphify-node-link/v1";
|
|
58
|
+
project_key: string;
|
|
59
|
+
nodes: GraphImportNode[];
|
|
60
|
+
edges: GraphImportEdge[];
|
|
61
|
+
warnings: string[];
|
|
62
|
+
stats: {
|
|
63
|
+
nodes: number;
|
|
64
|
+
edges: number;
|
|
65
|
+
warnings: number;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Validate and convert a Graphify node-link JSON value into a dry-run plan. */
|
|
69
|
+
export declare function buildGraphImportPlan(input: unknown, options?: GraphImportOptions): GraphImportPlan;
|
|
70
|
+
/** Render a human-auditable plan; this function never performs a write. */
|
|
71
|
+
export declare function renderGraphImportPlan(plan: GraphImportPlan): string;
|
|
72
|
+
//# sourceMappingURL=graphify-import.d.ts.map
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure Graphify node-link importer boundary.
|
|
3
|
+
*
|
|
4
|
+
* This module intentionally produces a dry-run plan only. A later adapter
|
|
5
|
+
* may apply the plan to Neo4j, but parsing and safety checks must be testable
|
|
6
|
+
* without a database or a filesystem write.
|
|
7
|
+
*/
|
|
8
|
+
import { normalizeProjectRelativePath, normalizeProvenance, stableCodeNodeId, } from "./graph-provenance.js";
|
|
9
|
+
function asRecord(value) {
|
|
10
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
11
|
+
? value
|
|
12
|
+
: {};
|
|
13
|
+
}
|
|
14
|
+
function requiredString(value, label) {
|
|
15
|
+
if (typeof value !== "string" || !value.trim())
|
|
16
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
17
|
+
return value.trim();
|
|
18
|
+
}
|
|
19
|
+
function safeCount(value, fallback) {
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
return fallback;
|
|
22
|
+
if (!Number.isInteger(value) || value < 1)
|
|
23
|
+
throw new Error("import limits must be positive integers");
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function normalizeRelation(value) {
|
|
27
|
+
const raw = typeof value === "string" && value.trim() ? value.trim() : "RELATED_TO";
|
|
28
|
+
return raw.normalize("NFKC").toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "RELATED_TO";
|
|
29
|
+
}
|
|
30
|
+
function languageForNode(node, sourceFile) {
|
|
31
|
+
if (typeof node.language === "string" && node.language.trim())
|
|
32
|
+
return node.language.trim();
|
|
33
|
+
const fileType = typeof node.file_type === "string" ? node.file_type.trim() : "";
|
|
34
|
+
if (fileType && fileType.toLowerCase() !== "code")
|
|
35
|
+
return fileType;
|
|
36
|
+
const extension = sourceFile?.split(".").pop();
|
|
37
|
+
return extension || "code";
|
|
38
|
+
}
|
|
39
|
+
function nodeTypeFor(node) {
|
|
40
|
+
const fileType = String(node.file_type ?? "code").toLowerCase();
|
|
41
|
+
if (fileType === "document" || fileType === "doc" || fileType === "pdf")
|
|
42
|
+
return "DOCUMENT";
|
|
43
|
+
if (fileType === "code" || fileType === "source")
|
|
44
|
+
return "CODE_SYMBOL";
|
|
45
|
+
return "CODE_NODE";
|
|
46
|
+
}
|
|
47
|
+
/** Validate and convert a Graphify node-link JSON value into a dry-run plan. */
|
|
48
|
+
export function buildGraphImportPlan(input, options = {}) {
|
|
49
|
+
const root = asRecord(input);
|
|
50
|
+
if (!Array.isArray(root.nodes) || root.nodes.length === 0) {
|
|
51
|
+
throw new Error("nodes must be a non-empty array");
|
|
52
|
+
}
|
|
53
|
+
const hasLinks = root.links !== undefined;
|
|
54
|
+
const hasEdges = root.edges !== undefined;
|
|
55
|
+
const rawEdges = hasLinks ? root.links : root.edges;
|
|
56
|
+
if (rawEdges !== undefined && !Array.isArray(rawEdges)) {
|
|
57
|
+
throw new Error("links/edges must be an array");
|
|
58
|
+
}
|
|
59
|
+
const links = (rawEdges ?? []);
|
|
60
|
+
const maxNodes = safeCount(options.maxNodes, 50_000);
|
|
61
|
+
const maxEdges = safeCount(options.maxEdges, 100_000);
|
|
62
|
+
if (root.nodes.length > maxNodes)
|
|
63
|
+
throw new Error(`graph has too many nodes (max ${maxNodes})`);
|
|
64
|
+
if (links.length > maxEdges)
|
|
65
|
+
throw new Error(`graph has too many edges (max ${maxEdges})`);
|
|
66
|
+
const projectKey = requiredString(options.projectKey ?? "project", "projectKey");
|
|
67
|
+
const warnings = [];
|
|
68
|
+
if (hasLinks && hasEdges)
|
|
69
|
+
warnings.push("both links and edges were supplied; links took precedence");
|
|
70
|
+
const seenIds = new Set();
|
|
71
|
+
const nodes = [];
|
|
72
|
+
const nodeByExternalId = new Map();
|
|
73
|
+
for (const rawNode of root.nodes) {
|
|
74
|
+
const node = asRecord(rawNode);
|
|
75
|
+
const externalId = requiredString(node.id, "node id");
|
|
76
|
+
if (seenIds.has(externalId))
|
|
77
|
+
throw new Error(`duplicate node id '${externalId}'`);
|
|
78
|
+
seenIds.add(externalId);
|
|
79
|
+
const sourceFile = node.source_file === undefined
|
|
80
|
+
? undefined
|
|
81
|
+
: normalizeProjectRelativePath(requiredString(node.source_file, `source_file for '${externalId}'`));
|
|
82
|
+
const label = typeof node.label === "string" && node.label.trim() ? node.label.trim() : externalId;
|
|
83
|
+
const stableId = stableCodeNodeId(sourceFile ?? externalId, languageForNode(node, sourceFile), label, externalId);
|
|
84
|
+
const provenance = normalizeProvenance({
|
|
85
|
+
...node,
|
|
86
|
+
source_file: sourceFile,
|
|
87
|
+
source_location: node.source_location,
|
|
88
|
+
});
|
|
89
|
+
const imported = {
|
|
90
|
+
external_id: externalId,
|
|
91
|
+
stable_id: stableId,
|
|
92
|
+
label,
|
|
93
|
+
node_type: nodeTypeFor(node),
|
|
94
|
+
provenance,
|
|
95
|
+
};
|
|
96
|
+
if (typeof node.file_type === "string" && node.file_type.trim())
|
|
97
|
+
imported.file_type = node.file_type.trim();
|
|
98
|
+
if (sourceFile)
|
|
99
|
+
imported.source_file = sourceFile;
|
|
100
|
+
if (typeof node.source_location === "string" && node.source_location.trim())
|
|
101
|
+
imported.source_location = node.source_location.trim();
|
|
102
|
+
if (typeof node.community === "number" || typeof node.community === "string")
|
|
103
|
+
imported.community = node.community;
|
|
104
|
+
nodes.push(imported);
|
|
105
|
+
nodeByExternalId.set(externalId, imported);
|
|
106
|
+
}
|
|
107
|
+
const seenEdges = new Set();
|
|
108
|
+
const edges = [];
|
|
109
|
+
for (const rawLink of links) {
|
|
110
|
+
const link = asRecord(rawLink);
|
|
111
|
+
const source = requiredString(link.source ?? link._src, "edge source");
|
|
112
|
+
const target = requiredString(link.target ?? link._tgt, "edge target");
|
|
113
|
+
if (!nodeByExternalId.has(source))
|
|
114
|
+
throw new Error(`unknown source node '${source}'`);
|
|
115
|
+
if (!nodeByExternalId.has(target))
|
|
116
|
+
throw new Error(`unknown target node '${target}'`);
|
|
117
|
+
const relation = normalizeRelation(link.relation ?? link.type);
|
|
118
|
+
const edgeKey = `${source}\u0000${target}\u0000${relation}`;
|
|
119
|
+
if (seenEdges.has(edgeKey))
|
|
120
|
+
throw new Error(`duplicate edge '${source}' -> '${target}' (${relation})`);
|
|
121
|
+
seenEdges.add(edgeKey);
|
|
122
|
+
const provenance = normalizeProvenance(link);
|
|
123
|
+
const edge = {
|
|
124
|
+
source_id: nodeByExternalId.get(source).stable_id,
|
|
125
|
+
target_id: nodeByExternalId.get(target).stable_id,
|
|
126
|
+
relation,
|
|
127
|
+
provenance,
|
|
128
|
+
};
|
|
129
|
+
const weight = Number(link.weight);
|
|
130
|
+
if (Number.isFinite(weight))
|
|
131
|
+
edge.weight = weight;
|
|
132
|
+
edges.push(edge);
|
|
133
|
+
}
|
|
134
|
+
// Stable ordering makes review diffs and future cache keys reproducible.
|
|
135
|
+
nodes.sort((a, b) => a.stable_id.localeCompare(b.stable_id));
|
|
136
|
+
edges.sort((a, b) => `${a.source_id}:${a.relation}:${a.target_id}`.localeCompare(`${b.source_id}:${b.relation}:${b.target_id}`));
|
|
137
|
+
return {
|
|
138
|
+
version: "graphify-node-link/v1",
|
|
139
|
+
project_key: projectKey,
|
|
140
|
+
nodes,
|
|
141
|
+
edges,
|
|
142
|
+
warnings,
|
|
143
|
+
stats: { nodes: nodes.length, edges: edges.length, warnings: warnings.length },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function oneLine(value, max = 100) {
|
|
147
|
+
const flattened = value.replace(/[\r\n]+/g, " ").trim();
|
|
148
|
+
return flattened.length > max ? `${flattened.slice(0, max - 1)}…` : flattened;
|
|
149
|
+
}
|
|
150
|
+
/** Render a human-auditable plan; this function never performs a write. */
|
|
151
|
+
export function renderGraphImportPlan(plan) {
|
|
152
|
+
const lines = [
|
|
153
|
+
"## Graphify Import Plan (dry-run)",
|
|
154
|
+
"",
|
|
155
|
+
`Project: \`${plan.project_key}\``,
|
|
156
|
+
`Version: \`${plan.version}\``,
|
|
157
|
+
`Nodes: **${plan.stats.nodes}** · Edges: **${plan.stats.edges}** · Warnings: **${plan.stats.warnings}**`,
|
|
158
|
+
"",
|
|
159
|
+
"> No Neo4j, Qdrant, or memory-cube data was written.",
|
|
160
|
+
"",
|
|
161
|
+
];
|
|
162
|
+
if (plan.warnings.length > 0) {
|
|
163
|
+
lines.push("### Warnings", ...plan.warnings.map((warning) => `- ${oneLine(warning)}`), "");
|
|
164
|
+
}
|
|
165
|
+
if (plan.nodes.length > 0) {
|
|
166
|
+
lines.push("### Node preview");
|
|
167
|
+
for (const node of plan.nodes.slice(0, 8)) {
|
|
168
|
+
const location = node.source_file
|
|
169
|
+
? ` (${node.source_file}${node.source_location ? `:${node.source_location}` : ""})`
|
|
170
|
+
: "";
|
|
171
|
+
lines.push(`- \`${node.stable_id}\` — ${oneLine(node.label)}${location}`);
|
|
172
|
+
}
|
|
173
|
+
if (plan.nodes.length > 8)
|
|
174
|
+
lines.push(`- … ${plan.nodes.length - 8} more node(s)`);
|
|
175
|
+
lines.push("");
|
|
176
|
+
}
|
|
177
|
+
if (plan.edges.length > 0) {
|
|
178
|
+
lines.push("### Edge preview");
|
|
179
|
+
for (const edge of plan.edges.slice(0, 8)) {
|
|
180
|
+
lines.push(`- \`${edge.relation}\`: \`${edge.source_id}\` → \`${edge.target_id}\` (${formatProvenanceForPlan(edge.provenance)})`);
|
|
181
|
+
}
|
|
182
|
+
if (plan.edges.length > 8)
|
|
183
|
+
lines.push(`- … ${plan.edges.length - 8} more edge(s)`);
|
|
184
|
+
}
|
|
185
|
+
return lines.join("\n");
|
|
186
|
+
}
|
|
187
|
+
function formatProvenanceForPlan(provenance) {
|
|
188
|
+
const score = provenance.confidence_score === undefined ? "n/a" : provenance.confidence_score.toFixed(2);
|
|
189
|
+
const source = provenance.source_file
|
|
190
|
+
? `${provenance.source_file}${provenance.source_location ? `:${provenance.source_location}` : ""}`
|
|
191
|
+
: "no source";
|
|
192
|
+
return `${provenance.evidence_kind}, confidence ${score}, ${source}`;
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=graphify-import.js.map
|
package/dist/handlers/admin.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* memos_admin(action="validate_cubes"), memos_delete
|
|
6
6
|
*/
|
|
7
7
|
import type { TextContent } from "../types.js";
|
|
8
|
+
export declare function handleMemosCapabilities(): TextContent[];
|
|
8
9
|
export declare function handleMemosListCubes(arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
9
10
|
export declare function handleMemosRegisterCube(arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
10
11
|
export declare function handleMemosCreateUser(arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
package/dist/handlers/admin.js
CHANGED
|
@@ -11,6 +11,24 @@ import { fetchWithTimeout } from "../api-client.js";
|
|
|
11
11
|
import { ensureCubeDirectory, ensureCubeRegistered, getCubePath, getCubesBaseDir, getLocalCubesBaseDir, listAvailableCubes, validateAndFixCubeConfig, verifyCubeLoaded, } from "../cube-manager.js";
|
|
12
12
|
import { ERR_CUBE_NOT_FOUND, ERR_DELETE_DISABLED, ERR_PARAM_MISSING, apiErrorResponse, cubeRegistrationError, errorResponse, getCubeIdFromArgs, } from "./utils.js";
|
|
13
13
|
// ============================================================================
|
|
14
|
+
// memos_admin(action="capabilities")
|
|
15
|
+
// ============================================================================
|
|
16
|
+
export function handleMemosCapabilities() {
|
|
17
|
+
const mode = process.env.MEMOS_MODE?.toLowerCase() === "lite" ? "lite" : "full";
|
|
18
|
+
return [{ type: "text", text: [
|
|
19
|
+
"## MemOS capabilities",
|
|
20
|
+
"",
|
|
21
|
+
`**Mode**: ${mode}`,
|
|
22
|
+
mode === "lite"
|
|
23
|
+
? "Lite reuses the configured HTTP API and cube storage; it lowers search limits and filters auto-capture. It is a noise/resource policy, not an offline SQLite backend."
|
|
24
|
+
: "Full exposes API, graph, vector, BM25, Wiki round-trip, canvas, and skill lifecycle tools.",
|
|
25
|
+
"",
|
|
26
|
+
"**tree_text update**: in-place update is unsupported; the API now fails explicitly instead of silently succeeding. Use versioned writes.",
|
|
27
|
+
"**Skill install**: requires an approved candidate and writes only to `.claude/skills/<slug>/SKILL.md`.",
|
|
28
|
+
"**Auto-capture**: disabled by default; Lite keeps it disabled.",
|
|
29
|
+
].join("\n") }];
|
|
30
|
+
}
|
|
31
|
+
// ============================================================================
|
|
14
32
|
// memos_admin(action="list_cubes")
|
|
15
33
|
// ============================================================================
|
|
16
34
|
export async function handleMemosListCubes(arguments_) {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memos_canvas — symbolic short-term task memory.
|
|
3
|
+
*
|
|
4
|
+
* The long-term side of this server (memories, graph, wiki) answers "what do we
|
|
5
|
+
* know". The canvas answers "where was I", which is a different question with a
|
|
6
|
+
* different lifetime: it changes several times an hour, it is worthless a week
|
|
7
|
+
* later, and it must survive exactly one event — context compaction.
|
|
8
|
+
*
|
|
9
|
+
* Why a Mermaid file rather than more memories:
|
|
10
|
+
*
|
|
11
|
+
* - A memory costs an LLM extraction and an embedding on write. A status flip
|
|
12
|
+
* from doing→done deserves neither.
|
|
13
|
+
* - The value of the canvas is its *topology* — which step follows which, and
|
|
14
|
+
* which one is stuck. Flat memory rows lose exactly that.
|
|
15
|
+
* - `node_id` is greppable, so a node can be cited from a commit message or a
|
|
16
|
+
* memory body and still be found later.
|
|
17
|
+
*
|
|
18
|
+
* The `ref` field is where the two halves meet: a node points at durable
|
|
19
|
+
* evidence with `mem:<uuid>` (a memory in the graph), `file:<path>` (a file the
|
|
20
|
+
* harness already offloaded), or `note:<text>`. Abstraction stays cheap, and the
|
|
21
|
+
* path back to evidence stays open.
|
|
22
|
+
*
|
|
23
|
+
* One thing this deliberately does not do: claim a token saving. That claim
|
|
24
|
+
* belongs to designs that intercept and replace tool output before the model
|
|
25
|
+
* sees it, which a Claude Code hook cannot do — PostToolUse can add context but
|
|
26
|
+
* not rewrite a result. What is on offer here is task state that survives
|
|
27
|
+
* compaction, and anchors that make a summary checkable.
|
|
28
|
+
*/
|
|
29
|
+
import type { TextContent } from "../types.js";
|
|
30
|
+
export declare function handleMemosCanvas(arguments_: Record<string, unknown>): Promise<TextContent[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Active-canvas digest for `memos_context_resume`.
|
|
33
|
+
*
|
|
34
|
+
* Headlines only — a few dozen tokens for the whole index. Injecting canvas
|
|
35
|
+
* bodies here would rebuild the context bloat the canvas exists to avoid; the
|
|
36
|
+
* model opens what it needs with `show`. Never throws: this runs during session
|
|
37
|
+
* recovery, and a canvas problem must not break that.
|
|
38
|
+
*/
|
|
39
|
+
export declare function summarizeActiveCanvases(cubeId: string, limit?: number): string[];
|
|
40
|
+
//# sourceMappingURL=canvas.d.ts.map
|