superlocalmemory 3.0.26 → 3.0.28

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 CHANGED
@@ -16,6 +16,8 @@
16
16
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="MIT License"/></a>
17
17
  <a href="#eu-ai-act-compliance"><img src="https://img.shields.io/badge/EU_AI_Act-Compliant-brightgreen?style=for-the-badge" alt="EU AI Act"/></a>
18
18
  <a href="https://superlocalmemory.com"><img src="https://img.shields.io/badge/Web-superlocalmemory.com-ff6b35?style=for-the-badge" alt="Website"/></a>
19
+ <a href="#dual-interface-mcp--cli"><img src="https://img.shields.io/badge/MCP-Native-blue?style=for-the-badge" alt="MCP Native"/></a>
20
+ <a href="#dual-interface-mcp--cli"><img src="https://img.shields.io/badge/CLI-Agent--Native-green?style=for-the-badge" alt="CLI Agent-Native"/></a>
19
21
  </p>
20
22
 
21
23
  ---
@@ -84,6 +86,32 @@ slm status
84
86
 
85
87
  24 MCP tools available. Works with Claude Code, Cursor, Windsurf, VS Code Copilot, Continue, Cody, ChatGPT Desktop, Gemini CLI, JetBrains, Zed, and 17+ AI tools.
86
88
 
89
+ ### Dual Interface: MCP + CLI
90
+
91
+ SLM works everywhere -- from IDEs to CI pipelines to Docker containers. The only AI memory system with both MCP and agent-native CLI.
92
+
93
+ | Need | Use | Example |
94
+ |------|-----|---------|
95
+ | IDE integration | MCP | Auto-configured for 17+ IDEs via `slm connect` |
96
+ | Shell scripts | CLI + `--json` | `slm recall "auth" --json \| jq '.data.results[0]'` |
97
+ | CI/CD pipelines | CLI + `--json` | `slm remember "deployed v2.1" --json` in GitHub Actions |
98
+ | Agent frameworks | CLI + `--json` | OpenClaw, Codex, Goose, nanobot |
99
+ | Human use | CLI | `slm recall "auth"` (readable text output) |
100
+
101
+ **Agent-native JSON output** on every command:
102
+
103
+ ```bash
104
+ # Human-readable (default)
105
+ slm recall "database schema"
106
+ # 1. [0.87] Database uses PostgreSQL 16 on port 5432...
107
+
108
+ # Agent-native JSON
109
+ slm recall "database schema" --json
110
+ # {"success": true, "command": "recall", "version": "3.0.22", "data": {"results": [...]}}
111
+ ```
112
+
113
+ All `--json` responses follow a consistent envelope with `success`, `command`, `version`, `data`, and `next_actions` for agent guidance.
114
+
87
115
  ---
88
116
 
89
117
  ## Three Operating Modes
@@ -237,9 +237,60 @@ These options work with any command:
237
237
  | `--help` | Show help for a command |
238
238
  | `--version` | Show SLM version |
239
239
  | `--verbose` | Show detailed output |
240
- | `--json` | Output in JSON format (for scripting) |
240
+ | `--json` | Output structured JSON with agent-native envelope (for AI agents, scripts, CI/CD) |
241
241
  | `--profile name` | Override the active profile for this command |
242
242
 
243
+ ## Agent-Native JSON Output
244
+
245
+ All data-returning commands support `--json` for structured output. The envelope follows the 2026 agent-native CLI standard:
246
+
247
+ ```json
248
+ {
249
+ "success": true,
250
+ "command": "recall",
251
+ "version": "3.0.22",
252
+ "data": {
253
+ "results": [
254
+ {"fact_id": "abc123", "score": 0.87, "content": "Database uses PostgreSQL 16"}
255
+ ],
256
+ "count": 1,
257
+ "query_type": "semantic"
258
+ },
259
+ "next_actions": [
260
+ {"command": "slm list --json", "description": "List recent memories"}
261
+ ]
262
+ }
263
+ ```
264
+
265
+ ### Supported Commands
266
+
267
+ `recall`, `remember`, `list`, `status`, `health`, `trace`, `forget`, `delete`, `update`, `mode`, `profile`, `connect`
268
+
269
+ ### Usage with jq
270
+
271
+ ```bash
272
+ # Get first result content
273
+ slm recall "auth" --json | jq '.data.results[0].content'
274
+
275
+ # Get all fact IDs
276
+ slm list --json | jq '.data.results[].fact_id'
277
+
278
+ # Check current mode
279
+ slm status --json | jq '.data.mode'
280
+ ```
281
+
282
+ ### In CI/CD (GitHub Actions)
283
+
284
+ ```yaml
285
+ - name: Store deployment info
286
+ run: slm remember "Deployed ${{ github.sha }} to production" --json
287
+
288
+ - name: Check memory health
289
+ run: slm status --json | jq -e '.success'
290
+ ```
291
+
292
+ ---
293
+
243
294
  ## Examples
244
295
 
245
296
  ### Daily workflow
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "superlocalmemory",
3
- "version": "3.0.26",
3
+ "version": "3.0.28",
4
4
  "description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
5
5
  "keywords": [
6
6
  "ai-memory",
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "superlocalmemory"
3
- version = "3.0.26"
3
+ version = "3.0.28"
4
4
  description = "Information-geometric agent memory with mathematical guarantees"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
@@ -5,6 +5,7 @@
5
5
  """CLI command implementations.
6
6
 
7
7
  Each function handles one CLI command. Dispatch routes by name.
8
+ All data-returning commands support --json for agent-native output.
8
9
 
9
10
  Part of Qualixar | Author: Varun Pratap Bhardwaj
10
11
  """
@@ -45,6 +46,9 @@ def dispatch(args: Namespace) -> None:
45
46
  sys.exit(1)
46
47
 
47
48
 
49
+ # -- Setup & Config (no --json — interactive commands) ---------------------
50
+
51
+
48
52
  def cmd_setup(_args: Namespace) -> None:
49
53
  """Run the interactive setup wizard."""
50
54
  from superlocalmemory.cli.setup_wizard import run_wizard
@@ -59,6 +63,31 @@ def cmd_mode(args: Namespace) -> None:
59
63
 
60
64
  config = SLMConfig.load()
61
65
 
66
+ if getattr(args, 'json', False):
67
+ from superlocalmemory.cli.json_output import json_print
68
+ if args.value:
69
+ old_mode = config.mode.value.upper()
70
+ updated = SLMConfig.for_mode(
71
+ Mode(args.value),
72
+ llm_provider=config.llm.provider,
73
+ llm_model=config.llm.model,
74
+ llm_api_key=config.llm.api_key,
75
+ llm_api_base=config.llm.api_base,
76
+ )
77
+ updated.save()
78
+ json_print("mode", data={
79
+ "previous_mode": old_mode, "current_mode": args.value.upper(),
80
+ }, next_actions=[
81
+ {"command": "slm status --json", "description": "Check system status"},
82
+ ])
83
+ else:
84
+ json_print("mode", data={"current_mode": config.mode.value.upper()},
85
+ next_actions=[
86
+ {"command": "slm mode a --json", "description": "Switch to zero-cloud mode"},
87
+ {"command": "slm mode c --json", "description": "Switch to full-power mode"},
88
+ ])
89
+ return
90
+
62
91
  if args.value:
63
92
  updated = SLMConfig.for_mode(
64
93
  Mode(args.value),
@@ -94,6 +123,24 @@ def cmd_connect(args: Namespace) -> None:
94
123
  from superlocalmemory.hooks.ide_connector import IDEConnector
95
124
 
96
125
  connector = IDEConnector()
126
+
127
+ if getattr(args, 'json', False):
128
+ from superlocalmemory.cli.json_output import json_print
129
+ if getattr(args, "list", False):
130
+ json_print("connect", data={"ides": connector.get_status()},
131
+ next_actions=[
132
+ {"command": "slm connect --json", "description": "Auto-configure all IDEs"},
133
+ ])
134
+ elif getattr(args, "ide", None):
135
+ success = connector.connect(args.ide)
136
+ json_print("connect", data={"ide": args.ide, "connected": success})
137
+ else:
138
+ json_print("connect", data={"results": connector.connect_all()},
139
+ next_actions=[
140
+ {"command": "slm status --json", "description": "Check system status"},
141
+ ])
142
+ return
143
+
97
144
  if getattr(args, "list", False):
98
145
  status = connector.get_status()
99
146
  for s in status:
@@ -116,20 +163,47 @@ def cmd_migrate(args: Namespace) -> None:
116
163
  _migrate(args)
117
164
 
118
165
 
166
+ # -- Memory Operations (all support --json) --------------------------------
167
+
168
+
119
169
  def cmd_list(args: Namespace) -> None:
120
170
  """List recent memories chronologically."""
121
171
  from superlocalmemory.core.config import SLMConfig
122
172
  from superlocalmemory.core.engine import MemoryEngine
123
173
 
124
- config = SLMConfig.load()
125
- engine = MemoryEngine(config)
126
- engine.initialize()
127
-
128
- limit = getattr(args, "limit", 20)
129
- facts = engine._db.get_all_facts(engine.profile_id)
130
- # Sort by created_at descending, take limit
131
- facts.sort(key=lambda f: f.created_at or "", reverse=True)
132
- facts = facts[:limit]
174
+ use_json = getattr(args, 'json', False)
175
+ try:
176
+ config = SLMConfig.load()
177
+ engine = MemoryEngine(config)
178
+ engine.initialize()
179
+
180
+ limit = getattr(args, "limit", 20)
181
+ facts = engine._db.get_all_facts(engine.profile_id)
182
+ facts.sort(key=lambda f: f.created_at or "", reverse=True)
183
+ facts = facts[:limit]
184
+ except Exception as exc:
185
+ if use_json:
186
+ from superlocalmemory.cli.json_output import json_print
187
+ json_print("list", error={"code": "ENGINE_ERROR", "message": str(exc)})
188
+ sys.exit(1)
189
+ raise
190
+
191
+ if use_json:
192
+ from superlocalmemory.cli.json_output import json_print
193
+ items = []
194
+ for f in facts:
195
+ ftype_raw = getattr(f, "fact_type", "")
196
+ ftype = ftype_raw.value if hasattr(ftype_raw, "value") else str(ftype_raw)
197
+ items.append({
198
+ "fact_id": f.fact_id, "content": f.content,
199
+ "fact_type": ftype, "created_at": (f.created_at or "")[:19],
200
+ })
201
+ json_print("list", data={"results": items, "count": len(items)},
202
+ next_actions=[
203
+ {"command": "slm recall '<query>' --json", "description": "Search memories"},
204
+ {"command": "slm delete <fact_id> --json --yes", "description": "Delete a memory"},
205
+ ])
206
+ return
133
207
 
134
208
  if not facts:
135
209
  print("No memories stored yet.")
@@ -149,12 +223,30 @@ def cmd_remember(args: Namespace) -> None:
149
223
  from superlocalmemory.core.config import SLMConfig
150
224
  from superlocalmemory.core.engine import MemoryEngine
151
225
 
152
- config = SLMConfig.load()
153
- engine = MemoryEngine(config)
154
- engine.initialize()
226
+ use_json = getattr(args, 'json', False)
227
+ try:
228
+ config = SLMConfig.load()
229
+ engine = MemoryEngine(config)
230
+ engine.initialize()
231
+
232
+ metadata = {"tags": args.tags} if args.tags else {}
233
+ fact_ids = engine.store(args.content, metadata=metadata)
234
+ except Exception as exc:
235
+ if use_json:
236
+ from superlocalmemory.cli.json_output import json_print
237
+ json_print("remember", error={"code": "STORE_ERROR", "message": str(exc)})
238
+ sys.exit(1)
239
+ raise
240
+
241
+ if use_json:
242
+ from superlocalmemory.cli.json_output import json_print
243
+ json_print("remember", data={"fact_ids": fact_ids, "count": len(fact_ids)},
244
+ next_actions=[
245
+ {"command": "slm recall '<query>' --json", "description": "Search your memories"},
246
+ {"command": "slm list --json -n 5", "description": "See recent memories"},
247
+ ])
248
+ return
155
249
 
156
- metadata = {"tags": args.tags} if args.tags else {}
157
- fact_ids = engine.store(args.content, metadata=metadata)
158
250
  print(f"Stored {len(fact_ids)} facts.")
159
251
 
160
252
 
@@ -163,11 +255,39 @@ def cmd_recall(args: Namespace) -> None:
163
255
  from superlocalmemory.core.config import SLMConfig
164
256
  from superlocalmemory.core.engine import MemoryEngine
165
257
 
166
- config = SLMConfig.load()
167
- engine = MemoryEngine(config)
168
- engine.initialize()
258
+ use_json = getattr(args, 'json', False)
259
+ try:
260
+ config = SLMConfig.load()
261
+ engine = MemoryEngine(config)
262
+ engine.initialize()
263
+
264
+ response = engine.recall(args.query, limit=args.limit)
265
+ except Exception as exc:
266
+ if use_json:
267
+ from superlocalmemory.cli.json_output import json_print
268
+ json_print("recall", error={"code": "RECALL_ERROR", "message": str(exc)})
269
+ sys.exit(1)
270
+ raise
271
+
272
+ if use_json:
273
+ from superlocalmemory.cli.json_output import json_print
274
+ items = []
275
+ for r in response.results:
276
+ item = {
277
+ "fact_id": r.fact.fact_id, "content": r.fact.content,
278
+ "score": round(r.score, 3),
279
+ }
280
+ if hasattr(r, "channel_scores") and r.channel_scores:
281
+ item["channel_scores"] = {k: round(v, 3) for k, v in r.channel_scores.items()}
282
+ items.append(item)
283
+ json_print("recall", data={
284
+ "results": items, "count": len(items),
285
+ "query_type": getattr(response, "query_type", "unknown"),
286
+ }, next_actions=[
287
+ {"command": "slm list --json", "description": "List recent memories"},
288
+ ])
289
+ return
169
290
 
170
- response = engine.recall(args.query, limit=args.limit)
171
291
  if not response.results:
172
292
  print("No memories found.")
173
293
  return
@@ -180,18 +300,57 @@ def cmd_forget(args: Namespace) -> None:
180
300
  from superlocalmemory.core.engine import MemoryEngine
181
301
  from superlocalmemory.core.config import SLMConfig
182
302
 
183
- config = SLMConfig.load()
184
- engine = MemoryEngine(config)
185
- engine.initialize()
186
- facts = engine._db.get_all_facts(engine.profile_id)
187
- query_lower = args.query.lower()
188
- matches = [f for f in facts if query_lower in f.content.lower()]
303
+ use_json = getattr(args, 'json', False)
304
+ try:
305
+ config = SLMConfig.load()
306
+ engine = MemoryEngine(config)
307
+ engine.initialize()
308
+ facts = engine._db.get_all_facts(engine.profile_id)
309
+ query_lower = args.query.lower()
310
+ matches = [f for f in facts if query_lower in f.content.lower()]
311
+ except Exception as exc:
312
+ if use_json:
313
+ from superlocalmemory.cli.json_output import json_print
314
+ json_print("forget", error={"code": "ENGINE_ERROR", "message": str(exc)})
315
+ sys.exit(1)
316
+ raise
317
+
318
+ if use_json:
319
+ from superlocalmemory.cli.json_output import json_print
320
+ if not matches:
321
+ json_print("forget", data={"matched_count": 0, "deleted_count": 0, "matches": []})
322
+ return
323
+ match_items = [{"fact_id": f.fact_id, "content": f.content[:120]} for f in matches[:20]]
324
+ if getattr(args, 'yes', False):
325
+ for f in matches:
326
+ engine._db.delete_fact(f.fact_id)
327
+ json_print("forget", data={
328
+ "matched_count": len(matches), "deleted_count": len(matches),
329
+ "deleted": [f.fact_id for f in matches],
330
+ }, next_actions=[
331
+ {"command": "slm list --json", "description": "Verify remaining memories"},
332
+ ])
333
+ else:
334
+ json_print("forget", data={
335
+ "matched_count": len(matches), "deleted_count": 0,
336
+ "matches": match_items,
337
+ "hint": "Add --yes to confirm deletion",
338
+ }, next_actions=[
339
+ {"command": f"slm forget '{args.query}' --json --yes", "description": "Confirm deletion"},
340
+ ])
341
+ return
342
+
189
343
  if not matches:
190
344
  print(f"No memories matching '{args.query}'")
191
345
  return
192
346
  print(f"Found {len(matches)} matching memories:")
193
347
  for f in matches[:10]:
194
348
  print(f" - {f.fact_id[:8]}... {f.content[:80]}")
349
+ if getattr(args, 'yes', False):
350
+ for f in matches:
351
+ engine._db.delete_fact(f.fact_id)
352
+ print(f"Deleted {len(matches)} memories.")
353
+ return
195
354
  confirm = input(f"Delete {len(matches)} memories? [y/N] ").strip().lower()
196
355
  if confirm in ("y", "yes"):
197
356
  for f in matches:
@@ -206,16 +365,47 @@ def cmd_delete(args: Namespace) -> None:
206
365
  from superlocalmemory.core.config import SLMConfig
207
366
  from superlocalmemory.core.engine import MemoryEngine
208
367
 
209
- config = SLMConfig.load()
210
- engine = MemoryEngine(config)
211
- engine.initialize()
368
+ use_json = getattr(args, 'json', False)
369
+ try:
370
+ config = SLMConfig.load()
371
+ engine = MemoryEngine(config)
372
+ engine.initialize()
373
+
374
+ fact_id = args.fact_id.strip()
375
+ rows = engine._db.execute(
376
+ "SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
377
+ (fact_id, engine.profile_id),
378
+ )
379
+ except Exception as exc:
380
+ if use_json:
381
+ from superlocalmemory.cli.json_output import json_print
382
+ json_print("delete", error={"code": "ENGINE_ERROR", "message": str(exc)})
383
+ sys.exit(1)
384
+ raise
385
+
386
+ if use_json:
387
+ from superlocalmemory.cli.json_output import json_print
388
+ if not rows:
389
+ json_print("delete", error={
390
+ "code": "NOT_FOUND", "message": f"Memory not found: {fact_id}",
391
+ })
392
+ sys.exit(1)
393
+ content = dict(rows[0]).get("content", "")
394
+ if getattr(args, "yes", False):
395
+ engine._db.delete_fact(fact_id)
396
+ json_print("delete", data={"deleted": fact_id, "content": content[:120]},
397
+ next_actions=[
398
+ {"command": "slm list --json", "description": "Verify remaining memories"},
399
+ ])
400
+ else:
401
+ json_print("delete", data={
402
+ "fact_id": fact_id, "content": content[:120], "deleted": False,
403
+ "hint": "Add --yes to confirm deletion",
404
+ }, next_actions=[
405
+ {"command": f"slm delete {fact_id} --json --yes", "description": "Confirm deletion"},
406
+ ])
407
+ return
212
408
 
213
- fact_id = args.fact_id.strip()
214
- # Look up the memory first so user can confirm
215
- rows = engine._db.execute(
216
- "SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
217
- (fact_id, engine.profile_id),
218
- )
219
409
  if not rows:
220
410
  print(f"Memory not found: {fact_id}")
221
411
  return
@@ -238,40 +428,91 @@ def cmd_update(args: Namespace) -> None:
238
428
  from superlocalmemory.core.config import SLMConfig
239
429
  from superlocalmemory.core.engine import MemoryEngine
240
430
 
241
- config = SLMConfig.load()
242
- engine = MemoryEngine(config)
243
- engine.initialize()
244
-
431
+ use_json = getattr(args, 'json', False)
245
432
  fact_id = args.fact_id.strip()
246
433
  new_content = args.content.strip()
434
+
247
435
  if not new_content:
436
+ if use_json:
437
+ from superlocalmemory.cli.json_output import json_print
438
+ json_print("update", error={"code": "INVALID_INPUT", "message": "content cannot be empty"})
439
+ sys.exit(1)
248
440
  print("Error: content cannot be empty")
249
441
  return
250
442
 
251
- rows = engine._db.execute(
252
- "SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
253
- (fact_id, engine.profile_id),
254
- )
443
+ try:
444
+ config = SLMConfig.load()
445
+ engine = MemoryEngine(config)
446
+ engine.initialize()
447
+
448
+ rows = engine._db.execute(
449
+ "SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
450
+ (fact_id, engine.profile_id),
451
+ )
452
+ except Exception as exc:
453
+ if use_json:
454
+ from superlocalmemory.cli.json_output import json_print
455
+ json_print("update", error={"code": "ENGINE_ERROR", "message": str(exc)})
456
+ sys.exit(1)
457
+ raise
458
+
255
459
  if not rows:
460
+ if use_json:
461
+ from superlocalmemory.cli.json_output import json_print
462
+ json_print("update", error={
463
+ "code": "NOT_FOUND", "message": f"Memory not found: {fact_id}",
464
+ })
465
+ sys.exit(1)
256
466
  print(f"Memory not found: {fact_id}")
257
467
  return
258
468
 
259
469
  old_content = dict(rows[0]).get("content", "")
260
- print(f"Old: {old_content[:100]}")
261
- print(f"New: {new_content[:100]}")
262
-
263
470
  engine._db.execute(
264
471
  "UPDATE atomic_facts SET content = ? WHERE fact_id = ?",
265
472
  (new_content, fact_id),
266
473
  )
474
+
475
+ if use_json:
476
+ from superlocalmemory.cli.json_output import json_print
477
+ json_print("update", data={
478
+ "fact_id": fact_id,
479
+ "old_content": old_content[:120],
480
+ "new_content": new_content[:120],
481
+ }, next_actions=[
482
+ {"command": "slm list --json", "description": "List recent memories"},
483
+ ])
484
+ return
485
+
486
+ print(f"Old: {old_content[:100]}")
487
+ print(f"New: {new_content[:100]}")
267
488
  print(f"Updated: {fact_id}")
268
489
 
269
490
 
270
- def cmd_status(_args: Namespace) -> None:
491
+ # -- Diagnostics (all support --json) -------------------------------------
492
+
493
+
494
+ def cmd_status(args: Namespace) -> None:
271
495
  """Show system status."""
272
496
  from superlocalmemory.core.config import SLMConfig
273
497
 
274
498
  config = SLMConfig.load()
499
+
500
+ if getattr(args, 'json', False):
501
+ from superlocalmemory.cli.json_output import json_print
502
+ data = {
503
+ "mode": config.mode.value.upper(),
504
+ "provider": config.llm.provider or "none",
505
+ "base_dir": str(config.base_dir),
506
+ "db_path": str(config.db_path),
507
+ }
508
+ if config.db_path.exists():
509
+ data["db_size_mb"] = round(config.db_path.stat().st_size / 1024 / 1024, 2)
510
+ json_print("status", data=data, next_actions=[
511
+ {"command": "slm health --json", "description": "Check math layer health"},
512
+ {"command": "slm list --json", "description": "List recent memories"},
513
+ ])
514
+ return
515
+
275
516
  print("SuperLocalMemory V3")
276
517
  print(f" Mode: {config.mode.value.upper()}")
277
518
  print(f" Provider: {config.llm.provider or 'none'}")
@@ -282,17 +523,39 @@ def cmd_status(_args: Namespace) -> None:
282
523
  print(f" DB size: {size_mb} MB")
283
524
 
284
525
 
285
- def cmd_health(_args: Namespace) -> None:
526
+ def cmd_health(args: Namespace) -> None:
286
527
  """Show math layer health status."""
287
528
  from superlocalmemory.core.engine import MemoryEngine
288
529
  from superlocalmemory.core.config import SLMConfig
289
530
 
290
- config = SLMConfig.load()
291
- engine = MemoryEngine(config)
292
- engine.initialize()
293
- facts = engine._db.get_all_facts(engine.profile_id)
294
- fisher_count = sum(1 for f in facts if f.fisher_mean is not None)
295
- langevin_count = sum(1 for f in facts if f.langevin_position is not None)
531
+ use_json = getattr(args, 'json', False)
532
+ try:
533
+ config = SLMConfig.load()
534
+ engine = MemoryEngine(config)
535
+ engine.initialize()
536
+ facts = engine._db.get_all_facts(engine.profile_id)
537
+ fisher_count = sum(1 for f in facts if f.fisher_mean is not None)
538
+ langevin_count = sum(1 for f in facts if f.langevin_position is not None)
539
+ except Exception as exc:
540
+ if use_json:
541
+ from superlocalmemory.cli.json_output import json_print
542
+ json_print("health", error={"code": "ENGINE_ERROR", "message": str(exc)})
543
+ sys.exit(1)
544
+ raise
545
+
546
+ if use_json:
547
+ from superlocalmemory.cli.json_output import json_print
548
+ json_print("health", data={
549
+ "total_facts": len(facts),
550
+ "similarity_indexed": fisher_count,
551
+ "lifecycle_positioned": langevin_count,
552
+ "mode": config.mode.value.upper(),
553
+ }, next_actions=[
554
+ {"command": "slm status --json", "description": "Check system status"},
555
+ {"command": "slm recall '<query>' --json", "description": "Test retrieval"},
556
+ ])
557
+ return
558
+
296
559
  print("Math Layer Health:")
297
560
  print(f" Total facts: {len(facts)}")
298
561
  print(f" Fisher-Rao indexed: {fisher_count}/{len(facts)}")
@@ -305,9 +568,41 @@ def cmd_trace(args: Namespace) -> None:
305
568
  from superlocalmemory.core.engine import MemoryEngine
306
569
  from superlocalmemory.core.config import SLMConfig
307
570
 
308
- config = SLMConfig.load()
309
- engine = MemoryEngine(config)
310
- response = engine.recall(args.query, limit=5)
571
+ use_json = getattr(args, 'json', False)
572
+ try:
573
+ config = SLMConfig.load()
574
+ engine = MemoryEngine(config)
575
+ response = engine.recall(args.query, limit=5)
576
+ except Exception as exc:
577
+ if use_json:
578
+ from superlocalmemory.cli.json_output import json_print
579
+ json_print("trace", error={"code": "ENGINE_ERROR", "message": str(exc)})
580
+ sys.exit(1)
581
+ raise
582
+
583
+ if use_json:
584
+ from superlocalmemory.cli.json_output import json_print
585
+ items = []
586
+ for r in response.results:
587
+ item = {
588
+ "fact_id": r.fact.fact_id, "content": r.fact.content[:200],
589
+ "score": round(r.score, 3),
590
+ }
591
+ if hasattr(r, "channel_scores") and r.channel_scores:
592
+ item["channel_scores"] = {
593
+ k: round(v, 3) for k, v in r.channel_scores.items()
594
+ }
595
+ items.append(item)
596
+ json_print("trace", data={
597
+ "query": args.query,
598
+ "query_type": getattr(response, "query_type", "unknown"),
599
+ "retrieval_time_ms": round(getattr(response, "retrieval_time_ms", 0), 1),
600
+ "results": items, "count": len(items),
601
+ }, next_actions=[
602
+ {"command": "slm recall '<query>' --json", "description": "Standard recall"},
603
+ ])
604
+ return
605
+
311
606
  print(f"Query: {args.query}")
312
607
  print(f"Type: {response.query_type} | Time: {response.retrieval_time_ms:.0f}ms")
313
608
  print(f"Results: {len(response.results)}")
@@ -318,6 +613,9 @@ def cmd_trace(args: Namespace) -> None:
318
613
  print(f" {ch}: {sc:.3f}")
319
614
 
320
615
 
616
+ # -- Services (no --json — these start long-running processes) -------------
617
+
618
+
321
619
  def cmd_mcp(_args: Namespace) -> None:
322
620
  """Start the V3 MCP server (stdio transport for IDE integration)."""
323
621
  from superlocalmemory.mcp.server import server
@@ -397,6 +695,9 @@ def cmd_dashboard(args: Namespace) -> None:
397
695
  uvicorn.run(app, host="127.0.0.1", port=ui_port, log_level="info")
398
696
 
399
697
 
698
+ # -- Profiles (supports --json) -------------------------------------------
699
+
700
+
400
701
  def cmd_profile(args: Namespace) -> None:
401
702
  """Profile management (list, switch, create)."""
402
703
  from superlocalmemory.core.config import SLMConfig
@@ -407,6 +708,34 @@ def cmd_profile(args: Namespace) -> None:
407
708
  db = DatabaseManager(config.db_path)
408
709
  db.initialize(schema)
409
710
 
711
+ if getattr(args, 'json', False):
712
+ from superlocalmemory.cli.json_output import json_print
713
+ if args.action == "list":
714
+ rows = db.execute("SELECT profile_id, name FROM profiles")
715
+ profiles = [
716
+ {"profile_id": dict(r)["profile_id"], "name": dict(r).get("name", "")}
717
+ for r in rows
718
+ ]
719
+ json_print("profile", data={"profiles": profiles, "count": len(profiles)},
720
+ next_actions=[
721
+ {"command": "slm profile switch <name> --json", "description": "Switch profile"},
722
+ ])
723
+ elif args.action == "switch":
724
+ config.active_profile = args.name
725
+ config.save()
726
+ json_print("profile", data={"action": "switched", "profile": args.name})
727
+ elif args.action == "create":
728
+ db.execute(
729
+ "INSERT OR IGNORE INTO profiles (profile_id, name) VALUES (?, ?)",
730
+ (args.name, args.name),
731
+ )
732
+ json_print("profile", data={"action": "created", "profile": args.name},
733
+ next_actions=[
734
+ {"command": f"slm profile switch {args.name} --json",
735
+ "description": "Switch to new profile"},
736
+ ])
737
+ return
738
+
410
739
  if args.action == "list":
411
740
  rows = db.execute("SELECT profile_id, name FROM profiles")
412
741
  print("Profiles:")
@@ -0,0 +1,81 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under the MIT License - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Shared JSON envelope for agent-native CLI output.
6
+
7
+ Follows the 2026 agent-native CLI standard:
8
+ - Consistent envelope: success, command, version, data/error
9
+ - HATEOAS next_actions for agent guidance
10
+ - Metadata for execution context
11
+
12
+ Part of Qualixar | Author: Varun Pratap Bhardwaj
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+
19
+
20
+ def _get_version() -> str:
21
+ """Read version from package.json (npm), pyproject.toml, or metadata."""
22
+ from pathlib import Path
23
+ pkg_root = Path(__file__).resolve().parent.parent.parent.parent
24
+ # 1. package.json (npm installs)
25
+ try:
26
+ pkg_json = pkg_root / "package.json"
27
+ if pkg_json.exists():
28
+ with open(pkg_json) as f:
29
+ v = json.load(f).get("version", "")
30
+ if v:
31
+ return v
32
+ except Exception:
33
+ pass
34
+ # 2. pyproject.toml (pip installs)
35
+ try:
36
+ import tomllib
37
+ toml_path = pkg_root / "pyproject.toml"
38
+ if toml_path.exists():
39
+ with open(toml_path, "rb") as f:
40
+ return tomllib.load(f)["project"]["version"]
41
+ except Exception:
42
+ pass
43
+ # 3. importlib.metadata fallback
44
+ try:
45
+ from importlib.metadata import version
46
+ return version("superlocalmemory")
47
+ except Exception:
48
+ pass
49
+ return "unknown"
50
+
51
+
52
+ def json_print(
53
+ command: str,
54
+ *,
55
+ data: dict | None = None,
56
+ error: dict | None = None,
57
+ next_actions: list[dict] | None = None,
58
+ metadata: dict | None = None,
59
+ ) -> None:
60
+ """Print a standard JSON envelope to stdout.
61
+
62
+ Success envelope:
63
+ {"success": true, "command": "...", "version": "...", "data": {...}}
64
+
65
+ Error envelope:
66
+ {"success": false, "command": "...", "version": "...", "error": {...}}
67
+ """
68
+ envelope: dict = {
69
+ "success": error is None,
70
+ "command": command,
71
+ "version": _get_version(),
72
+ }
73
+ if error is not None:
74
+ envelope["error"] = error
75
+ else:
76
+ envelope["data"] = data if data is not None else {}
77
+ if metadata:
78
+ envelope["metadata"] = metadata
79
+ if next_actions:
80
+ envelope["next_actions"] = next_actions
81
+ print(json.dumps(envelope, indent=2, default=str))
@@ -59,6 +59,7 @@ examples:
59
59
  slm trace "auth flow" Recall with per-channel score breakdown
60
60
  slm health Check math layer status
61
61
  slm dashboard --port 9000 Dashboard on custom port
62
+ slm recall "query" --json Agent-native JSON output (for scripts, CI/CD)
62
63
 
63
64
  documentation:
64
65
  Website: https://superlocalmemory.com
@@ -69,11 +70,8 @@ documentation:
69
70
 
70
71
  def main() -> None:
71
72
  """Parse CLI arguments and dispatch to command handlers."""
72
- try:
73
- from importlib.metadata import version as _pkg_version
74
- _ver = _pkg_version("superlocalmemory")
75
- except Exception:
76
- _ver = "unknown"
73
+ from superlocalmemory.cli.json_output import _get_version
74
+ _ver = _get_version()
77
75
 
78
76
  parser = argparse.ArgumentParser(
79
77
  prog="slm",
@@ -93,6 +91,7 @@ def main() -> None:
93
91
  mode_p.add_argument(
94
92
  "value", nargs="?", choices=["a", "b", "c"], help="Mode to set",
95
93
  )
94
+ mode_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
96
95
 
97
96
  provider_p = sub.add_parser("provider", help="Get or set LLM provider for Mode B/C")
98
97
  provider_p.add_argument(
@@ -104,6 +103,7 @@ def main() -> None:
104
103
  connect_p.add_argument(
105
104
  "--list", action="store_true", help="List all supported IDEs",
106
105
  )
106
+ connect_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
107
107
 
108
108
  migrate_p = sub.add_parser("migrate", help="Migrate data from V2 to V3 schema")
109
109
  migrate_p.add_argument(
@@ -114,33 +114,44 @@ def main() -> None:
114
114
  remember_p = sub.add_parser("remember", help="Store a memory (extracts facts, builds graph)")
115
115
  remember_p.add_argument("content", help="Content to remember")
116
116
  remember_p.add_argument("--tags", default="", help="Comma-separated tags")
117
+ remember_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
117
118
 
118
119
  recall_p = sub.add_parser("recall", help="Semantic search with 4-channel retrieval")
119
120
  recall_p.add_argument("query", help="Search query")
120
121
  recall_p.add_argument("--limit", type=int, default=10, help="Max results (default 10)")
122
+ recall_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
121
123
 
122
124
  forget_p = sub.add_parser("forget", help="Delete memories matching a query (fuzzy)")
123
125
  forget_p.add_argument("query", help="Query to match for deletion")
126
+ forget_p.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompt")
127
+ forget_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
124
128
 
125
129
  delete_p = sub.add_parser("delete", help="Delete a specific memory by ID (precise)")
126
130
  delete_p.add_argument("fact_id", help="Exact fact ID to delete")
127
131
  delete_p.add_argument("--yes", "-y", action="store_true", help="Skip confirmation")
132
+ delete_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
128
133
 
129
134
  update_p = sub.add_parser("update", help="Edit the content of a specific memory by ID")
130
135
  update_p.add_argument("fact_id", help="Exact fact ID to update")
131
136
  update_p.add_argument("content", help="New content for the memory")
137
+ update_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
132
138
 
133
139
  list_p = sub.add_parser("list", help="List recent memories chronologically (shows IDs for delete/update)")
134
140
  list_p.add_argument(
135
141
  "--limit", "-n", type=int, default=20, help="Number of entries (default 20)",
136
142
  )
143
+ list_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
137
144
 
138
145
  # -- Diagnostics ---------------------------------------------------
139
- sub.add_parser("status", help="System status (mode, profile, DB size)")
140
- sub.add_parser("health", help="Math layer health (Fisher-Rao, Sheaf, Langevin)")
146
+ status_p = sub.add_parser("status", help="System status (mode, profile, DB size)")
147
+ status_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
148
+
149
+ health_p = sub.add_parser("health", help="Math layer health (Fisher-Rao, Sheaf, Langevin)")
150
+ health_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
141
151
 
142
152
  trace_p = sub.add_parser("trace", help="Recall with per-channel score breakdown")
143
153
  trace_p.add_argument("query", help="Search query")
154
+ trace_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
144
155
 
145
156
  # -- Services ------------------------------------------------------
146
157
  sub.add_parser("mcp", help="Start MCP server (stdio transport for IDE integration)")
@@ -157,6 +168,7 @@ def main() -> None:
157
168
  "action", choices=["list", "switch", "create"], help="Action",
158
169
  )
159
170
  profile_p.add_argument("name", nargs="?", help="Profile name")
171
+ profile_p.add_argument("--json", action="store_true", help="Output structured JSON (agent-native)")
160
172
 
161
173
  args = parser.parse_args()
162
174