sibyl-memory-cli 0.3.6__tar.gz → 0.3.7__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sibyl-memory-cli
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: Command-line interface for the Sibyl Memory Plugin. `sibyl init` activates, `sibyl upgrade` runs the staker / subscription flow, `sibyl status` shows current tier and DB stats, `sibyl whoami` gives a one-line account summary, `sibyl devices` lists active devices and supports per-device revoke.
5
5
  Author-email: "SIBYL, Sibyl Labs LLC" <sibyl@sibyllabs.org>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sibyl-memory-cli"
7
- version = "0.3.6"
7
+ version = "0.3.7"
8
8
  description = "Command-line interface for the Sibyl Memory Plugin. `sibyl init` activates, `sibyl upgrade` runs the staker / subscription flow, `sibyl status` shows current tier and DB stats, `sibyl whoami` gives a one-line account summary, `sibyl devices` lists active devices and supports per-device revoke."
9
9
  authors = [{ name = "SIBYL, Sibyl Labs LLC", email = "sibyl@sibyllabs.org" }]
10
10
  license = { text = "MIT" }
@@ -1042,7 +1042,7 @@ def build_parser() -> argparse.ArgumentParser:
1042
1042
  help="Auto-detect Hermes / Claude Code and wire SIBYL as the memory provider",
1043
1043
  )
1044
1044
  p_setup.add_argument(
1045
- "target", nargs="?", choices=list(["hermes", "claude-code"]),
1045
+ "target", nargs="?", choices=list(["hermes", "claude-code", "codex"]),
1046
1046
  help="Wire just this framework (default: detect all)",
1047
1047
  )
1048
1048
  p_setup.add_argument(
@@ -1063,7 +1063,11 @@ def build_parser() -> argparse.ArgumentParser:
1063
1063
  )
1064
1064
  p_setup.add_argument(
1065
1065
  "--claude-settings", default=None,
1066
- help="Override ~/.claude/settings.json autodetection",
1066
+ help="Override ~/.claude.json autodetection",
1067
+ )
1068
+ p_setup.add_argument(
1069
+ "--codex-config", default=None,
1070
+ help="Override ~/.codex/config.toml autodetection",
1067
1071
  )
1068
1072
  p_setup.set_defaults(func=cmd_setup)
1069
1073
 
@@ -3,11 +3,12 @@
3
3
  Maximum-efficiency onboarding command. Single-command path for the user:
4
4
 
5
5
  pip install sibyl-memory-cli
6
- sibyl setup # auto-detects Hermes + Claude Code, prompts per stack, wires
6
+ sibyl setup # auto-detects Hermes + Claude Code + Codex, prompts per stack, wires
7
7
 
8
- Two wirers in v0.1.4:
8
+ Three wirers in v0.3.7:
9
9
  - HermesWirer: install-plugin + edit $HERMES_HOME/config.yaml (memory.provider)
10
- - ClaudeCodeWirer: edit ~/.claude/settings.json (mcpServers.sibyl-memory)
10
+ - ClaudeCodeWirer: edit ~/.claude.json (mcpServers.sibyl-memory)
11
+ - CodexWirer: edit ~/.codex/config.toml ([mcp_servers.sibyl_memory])
11
12
 
12
13
  Each wirer follows the same protocol:
13
14
  is_present() -> bool (filesystem + PATH detect)
@@ -26,6 +27,7 @@ from __future__ import annotations
26
27
  import argparse
27
28
  import json
28
29
  import os
30
+ import re
29
31
  import shutil
30
32
  import sys
31
33
  from dataclasses import dataclass
@@ -229,9 +231,15 @@ class ClaudeCodeWirer:
229
231
  SIBYL_MCP_BLOCK = {"command": "sibyl-memory-mcp"}
230
232
 
231
233
  def __init__(self, *, settings_path: Optional[Union[str, Path]] = None):
234
+ # v0.3.7 (2026-05-22): default flipped from ~/.claude/settings.json to
235
+ # ~/.claude.json. Recent Claude Code reads MCP server config from the
236
+ # user-level ~/.claude.json — writing to ~/.claude/settings.json was a
237
+ # silent-failure path (wirer reported success but MCP server didn't
238
+ # appear in Claude Code). Override with --claude-settings if your
239
+ # install uses a non-standard location.
232
240
  self.settings_path = (
233
241
  Path(settings_path).expanduser() if settings_path
234
- else Path.home() / ".claude" / "settings.json"
242
+ else Path.home() / ".claude.json"
235
243
  )
236
244
 
237
245
  def is_present(self) -> bool:
@@ -333,6 +341,158 @@ class ClaudeCodeWirer:
333
341
  os.replace(tmp, self.settings_path)
334
342
 
335
343
 
344
+ # ----------------------------------------------------------------------
345
+ # CodexWirer — edits ~/.codex/config.toml
346
+ # ----------------------------------------------------------------------
347
+
348
+ class CodexWirer:
349
+ """Auto-wire Codex CLI's MCP server registry at ~/.codex/config.toml.
350
+
351
+ Codex reads server defs from a TOML table named `[mcp_servers.<name>]`.
352
+ We use `[mcp_servers.sibyl_memory]` (snake_case is TOML-idiomatic; the
353
+ Claude Code side uses `sibyl-memory` because JSON keys allow hyphens
354
+ and Anthropic docs use that form).
355
+
356
+ Idempotency is text-based rather than full TOML round-trip, so we
357
+ preserve operator hand-edits and comments in the rest of the file
358
+ instead of reformatting on every run. The cost: we won't catch
359
+ pathological TOML edge cases (e.g., comments mid-key). For the
360
+ canonical Codex config layout this is fine.
361
+ """
362
+ name = "codex"
363
+ display_name = "Codex CLI"
364
+ initial = "x" # 'c' is taken by Claude Code; 'x' from codeX
365
+
366
+ SIBYL_TOML_BLOCK = '[mcp_servers.sibyl_memory]\ncommand = "sibyl-memory-mcp"\n'
367
+ BLOCK_HEADER_RE = re.compile(
368
+ r'^\s*\[\s*mcp_servers\s*\.\s*sibyl_memory\s*\]\s*$', re.MULTILINE
369
+ )
370
+ NEXT_TABLE_RE = re.compile(r'^\s*\[', re.MULTILINE)
371
+ SIBYL_COMMAND_RE = re.compile(
372
+ r'^\s*command\s*=\s*"sibyl-memory-mcp"\s*$', re.MULTILINE
373
+ )
374
+
375
+ def __init__(self, *, config_path: Optional[Union[str, Path]] = None):
376
+ self.config_path = (
377
+ Path(config_path).expanduser() if config_path
378
+ else Path.home() / ".codex" / "config.toml"
379
+ )
380
+
381
+ def is_present(self) -> bool:
382
+ if self.config_path.exists():
383
+ return True
384
+ if shutil.which("codex"):
385
+ return True
386
+ return False
387
+
388
+ def _read(self) -> str:
389
+ if not self.config_path.exists():
390
+ return ""
391
+ try:
392
+ return self.config_path.read_text(encoding="utf-8")
393
+ except Exception:
394
+ return ""
395
+
396
+ def _extract_block(self, content: str) -> Optional[str]:
397
+ m = self.BLOCK_HEADER_RE.search(content)
398
+ if not m:
399
+ return None
400
+ # Find next [...] table header after our match (start-of-line "[").
401
+ next_tbl = self.NEXT_TABLE_RE.search(content, m.end())
402
+ end = next_tbl.start() if next_tbl else len(content)
403
+ return content[m.start():end]
404
+
405
+ def current_state(self) -> dict:
406
+ content = self._read()
407
+ block = self._extract_block(content)
408
+ sibyl_block_present = block is not None
409
+ wired_with_sibyl = bool(block and self.SIBYL_COMMAND_RE.search(block))
410
+ return {
411
+ "config_path": str(self.config_path),
412
+ "config_exists": self.config_path.exists(),
413
+ "sibyl_block_present": sibyl_block_present,
414
+ "wired_with_sibyl": wired_with_sibyl,
415
+ }
416
+
417
+ def wire(self, *, force: bool = False, dry_run: bool = False,
418
+ prompt_fn: Optional[Callable[..., str]] = None) -> WireOutcome:
419
+ state = self.current_state()
420
+
421
+ if state["wired_with_sibyl"]:
422
+ return WireOutcome(
423
+ self.name, "already",
424
+ f"Codex CLI already has [mcp_servers.sibyl_memory] block in {self.config_path}",
425
+ )
426
+
427
+ if state["sibyl_block_present"] and not force:
428
+ if prompt_fn is None:
429
+ return WireOutcome(
430
+ self.name, "skipped",
431
+ "Existing [mcp_servers.sibyl_memory] differs. Use --force to overwrite.",
432
+ )
433
+ ans = prompt_fn(
434
+ "Codex has [mcp_servers.sibyl_memory] but command differs. Update?",
435
+ default="N",
436
+ )
437
+ if ans != "y":
438
+ return WireOutcome(self.name, "skipped", "TOML block update declined.")
439
+
440
+ if dry_run:
441
+ verb = "replace" if state["sibyl_block_present"] else "append"
442
+ return WireOutcome(
443
+ self.name, "dry-run",
444
+ f"Would {verb} [mcp_servers.sibyl_memory] in {self.config_path}",
445
+ )
446
+
447
+ backup = self._backup_config()
448
+ try:
449
+ self._write_config_with_sibyl(replace_existing=state["sibyl_block_present"])
450
+ except Exception as e:
451
+ return WireOutcome(
452
+ self.name, "error",
453
+ f"config write failed: {type(e).__name__}: {e}",
454
+ backup_path=backup,
455
+ )
456
+ return WireOutcome(
457
+ self.name, "wired",
458
+ f"Added [mcp_servers.sibyl_memory] to {self.config_path}",
459
+ backup_path=backup,
460
+ )
461
+
462
+ def _backup_config(self) -> Optional[Path]:
463
+ if not self.config_path.exists():
464
+ return None
465
+ backup = self.config_path.with_suffix(".toml.bak")
466
+ shutil.copy2(self.config_path, backup)
467
+ return backup
468
+
469
+ def _write_config_with_sibyl(self, *, replace_existing: bool) -> None:
470
+ content = self._read()
471
+
472
+ if replace_existing:
473
+ existing = self._extract_block(content) or ""
474
+ # Rstrip the existing block to a single newline before splice so
475
+ # we don't accumulate blank lines on repeat replaces.
476
+ stripped = existing.rstrip("\n")
477
+ new_content = content.replace(stripped, self.SIBYL_TOML_BLOCK.rstrip("\n"), 1)
478
+ # Ensure file ends with a newline
479
+ if not new_content.endswith("\n"):
480
+ new_content += "\n"
481
+ else:
482
+ # Append: one blank line of separator if file is non-empty
483
+ if not content:
484
+ new_content = self.SIBYL_TOML_BLOCK
485
+ else:
486
+ # Normalize trailing whitespace to exactly "\n\n"
487
+ trimmed = content.rstrip("\n") + "\n\n"
488
+ new_content = trimmed + self.SIBYL_TOML_BLOCK
489
+
490
+ self.config_path.parent.mkdir(parents=True, exist_ok=True)
491
+ tmp = self.config_path.with_suffix(".toml.tmp")
492
+ tmp.write_text(new_content, encoding="utf-8")
493
+ os.replace(tmp, self.config_path)
494
+
495
+
336
496
  # ----------------------------------------------------------------------
337
497
  # Registry + dispatch
338
498
  # ----------------------------------------------------------------------
@@ -340,6 +500,7 @@ class ClaudeCodeWirer:
340
500
  ALL_WIRERS: dict = {
341
501
  "hermes": HermesWirer,
342
502
  "claude-code": ClaudeCodeWirer,
503
+ "codex": CodexWirer,
343
504
  }
344
505
 
345
506
 
@@ -366,6 +527,8 @@ def _wirer_kwargs(args: argparse.Namespace, name: str) -> dict:
366
527
  kw["hermes_home"] = args.hermes_home
367
528
  if name == "claude-code" and getattr(args, "claude_settings", None):
368
529
  kw["settings_path"] = args.claude_settings
530
+ if name == "codex" and getattr(args, "codex_config", None):
531
+ kw["config_path"] = args.codex_config
369
532
  return kw
370
533
 
371
534
 
@@ -402,12 +565,13 @@ def cmd_setup(args: argparse.Namespace) -> int:
402
565
  print(dim("Looked for:"))
403
566
  for name, w in wirers.items():
404
567
  st = w.current_state()
405
- loc = st.get("hermes_home") or st.get("settings_path")
568
+ loc = st.get("hermes_home") or st.get("settings_path") or st.get("config_path")
406
569
  print(f" {w.display_name}: {loc}")
407
570
  print()
408
571
  print(dim("To override detection, point setup at a custom path:"))
409
572
  print(f" {cyan('sibyl setup --hermes-home /custom/path')}")
410
- print(f" {cyan('sibyl setup --claude-settings /custom/settings.json')}")
573
+ print(f" {cyan('sibyl setup --claude-settings /custom/.claude.json')}")
574
+ print(f" {cyan('sibyl setup --codex-config /custom/config.toml')}")
411
575
  print()
412
576
  return 0
413
577
 
@@ -415,7 +579,7 @@ def cmd_setup(args: argparse.Namespace) -> int:
415
579
  print(dim("Detected:"))
416
580
  for name, w in detected.items():
417
581
  st = w.current_state()
418
- loc = st.get("hermes_home") or st.get("settings_path")
582
+ loc = st.get("hermes_home") or st.get("settings_path") or st.get("config_path")
419
583
  print(f" {w.display_name} at {loc}")
420
584
  print()
421
585
 
@@ -449,11 +613,14 @@ def cmd_setup(args: argparse.Namespace) -> int:
449
613
 
450
614
  # Pre-prompt for fresh adds (interactive only). Already-wired and
451
615
  # existing-other-provider are handled inside wire() itself.
616
+ # State keys per wirer: hermes → memory_provider; claude-code →
617
+ # sibyl_mcp; codex → sibyl_block_present.
452
618
  if (
453
619
  not args.yes
454
620
  and not st.get("wired_with_sibyl")
455
621
  and not st.get("memory_provider")
456
622
  and not st.get("sibyl_mcp")
623
+ and not st.get("sibyl_block_present")
457
624
  ):
458
625
  if name == "hermes":
459
626
  q = f"Set SIBYL as default memory provider in {wirer.display_name}?"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sibyl-memory-cli
3
- Version: 0.3.6
3
+ Version: 0.3.7
4
4
  Summary: Command-line interface for the Sibyl Memory Plugin. `sibyl init` activates, `sibyl upgrade` runs the staker / subscription flow, `sibyl status` shows current tier and DB stats, `sibyl whoami` gives a one-line account summary, `sibyl devices` lists active devices and supports per-device revoke.
5
5
  Author-email: "SIBYL, Sibyl Labs LLC" <sibyl@sibyllabs.org>
6
6
  License: MIT