@mporenta/mg 0.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/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # mg
2
+
3
+ Agent-friendly CLI for the memory-graph-mcp FastAPI surface.
4
+
5
+ ```bash
6
+ npm i -g @mporenta/mg
7
+ export MEMORY_GRAPH_URL=http://localhost:8000
8
+ mg config doctor
9
+ ```
10
+
11
+ Data is written to stdout; diagnostics are written to stderr. JSON is emitted by default when stdout is not a TTY. Use `--format pretty` for human-readable output.
12
+
13
+ ## Search recipes
14
+
15
+ All search subcommands require `--query` and return JSON-friendly results with `diagnostics` and `next_steps` hints.
16
+
17
+ ```bash
18
+ # Discover candidate entities before narrowing context.
19
+ mg search entities --query "memory graph" --limit 5
20
+
21
+ # Recall repo-scoped codebase context with paths and observation IDs.
22
+ mg search codebase --query "search diagnostics" --repo memory-graph-mcp --limit 5
23
+
24
+ # Pass an embedding vector when a caller can produce one for hybrid ranking.
25
+ mg search codebase --query "ranking" --repo memory-graph-mcp --query-embedding 0.1,0.2,0.3
26
+
27
+ # Follow up on exact observations for a known entity.
28
+ mg search memory --query "noise_floor" --entity "memory-graph-mcp" --limit 5
29
+ ```
package/dist/mg.js ADDED
@@ -0,0 +1,698 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // package.json
7
+ var package_default = {
8
+ name: "@mporenta/mg",
9
+ version: "0.1.0",
10
+ description: "Agent-friendly CLI for memory-graph-mcp",
11
+ license: "MIT",
12
+ type: "module",
13
+ bin: {
14
+ mg: "dist/mg.js"
15
+ },
16
+ files: [
17
+ "dist",
18
+ "generated",
19
+ "README.md"
20
+ ],
21
+ engines: {
22
+ node: ">=18"
23
+ },
24
+ scripts: {
25
+ build: "esbuild src/index.ts --bundle --packages=external --platform=node --format=esm --target=node18 --outfile=dist/mg.js --banner:js='#!/usr/bin/env node' && chmod +x dist/mg.js",
26
+ check: "tsc --noEmit",
27
+ test: "npm run build && node --test test/*.test.mjs",
28
+ "pack:dry-run": "npm pack --dry-run",
29
+ prepack: "npm run build",
30
+ "generate:types": "openapi-typescript ${MEMORY_GRAPH_OPENAPI_URL:-http://localhost:8000/openapi.json} -o generated/api.d.ts"
31
+ },
32
+ dependencies: {
33
+ commander: "^12.1.0",
34
+ "openapi-fetch": "^0.13.4"
35
+ },
36
+ devDependencies: {
37
+ "@types/node": "^22.10.1",
38
+ esbuild: "^0.24.0",
39
+ "openapi-typescript": "^7.4.4",
40
+ typescript: "^5.7.2"
41
+ }
42
+ };
43
+
44
+ // src/client.ts
45
+ import createClient from "openapi-fetch";
46
+ var EXIT_USER = 1;
47
+ var EXIT_SERVER = 2;
48
+ var EXIT_CONFIG = 3;
49
+ var MgError = class extends Error {
50
+ exitCode;
51
+ detail;
52
+ constructor(message, exitCode, detail) {
53
+ super(message);
54
+ this.name = "MgError";
55
+ this.exitCode = exitCode;
56
+ this.detail = detail;
57
+ }
58
+ };
59
+ function readConfig() {
60
+ const baseUrl = (process.env.MEMORY_GRAPH_URL || "http://localhost:8000").replace(/\/+$/, "");
61
+ if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
62
+ throw new MgError(
63
+ "MEMORY_GRAPH_URL must start with http:// or https://",
64
+ EXIT_CONFIG
65
+ );
66
+ }
67
+ return {
68
+ baseUrl,
69
+ token: process.env.MEMORY_GRAPH_TOKEN
70
+ };
71
+ }
72
+ async function postTool(tool, body) {
73
+ const config = readConfig();
74
+ const headers = {
75
+ "content-type": "application/json"
76
+ };
77
+ if (config.token) {
78
+ headers.authorization = `Bearer ${config.token}`;
79
+ }
80
+ let response;
81
+ try {
82
+ response = await fetch(`${config.baseUrl}/v1/${tool}`, {
83
+ method: "POST",
84
+ headers,
85
+ body: JSON.stringify(body)
86
+ });
87
+ } catch (error) {
88
+ throw new MgError(
89
+ `Could not reach memory graph server at ${config.baseUrl}: ${stringifyError(error)}`,
90
+ EXIT_CONFIG,
91
+ { cause: stringifyError(error) }
92
+ );
93
+ }
94
+ return await parseResponse(response, tool);
95
+ }
96
+ async function getJson(path) {
97
+ const config = readConfig();
98
+ const headers = {};
99
+ if (config.token) {
100
+ headers.authorization = `Bearer ${config.token}`;
101
+ }
102
+ let response;
103
+ try {
104
+ response = await fetch(`${config.baseUrl}${path}`, { headers });
105
+ } catch (error) {
106
+ throw new MgError(
107
+ `Could not reach memory graph server at ${config.baseUrl}: ${stringifyError(error)}`,
108
+ EXIT_CONFIG,
109
+ { cause: stringifyError(error) }
110
+ );
111
+ }
112
+ return await parseResponse(response, path);
113
+ }
114
+ async function parseResponse(response, tool) {
115
+ const text = await response.text();
116
+ let payload;
117
+ if (text.length > 0) {
118
+ try {
119
+ payload = JSON.parse(text);
120
+ } catch {
121
+ payload = text;
122
+ }
123
+ }
124
+ if (response.ok) {
125
+ return payload;
126
+ }
127
+ const message = extractMessage(payload) || `${tool} failed with HTTP ${response.status}`;
128
+ const exitCode = response.status >= 500 ? EXIT_SERVER : EXIT_USER;
129
+ throw new MgError(message, exitCode, payload);
130
+ }
131
+ function extractMessage(payload) {
132
+ if (payload && typeof payload === "object" && "message" in payload) {
133
+ const value = payload.message;
134
+ if (typeof value === "string") return value;
135
+ }
136
+ if (payload && typeof payload === "object" && "detail" in payload) {
137
+ const value = payload.detail;
138
+ if (typeof value === "string") return value;
139
+ }
140
+ return void 0;
141
+ }
142
+ function render(data, format = defaultFormat()) {
143
+ if (format === "json") {
144
+ process.stdout.write(`${JSON.stringify(data, null, 2)}
145
+ `);
146
+ return;
147
+ }
148
+ process.stdout.write(`${pretty(data)}
149
+ `);
150
+ }
151
+ function defaultFormat() {
152
+ return process.stdout.isTTY ? "pretty" : "json";
153
+ }
154
+ function parseFormat(value) {
155
+ if (value === void 0) return defaultFormat();
156
+ if (value === "json" || value === "pretty") return value;
157
+ throw new MgError("--format must be either 'json' or 'pretty'", EXIT_USER);
158
+ }
159
+ function compact(obj) {
160
+ return Object.fromEntries(
161
+ Object.entries(obj).filter(
162
+ ([, value]) => value !== void 0 && value !== ""
163
+ )
164
+ );
165
+ }
166
+ function parseCsv(value) {
167
+ if (!value) return void 0;
168
+ const items = value.split(",").map((item) => item.trim()).filter(Boolean);
169
+ return items.length > 0 ? items : void 0;
170
+ }
171
+ function parseFloatCsv(value) {
172
+ if (!value) return void 0;
173
+ const items = value.split(",").map((item) => item.trim()).filter(Boolean).map((item) => Number.parseFloat(item));
174
+ if (items.some((item) => !Number.isFinite(item))) {
175
+ throw new MgError("Expected a comma-separated list of numbers", EXIT_USER);
176
+ }
177
+ return items.length > 0 ? items : void 0;
178
+ }
179
+ function parseJsonObject(value) {
180
+ if (!value) return void 0;
181
+ const parsed = JSON.parse(value);
182
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
183
+ throw new MgError("Expected a JSON object", EXIT_USER);
184
+ }
185
+ return parsed;
186
+ }
187
+ function parseJsonObjectOrString(value) {
188
+ if (!value) return void 0;
189
+ const trimmed = value.trim();
190
+ if (!trimmed.startsWith("{")) {
191
+ return value;
192
+ }
193
+ return parseJsonObject(trimmed);
194
+ }
195
+ async function readStdin() {
196
+ const chunks = [];
197
+ for await (const chunk of process.stdin) {
198
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
199
+ }
200
+ return Buffer.concat(chunks).toString("utf8");
201
+ }
202
+ async function readFileOrStdin(path) {
203
+ if (path === "-") {
204
+ return await readStdin();
205
+ }
206
+ const fs = await import("node:fs/promises");
207
+ return await fs.readFile(path, "utf8");
208
+ }
209
+ async function runAndRender(tool, body, opts, transform) {
210
+ const requestBody = compact(body);
211
+ const data = await postTool(tool, requestBody);
212
+ render(
213
+ transform ? transform(data, requestBody) : data,
214
+ parseFormat(opts.format)
215
+ );
216
+ }
217
+ function handleError(error) {
218
+ if (error instanceof MgError) {
219
+ process.stderr.write(`${error.message}
220
+ `);
221
+ if (error.detail !== void 0 && process.env.MG_DEBUG) {
222
+ process.stderr.write(`${JSON.stringify(error.detail, null, 2)}
223
+ `);
224
+ }
225
+ process.exit(error.exitCode);
226
+ }
227
+ process.stderr.write(`${stringifyError(error)}
228
+ `);
229
+ process.exit(EXIT_SERVER);
230
+ }
231
+ function stringifyError(error) {
232
+ if (error instanceof Error) return error.message;
233
+ return String(error);
234
+ }
235
+ function pretty(data) {
236
+ if (data === void 0 || data === null) return "";
237
+ if (typeof data !== "object") return String(data);
238
+ return JSON.stringify(data, null, 2);
239
+ }
240
+
241
+ // src/commands/admin.ts
242
+ function registerAdmin(program) {
243
+ const admin = program.command("admin").description("Administrative import, export, and consistency commands");
244
+ admin.command("consistency-check").description("Run graph consistency checks").action(async (_opts, cmd) => {
245
+ await runAndRender("run_consistency_check", {}, cmd.optsWithGlobals());
246
+ });
247
+ admin.command("export").description("Export graph JSON").option("--include-deleted", "include soft-deleted rows").option("--include-events", "include event audit rows").action(async (opts, cmd) => {
248
+ await runAndRender("export_graph_json", {
249
+ include_deleted: opts.includeDeleted,
250
+ include_events: opts.includeEvents
251
+ }, cmd.optsWithGlobals());
252
+ });
253
+ admin.command("import [path]").description("Import graph JSON from a file or stdin with path/-/--file -").option("--file <path>", "JSON file path or - for stdin").option("--merge-strategy <mode>", "skip or merge", "skip").action(async (path, opts, cmd) => {
254
+ const input = opts.file ?? path;
255
+ if (!input) throw new MgError("Provide a file path, --file <path>, or - for stdin", EXIT_USER);
256
+ const text = await readFileOrStdin(input);
257
+ await runAndRender("import_graph_json", {
258
+ payload: JSON.parse(text),
259
+ merge_strategy: opts.mergeStrategy
260
+ }, cmd.optsWithGlobals());
261
+ });
262
+ admin.command("import-legacy [path]").description("Import legacy @modelcontextprotocol/server-memory JSONL").option("--file <path>", "JSONL file path or - for stdin").action(async (path, opts, cmd) => {
263
+ const input = opts.file ?? path;
264
+ if (!input) throw new MgError("Provide a file path, --file <path>, or - for stdin", EXIT_USER);
265
+ await runAndRender("import_legacy_jsonl", {
266
+ jsonl: await readFileOrStdin(input)
267
+ }, cmd.optsWithGlobals());
268
+ });
269
+ admin.command("find-conflicts").description("List recorded memory conflicts").option("--entity <name>", "entity filter").option("--status <status>", "open, resolved, or ignored", "open").option("--limit <n>", "max conflicts", parseInt).action(async (opts, cmd) => {
270
+ await runAndRender("find_conflicts", {
271
+ entity: opts.entity,
272
+ status: opts.status,
273
+ limit: opts.limit
274
+ }, cmd.optsWithGlobals());
275
+ });
276
+ }
277
+
278
+ // src/commands/config.ts
279
+ function redact(value) {
280
+ if (!value) return void 0;
281
+ return value.length <= 4 ? "****" : `${value.slice(0, 2)}\u2026${value.slice(-2)}`;
282
+ }
283
+ function registerConfig(program) {
284
+ const config = program.command("config").description("Inspect CLI and server configuration");
285
+ config.command("doctor").description("Validate MEMORY_GRAPH_* configuration and server readiness").action(async (_opts, cmd) => {
286
+ const cfg = readConfig();
287
+ let health;
288
+ let ok = false;
289
+ try {
290
+ health = await getJson("/healthz");
291
+ ok = Boolean(health && typeof health === "object" && health.ready === true);
292
+ } catch (error) {
293
+ const payload2 = {
294
+ ok: false,
295
+ client_version: package_default.version,
296
+ env: {
297
+ MEMORY_GRAPH_URL: cfg.baseUrl,
298
+ MEMORY_GRAPH_TOKEN: redact(cfg.token)
299
+ },
300
+ error: error instanceof Error ? error.message : String(error)
301
+ };
302
+ render(payload2, parseFormat(cmd.optsWithGlobals().format));
303
+ throw new MgError("memory graph configuration check failed", EXIT_CONFIG, payload2);
304
+ }
305
+ const payload = {
306
+ ok,
307
+ client_version: package_default.version,
308
+ env: {
309
+ MEMORY_GRAPH_URL: cfg.baseUrl,
310
+ MEMORY_GRAPH_TOKEN: redact(cfg.token)
311
+ },
312
+ health
313
+ };
314
+ render(payload, parseFormat(cmd.optsWithGlobals().format));
315
+ if (!ok) {
316
+ throw new MgError("memory graph server is reachable but not ready", EXIT_CONFIG, payload);
317
+ }
318
+ });
319
+ }
320
+
321
+ // src/commands/entity.ts
322
+ function registerEntity(program) {
323
+ const entity = program.command("entity").description("Resolve, inspect, and merge entities");
324
+ entity.command("neighborhood").description("Fetch an entity neighborhood").requiredOption("--entity <name-or-id>", "entity name or id").option("--depth <n>", "depth 1-2", parseInt).option("--relation-types <csv>", "comma-separated relation types").option("--no-include-observations", "omit observations").option("--limit <n>", "max rows", parseInt).action(async (opts, cmd) => {
325
+ await runAndRender("get_entity_neighborhood", {
326
+ entity: opts.entity,
327
+ depth: opts.depth,
328
+ relation_types: opts.relationTypes ? String(opts.relationTypes).split(",").map((x) => x.trim()).filter(Boolean) : void 0,
329
+ include_observations: opts.includeObservations,
330
+ limit: opts.limit
331
+ }, cmd.optsWithGlobals());
332
+ });
333
+ entity.command("merge").description("Merge a duplicate entity into a target entity").requiredOption("--source <name-or-id>", "source duplicate entity").requiredOption("--target <name-or-id>", "target canonical entity").option("--reason <text>", "audit reason", "").action(async (opts, cmd) => {
334
+ await runAndRender("merge_entities", {
335
+ source_entity: opts.source,
336
+ target_entity: opts.target,
337
+ reason: opts.reason
338
+ }, cmd.optsWithGlobals());
339
+ });
340
+ entity.command("resolve").description("Resolve a free-form query to entity matches").requiredOption("--query <text>", "entity query").option("--limit <n>", "max matches", parseInt).action(async (opts, cmd) => {
341
+ await runAndRender("resolve_entity", {
342
+ query: opts.query,
343
+ limit: opts.limit
344
+ }, cmd.optsWithGlobals());
345
+ });
346
+ }
347
+
348
+ // src/commands/find.ts
349
+ function addKindOptions(command) {
350
+ return command.option("--query <text>", "search query", "").option("--repo <repo>", "repo filter").option("--branch <branch>", "branch filter").option("--limit <n>", "max results", parseInt);
351
+ }
352
+ function registerFind(program) {
353
+ const find = program.command("find").description("Find codebase-shaped memory slices");
354
+ addKindOptions(find.command("decisions").description("Find Decision entities")).action(async (opts, cmd) => {
355
+ await runAndRender("find_decisions", { kind: "Decision", query: opts.query, repo: opts.repo, branch: opts.branch, limit: opts.limit }, cmd.optsWithGlobals());
356
+ });
357
+ addKindOptions(find.command("runbooks").description("Find Runbook entities")).action(async (opts, cmd) => {
358
+ await runAndRender("find_runbooks", { kind: "Runbook", query: opts.query, repo: opts.repo, branch: opts.branch, limit: opts.limit }, cmd.optsWithGlobals());
359
+ });
360
+ addKindOptions(find.command("incidents").description("Find Incident entities")).action(async (opts, cmd) => {
361
+ await runAndRender("find_incidents", { kind: "Incident", query: opts.query, repo: opts.repo, branch: opts.branch, limit: opts.limit }, cmd.optsWithGlobals());
362
+ });
363
+ find.command("by-path").description("Find entities and observations by path glob").requiredOption("--path-glob <glob>", "SQL glob pattern").option("--repo <repo>", "repo filter").option("--branch <branch>", "branch filter").option("--limit <n>", "max results", parseInt).option("--include-fixtures", "include fixture data").action(async (opts, cmd) => {
364
+ await runAndRender("find_by_path", {
365
+ repo: opts.repo,
366
+ path_glob: opts.pathGlob,
367
+ branch: opts.branch,
368
+ limit: opts.limit,
369
+ include_fixtures: opts.includeFixtures
370
+ }, cmd.optsWithGlobals());
371
+ });
372
+ addKindOptions(find.command("by-kind").description("Find entities by kind")).requiredOption("--kind <kind>", "entity type").action(async (opts, cmd) => {
373
+ await runAndRender("find_by_kind", { kind: opts.kind, query: opts.query, repo: opts.repo, branch: opts.branch, limit: opts.limit }, cmd.optsWithGlobals());
374
+ });
375
+ }
376
+
377
+ // src/commands/memory.ts
378
+ function registerMemory(program) {
379
+ const memory = program.command("memory").description("Store, remove, and explain memories");
380
+ memory.command("remember").description("Store an observation about an entity").requiredOption("--entity-name <name>", "entity canonical name").option("--entity-type <type>", "entity type", "Person").requiredOption("--content <text>", "observation text").option("--confidence <number>", "confidence 0.0-1.0", parseFloat).option("--source <source>", "source ref or source object JSON").option("--sensitivity <level>", "normal, sensitive, or secret").option("--metadata <json>", "metadata JSON object").option("--repo <repo>", "repo metadata").option("--branch <branch>", "branch metadata").option("--path <path>", "path metadata").option("--is-fixture", "mark as fixture data").option("--memory-type <type>", "working, episodic, semantic, procedural, preference, or policy").option("--scope <scope>", "memory scope", "project").option("--quality-score <number>", "quality score 0.0-1.0", parseFloat).option("--evidence-count <n>", "number of supporting evidence items", parseInt).option("--verification-status <status>", "unverified, verified, failed, or partial").option("--last-verified-at <iso>", "last verification timestamp").option("--source-run-id <uuid>", "source memory_runs id").option("--subtask-type <type>", "coding subtask type").option("--success-score <number>", "success score 0.0-1.0", parseFloat).action(async (opts, cmd) => {
381
+ await runAndRender("remember_observation", compact({
382
+ entity_name: opts.entityName,
383
+ entity_type: opts.entityType,
384
+ content: opts.content,
385
+ confidence: opts.confidence,
386
+ source: parseJsonObjectOrString(opts.source),
387
+ sensitivity: opts.sensitivity,
388
+ metadata: parseJsonObject(opts.metadata),
389
+ repo: opts.repo,
390
+ branch: opts.branch,
391
+ path: opts.path,
392
+ is_fixture: opts.isFixture,
393
+ memory_type: opts.memoryType,
394
+ scope: opts.scope,
395
+ quality_score: opts.qualityScore,
396
+ evidence_count: opts.evidenceCount,
397
+ verification_status: opts.verificationStatus,
398
+ last_verified_at: opts.lastVerifiedAt,
399
+ source_run_id: opts.sourceRunId,
400
+ subtask_type: opts.subtaskType,
401
+ success_score: opts.successScore
402
+ }), cmd.optsWithGlobals());
403
+ });
404
+ memory.command("forget").description("Forget an observation or entity").option("--query <text>", "substring query for observation forget").option("--target-id <uuid>", "target observation/entity id").option("--mode <mode>", "soft, stale, supersede, or hard", "soft").option("--reason <text>", "audit reason").option("--target-type <type>", "observation or entity", "observation").option("--superseded-by-content <text>", "replacement content for supersede mode").option("--entity-name <name>", "entity name for scoped forget/explain").action(async (opts, cmd) => {
405
+ await runAndRender("forget_memory", {
406
+ query: opts.query ?? "",
407
+ target_id: opts.targetId,
408
+ mode: opts.mode,
409
+ reason: opts.reason ?? "",
410
+ target_type: opts.targetType,
411
+ superseded_by_content: opts.supersededByContent,
412
+ entity_name: opts.entityName
413
+ }, cmd.optsWithGlobals());
414
+ });
415
+ memory.command("explain").description("Explain an observation, entity, or relation").requiredOption("--target-type <type>", "observation, entity, or relation").option("--target-id <uuid>", "target id").option("--query <text>", "lookup query").option("--entity-name <name>", "entity name").action(async (opts, cmd) => {
416
+ await runAndRender("explain_memory", {
417
+ target_type: opts.targetType,
418
+ target_id: opts.targetId,
419
+ query: opts.query,
420
+ entity_name: opts.entityName
421
+ }, cmd.optsWithGlobals());
422
+ });
423
+ }
424
+
425
+ // src/commands/relation.ts
426
+ function registerRelation(program) {
427
+ const relation = program.command("relation").description("Create and manage relationships");
428
+ relation.command("create").description("Create a relation between two entities").requiredOption("--from <name>", "source entity name").requiredOption("--to <name>", "target entity name").requiredOption("--type <type>", "relation type").option("--confidence <number>", "confidence 0.0-1.0", parseFloat).option("--source <source>", "source ref or source object JSON").option("--metadata <json>", "metadata JSON object").option("--repo <repo>", "repo metadata").option("--branch <branch>", "branch metadata").option("--path <path>", "path metadata").action(async (opts, cmd) => {
429
+ await runAndRender("create_relation_v2", compact({
430
+ from_entity: opts.from,
431
+ to_entity: opts.to,
432
+ relation_type: opts.type,
433
+ confidence: opts.confidence,
434
+ source: parseJsonObjectOrString(opts.source),
435
+ metadata: parseJsonObject(opts.metadata),
436
+ repo: opts.repo,
437
+ branch: opts.branch,
438
+ path: opts.path
439
+ }), cmd.optsWithGlobals());
440
+ });
441
+ }
442
+
443
+ // src/search_output.ts
444
+ var SEARCH_TEXT_KEYS = ["content", "matched_text", "snippet"];
445
+ var SCORE_THRESHOLD = 0.3;
446
+ function transformSearchResult(tool, requestBody, data) {
447
+ if (!isRecord(data)) {
448
+ return data;
449
+ }
450
+ const augmented = addDisplayFields(data);
451
+ if (!isRecord(augmented)) {
452
+ return augmented;
453
+ }
454
+ const hits = Array.isArray(augmented.hits) ? augmented.hits.filter(isRecord) : [];
455
+ const topScore = topHitScore(hits);
456
+ const noiseFloor = typeof augmented.noise_floor === "boolean" ? augmented.noise_floor : hits.length === 0 || (topScore ?? 0) < SCORE_THRESHOLD;
457
+ return {
458
+ ...augmented,
459
+ noise_floor: noiseFloor,
460
+ diagnostics: {
461
+ hit_count: hits.length,
462
+ noise_floor: noiseFloor,
463
+ top_score: topScore
464
+ },
465
+ next_steps: nextSteps(tool, requestBody, hits, noiseFloor)
466
+ };
467
+ }
468
+ function addDisplayFields(value) {
469
+ if (Array.isArray(value)) {
470
+ return value.map((item) => addDisplayFields(item));
471
+ }
472
+ if (isRecord(value)) {
473
+ const augmented = Object.fromEntries(
474
+ Object.entries(value).map(([entryKey, entryValue]) => [
475
+ entryKey,
476
+ addDisplayFields(entryValue)
477
+ ])
478
+ );
479
+ for (const key of SEARCH_TEXT_KEYS) {
480
+ const raw = value[key];
481
+ if (typeof raw !== "string") continue;
482
+ const display = cleanSearchText(raw);
483
+ if (display !== raw) {
484
+ augmented[`${key}_display`] = display;
485
+ }
486
+ }
487
+ return augmented;
488
+ }
489
+ return value;
490
+ }
491
+ function cleanSearchText(value) {
492
+ return value.replace(/,?StartSel=/g, "").replace(/,?StopSel=/g, "").replace(/<\/?b>/g, "");
493
+ }
494
+ function topHitScore(hits) {
495
+ const scores = hits.map((hit) => hit.score).filter(
496
+ (score) => typeof score === "number" && Number.isFinite(score)
497
+ );
498
+ return scores.length > 0 ? Math.max(...scores) : null;
499
+ }
500
+ function nextSteps(tool, requestBody, hits, noiseFloor) {
501
+ const steps = [];
502
+ const query = typeof requestBody.query === "string" ? requestBody.query : "";
503
+ if (hits.length === 0 || noiseFloor) {
504
+ steps.push(
505
+ "Broaden the query or remove filters if these hits are not enough."
506
+ );
507
+ }
508
+ const firstHit = hits[0];
509
+ if (!firstHit) {
510
+ steps.push(
511
+ `Retry with fewer filters: mg search ${commandName(tool)} --query ${shellQuote(query)} --limit 5`
512
+ );
513
+ return steps;
514
+ }
515
+ if (tool === "search_memory") {
516
+ const observationId = stringValue(firstHit.observation_id);
517
+ if (observationId) {
518
+ steps.push(
519
+ `Inspect the top observation: mg memory explain --target-type observation --target-id ${shellQuote(observationId)}`
520
+ );
521
+ }
522
+ }
523
+ if (tool === "search_entities") {
524
+ const canonicalName = stringValue(firstHit.canonical_name);
525
+ if (canonicalName) {
526
+ steps.push(
527
+ `Search observations for the top entity: mg search memory --query ${shellQuote(query)} --entity ${shellQuote(canonicalName)} --limit 5`
528
+ );
529
+ }
530
+ }
531
+ if (tool === "search_codebase") {
532
+ const repo = stringValue(firstHit.repo) ?? stringValue(requestBody.repo);
533
+ const path = stringValue(firstHit.path);
534
+ if (repo && path) {
535
+ steps.push(
536
+ `Find related path entries: mg find by-path --repo ${shellQuote(repo)} --path-glob ${shellQuote(`%${path}%`)}`
537
+ );
538
+ }
539
+ const observationId = stringValue(firstHit.observation_id);
540
+ if (observationId) {
541
+ steps.push(
542
+ `Inspect the top observation: mg memory explain --target-type observation --target-id ${shellQuote(observationId)}`
543
+ );
544
+ }
545
+ }
546
+ return steps.slice(0, 3);
547
+ }
548
+ function commandName(tool) {
549
+ if (tool === "search_memory") return "memory";
550
+ if (tool === "search_entities") return "entities";
551
+ return "codebase";
552
+ }
553
+ function stringValue(value) {
554
+ return typeof value === "string" && value.length > 0 ? value : void 0;
555
+ }
556
+ function isRecord(value) {
557
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
558
+ }
559
+ function shellQuote(value) {
560
+ return `'${value.replace(/'/g, "'\\''")}'`;
561
+ }
562
+
563
+ // src/commands/search.ts
564
+ function registerSearch(program) {
565
+ const search = program.command("search").description("Search memories, entities, and codebase context");
566
+ search.command("memory").description("Search observation text").requiredOption("--query <text>", "search query").option("--limit <n>", "max results", parseInt).option("--include-stale", "include stale memories").option("--entity <name>", "restrict to entity").option(
567
+ "--exclude-entity-types <csv>",
568
+ "comma-separated entity types to exclude"
569
+ ).option(
570
+ "--include-entity-types <csv>",
571
+ "comma-separated entity types to include"
572
+ ).option("--content-preview-chars <n>", "preview length", parseInt).option("--include-fixtures", "include fixture data").addHelpText(
573
+ "after",
574
+ `
575
+
576
+ Examples:
577
+ $ mg search memory --query "airflow retries" --limit 5
578
+ $ mg search memory --query "schema migration" --entity "memory-graph-mcp" --content-preview-chars 80
579
+ `
580
+ ).action(async (opts, cmd) => {
581
+ await runAndRender(
582
+ "search_memory",
583
+ {
584
+ query: opts.query,
585
+ limit: opts.limit,
586
+ include_stale: opts.includeStale,
587
+ entity: opts.entity,
588
+ exclude_entity_types: parseCsv(opts.excludeEntityTypes),
589
+ include_entity_types: parseCsv(opts.includeEntityTypes),
590
+ content_preview_chars: opts.contentPreviewChars,
591
+ include_fixtures: opts.includeFixtures
592
+ },
593
+ cmd.optsWithGlobals(),
594
+ (data, requestBody) => transformSearchResult("search_memory", requestBody, data)
595
+ );
596
+ });
597
+ search.command("codebase").description("Search codebase-scoped graph data").requiredOption("--query <text>", "search query").option("--repo <repo>", "repo filter").option("--branch <branch>", "branch filter").option("--entity-types <csv>", "comma-separated entity types").option(
598
+ "--exclude-entity-types <csv>",
599
+ "comma-separated entity types to exclude (empty/default excludes archives)"
600
+ ).option("--match-kinds <csv>", "comma-separated match kinds").option(
601
+ "--query-embedding <csv>",
602
+ "comma-separated embedding vector for optional hybrid search"
603
+ ).option("--limit <n>", "max results", parseInt).option("--include-stale", "include stale memories").option("--include-fixtures", "include fixture data").option("--content-preview-chars <n>", "preview length", parseInt).addHelpText(
604
+ "after",
605
+ `
606
+
607
+ Examples:
608
+ $ mg search codebase --query "search diagnostics" --repo memory-graph-mcp --limit 5
609
+ $ mg search codebase --query "services.py" --repo memory-graph-mcp --match-kinds entity_path,observation_fts
610
+ `
611
+ ).action(async (opts, cmd) => {
612
+ await runAndRender(
613
+ "search_codebase",
614
+ {
615
+ query: opts.query,
616
+ repo: opts.repo,
617
+ branch: opts.branch,
618
+ entity_types: parseCsv(opts.entityTypes),
619
+ exclude_entity_types: parseCsv(opts.excludeEntityTypes),
620
+ match_kinds: parseCsv(opts.matchKinds),
621
+ query_embedding: parseFloatCsv(opts.queryEmbedding),
622
+ limit: opts.limit,
623
+ include_stale: opts.includeStale,
624
+ include_fixtures: opts.includeFixtures,
625
+ content_preview_chars: opts.contentPreviewChars
626
+ },
627
+ cmd.optsWithGlobals(),
628
+ (data, requestBody) => transformSearchResult("search_codebase", requestBody, data)
629
+ );
630
+ });
631
+ search.command("entities").description("Search entity names, aliases, types, and observations").requiredOption("--query <text>", "search query").option("--limit <n>", "max results", parseInt).option(
632
+ "--exclude-entity-types <csv>",
633
+ "comma-separated entity types to exclude"
634
+ ).option(
635
+ "--include-entity-types <csv>",
636
+ "comma-separated entity types to include"
637
+ ).option("--include-fixtures", "include fixture data").addHelpText(
638
+ "after",
639
+ `
640
+
641
+ Examples:
642
+ $ mg search entities --query "Mike" --limit 5
643
+ $ mg search entities --query "memory graph" --include-entity-types Repo,Decision
644
+ `
645
+ ).action(async (opts, cmd) => {
646
+ await runAndRender(
647
+ "search_entities",
648
+ {
649
+ query: opts.query,
650
+ limit: opts.limit,
651
+ exclude_entity_types: parseCsv(opts.excludeEntityTypes),
652
+ include_entity_types: parseCsv(opts.includeEntityTypes),
653
+ include_fixtures: opts.includeFixtures
654
+ },
655
+ cmd.optsWithGlobals(),
656
+ (data, requestBody) => transformSearchResult("search_entities", requestBody, data)
657
+ );
658
+ });
659
+ }
660
+
661
+ // src/index.ts
662
+ async function serverVersion() {
663
+ try {
664
+ const health = await getJson("/healthz");
665
+ if (health && typeof health === "object" && "version" in health) {
666
+ const value = health.version;
667
+ return typeof value === "string" ? value : void 0;
668
+ }
669
+ } catch {
670
+ return void 0;
671
+ }
672
+ return void 0;
673
+ }
674
+ async function main() {
675
+ const program = new Command();
676
+ program.name("mg").description("Agent-friendly CLI for memory-graph-mcp").option("--format <format>", "output format: json or pretty").option("--url <url>", "override MEMORY_GRAPH_URL for this invocation").helpOption("-h, --help", "display help").version(package_default.version, "-V, --version", "print mg client version").showHelpAfterError();
677
+ program.hook("preAction", (thisCommand) => {
678
+ const opts = thisCommand.opts();
679
+ if (opts.url) {
680
+ process.env.MEMORY_GRAPH_URL = opts.url;
681
+ }
682
+ parseFormat(opts.format);
683
+ readConfig();
684
+ });
685
+ program.command("version").description("Print client and server versions").action(async (_opts, cmd) => {
686
+ const server = await serverVersion();
687
+ render({ client: package_default.version, server: server ?? null }, parseFormat(cmd.optsWithGlobals().format));
688
+ });
689
+ registerMemory(program);
690
+ registerSearch(program);
691
+ registerFind(program);
692
+ registerRelation(program);
693
+ registerEntity(program);
694
+ registerAdmin(program);
695
+ registerConfig(program);
696
+ await program.parseAsync(process.argv);
697
+ }
698
+ main().catch(handleError);
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Minimal checked-in OpenAPI type placeholder for the mg CLI.
3
+ * Regenerate against a running server with: npm run generate:types
4
+ */
5
+ export interface paths {
6
+ [path: string]: {
7
+ post?: {
8
+ requestBody?: { content: { "application/json": unknown } };
9
+ responses?: { [statusCode: string]: { content: { "application/json": unknown } } };
10
+ };
11
+ get?: {
12
+ responses?: { [statusCode: string]: { content: { "application/json": unknown } } };
13
+ };
14
+ };
15
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@mporenta/mg",
3
+ "version": "0.1.0",
4
+ "description": "Agent-friendly CLI for memory-graph-mcp",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "mg": "dist/mg.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "generated",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "scripts": {
19
+ "build": "esbuild src/index.ts --bundle --packages=external --platform=node --format=esm --target=node18 --outfile=dist/mg.js --banner:js='#!/usr/bin/env node' && chmod +x dist/mg.js",
20
+ "check": "tsc --noEmit",
21
+ "test": "npm run build && node --test test/*.test.mjs",
22
+ "pack:dry-run": "npm pack --dry-run",
23
+ "prepack": "npm run build",
24
+ "generate:types": "openapi-typescript ${MEMORY_GRAPH_OPENAPI_URL:-http://localhost:8000/openapi.json} -o generated/api.d.ts"
25
+ },
26
+ "dependencies": {
27
+ "commander": "^12.1.0",
28
+ "openapi-fetch": "^0.13.4"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.10.1",
32
+ "esbuild": "^0.24.0",
33
+ "openapi-typescript": "^7.4.4",
34
+ "typescript": "^5.7.2"
35
+ }
36
+ }