elephia 0.1.1__py3-none-any.whl

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.
elephia/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """elephia - git for your AI's context.
2
+
3
+ A local-first, deterministic memory engine exposed over MCP (Model Context
4
+ Protocol) and a git-style CLI. Every conversation turn is committed to an
5
+ append-only journal; durable facts merge into a versioned wiki; each prompt
6
+ gets a token-budgeted, salience-ranked context "branch" compiled from history.
7
+ """
8
+
9
+ __version__ = "0.1.1"
10
+
11
+ from elephia.engine import Elephia, resolve_store_dir # noqa: F401
elephia/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from elephia.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
elephia/cli.py ADDED
@@ -0,0 +1,416 @@
1
+ """elephia CLI — git-style commands over your AI's context.
2
+
3
+ elephia init create a context store here (.elephia/)
4
+ elephia ui open the point-and-click dashboard in your browser
5
+ elephia serve run the MCP server on stdio
6
+ elephia install X wire the server into claude-desktop/claude-code/codex/cursor
7
+ elephia status store overview + token counters
8
+ elephia log recent events (newest first)
9
+ elephia show REF full record: event:<id> | wiki:<title> | mut:<id>
10
+ elephia search QUERY BM25 search over events + wiki
11
+ elephia branch PROMPT compile the context branch for a prompt
12
+ elephia merges merge history + pending queue
13
+ elephia pending ... list / approve / reject pending merges
14
+ elephia remember FACT save a durable fact
15
+ elephia stale PAGE mark a wiki page stale
16
+ elephia stats token usage ledger
17
+ elephia demo seed demo data to try things out
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import argparse
23
+ import json
24
+ import os
25
+ import sys
26
+ from typing import Any
27
+
28
+ from elephia import __version__
29
+ from elephia.engine import Elephia, GLOBAL_STORE, STORE_DIRNAME, resolve_store_dir
30
+ from elephia.install import INSTALLERS, snippets
31
+
32
+
33
+ def _print_json(data: Any) -> None:
34
+ print(json.dumps(data, indent=2, ensure_ascii=False))
35
+
36
+
37
+ def _engine(args: argparse.Namespace) -> Elephia:
38
+ return Elephia(getattr(args, "store", None), budget=getattr(args, "budget", None) or 700)
39
+
40
+
41
+ def cmd_init(args: argparse.Namespace) -> int:
42
+ if args.store:
43
+ target = os.path.abspath(os.path.expanduser(args.store))
44
+ elif getattr(args, "global_store", False):
45
+ target = GLOBAL_STORE
46
+ else:
47
+ target = os.path.join(os.getcwd(), STORE_DIRNAME)
48
+ Elephia(target)
49
+ print(f"Initialized empty context store in {target}")
50
+ print("Next steps:")
51
+ print(" elephia demo # optional: seed sample data")
52
+ print(" elephia install claude-code # or claude-desktop / codex / cursor")
53
+ return 0
54
+
55
+
56
+ def cmd_serve(args: argparse.Namespace) -> int:
57
+ from elephia.server import run_stdio
58
+
59
+ store = args.store or os.environ.get("ELEPHIA_DIR") or os.environ.get("CONTEXTGIT_DIR") or None
60
+ engine = Elephia(
61
+ store,
62
+ budget=args.budget,
63
+ compiler_config={"include_full_history": args.full_history},
64
+ )
65
+ run_stdio(engine)
66
+ return 0
67
+
68
+
69
+ def cmd_ui(args: argparse.Namespace) -> int:
70
+ from elephia.ui import run_ui
71
+
72
+ return run_ui(
73
+ _engine(args),
74
+ port=args.port,
75
+ open_browser=not args.no_open,
76
+ )
77
+
78
+
79
+ def cmd_status(args: argparse.Namespace) -> int:
80
+ status = _engine(args).status()
81
+ if args.json:
82
+ _print_json(status)
83
+ return 0
84
+ print(f"store: {status['store_dir']}")
85
+ print(f"events: {status['events']} ({status['full_history_tokens']} tokens of raw history)")
86
+ print(
87
+ f"wiki: {status['wiki_pages']} pages "
88
+ f"({status['wiki_pages_active']} active, {status['wiki_pages_stale']} stale)"
89
+ )
90
+ print(f"merges: {status['mutations']} mutations, {status['pending_merges']} pending review")
91
+ print(f"budget: {status['budget_tokens']} tokens/patch (counter: {status['token_counter']})")
92
+ usage = status["usage"]
93
+ print(
94
+ f"usage: {usage['compilations']} compilations, "
95
+ f"{usage['saved_tokens_total']} tokens saved ({usage['savings_pct']}%)"
96
+ )
97
+ return 0
98
+
99
+
100
+ def cmd_log(args: argparse.Namespace) -> int:
101
+ rows = _engine(args).log(limit=args.number)
102
+ if args.json:
103
+ _print_json(rows)
104
+ return 0
105
+ if not rows:
106
+ print("(no events yet — try `elephia demo` or `elephia remember \"...\"`)")
107
+ return 0
108
+ for row in rows:
109
+ print(f"{row['ref']}")
110
+ print(f" {row['timestamp']} {row['speaker']:<9} {row['tokens']:>4} tok {row['summary']}")
111
+ return 0
112
+
113
+
114
+ def cmd_show(args: argparse.Namespace) -> int:
115
+ try:
116
+ _print_json(_engine(args).show(args.ref))
117
+ return 0
118
+ except (KeyError, ValueError) as exc:
119
+ print(f"error: {exc}", file=sys.stderr)
120
+ return 1
121
+
122
+
123
+ def cmd_search(args: argparse.Namespace) -> int:
124
+ results = _engine(args).search(args.query, limit=args.number)
125
+ if args.json:
126
+ _print_json(results)
127
+ return 0
128
+ if not results:
129
+ print("(no matches)")
130
+ return 0
131
+ for row in results:
132
+ print(f"{row['score']:>7.3f} {row['ref']}")
133
+ print(f" {row['summary']}")
134
+ return 0
135
+
136
+
137
+ def cmd_branch(args: argparse.Namespace) -> int:
138
+ engine = _engine(args)
139
+ if args.explain:
140
+ explanation = engine.explain(args.prompt, budget=args.budget)
141
+ if args.json:
142
+ _print_json(explanation)
143
+ return 0
144
+ print(f"budget: {explanation['budget']} tokens -> patch: {explanation['estimated_tokens']} tokens\n")
145
+ print("SELECTED:")
146
+ for row in explanation["selected"]:
147
+ comps = row["score_components"]
148
+ print(f" {row['score']:+.3f} {row['ref']} ({row['tokens']} tok)")
149
+ print(
150
+ f" rel={comps.get('query_relevance', 0):.2f} "
151
+ f"rec={comps.get('recency', 0):.2f} freq={comps.get('frequency', 0):.2f} "
152
+ f"corr={comps.get('correction_priority', 0):.0f} stale={comps.get('stale_noise_penalty', 0):.1f}"
153
+ )
154
+ print(f"\nEXCLUDED ({explanation['excluded_total']}):")
155
+ for row in explanation["excluded"]:
156
+ reasons = ",".join(row["exclusion_reasons"]) or "ranked_below_cut"
157
+ print(f" {row['score']:+.3f} {row['ref']} [{reasons}]")
158
+ return 0
159
+ result = engine.prepare(args.prompt, budget=args.budget, record_usage=not args.dry_run)
160
+ if args.json:
161
+ _print_json(result)
162
+ return 0
163
+ print(result["context"])
164
+ print(
165
+ f"\n-- {result['estimated_tokens']} tokens (budget {result['budget']}) | "
166
+ f"full history would be {result['full_history_tokens']} tokens | "
167
+ f"saved {result['saved_tokens']} ({result['savings_pct']}%)"
168
+ )
169
+ return 0
170
+
171
+
172
+ def cmd_merges(args: argparse.Namespace) -> int:
173
+ data = _engine(args).merges(limit=args.number)
174
+ if args.json:
175
+ _print_json(data)
176
+ return 0
177
+ if not data["mutations"]:
178
+ print("(no merges yet)")
179
+ for row in data["mutations"]:
180
+ claim = (row["claim"] or "")[:90]
181
+ target = f" -> {row['target_page']}" if row["target_page"] else ""
182
+ print(f"{row['ref']}")
183
+ print(f" {row['timestamp']} {row['action']:<11} {claim}{target}")
184
+ if data["pending"]:
185
+ print(f"\nPENDING REVIEW ({len(data['pending'])}):")
186
+ for item in data["pending"]:
187
+ print(f" [{item['type']}] {item['content'][:90]}")
188
+ print(f" reason: {item['reason']}")
189
+ return 0
190
+
191
+
192
+ def cmd_pending(args: argparse.Namespace) -> int:
193
+ engine = _engine(args)
194
+ if args.action == "list":
195
+ items = engine.merges(limit=0)["pending"]
196
+ if args.json:
197
+ _print_json(items)
198
+ elif not items:
199
+ print("(pending queue is empty)")
200
+ else:
201
+ for item in items:
202
+ print(f"[{item['type']}] {item['content']}")
203
+ print(f" reason: {item['reason']}")
204
+ return 0
205
+ if not args.content:
206
+ print("error: approve/reject require the pending item's content", file=sys.stderr)
207
+ return 1
208
+ try:
209
+ result = engine.resolve_pending(args.content, args.action)
210
+ except (KeyError, ValueError) as exc:
211
+ print(f"error: {exc}", file=sys.stderr)
212
+ return 1
213
+ print(f"{result['action']}: {args.content[:90]} ({result['ref']})")
214
+ return 0
215
+
216
+
217
+ def cmd_remember(args: argparse.Namespace) -> int:
218
+ result = _engine(args).remember(args.fact, page=args.page)
219
+ if result.get("pending_review"):
220
+ print(f"pending review for '{result['target_page']}' ({result['ref']})")
221
+ print(f" {result['claim']}")
222
+ return 0
223
+ print(f"saved to '{result['target_page']}' ({result['ref']})")
224
+ print(f" {result['claim']}")
225
+ return 0
226
+
227
+
228
+ def cmd_stale(args: argparse.Namespace) -> int:
229
+ result = _engine(args).mark_stale(args.page, superseded_by=args.superseded_by)
230
+ print(f"marked stale: {args.page} ({result['ref']})")
231
+ return 0
232
+
233
+
234
+ def cmd_stats(args: argparse.Namespace) -> int:
235
+ stats = _engine(args).stats()
236
+ if args.json:
237
+ _print_json(stats)
238
+ return 0
239
+ all_time = stats["all_time"]
240
+ week = stats["last_7_days"]
241
+ print(f"store: {stats['store_dir']} (token counter: {stats['token_counter']})\n")
242
+ print(f"{'':<14}{'compilations':>14}{'patch tokens':>14}{'saved tokens':>14}{'savings':>10}")
243
+ print(
244
+ f"{'all time':<14}{all_time['compilations']:>14}{all_time['patch_tokens_total']:>14}"
245
+ f"{all_time['saved_tokens_total']:>14}{all_time['savings_pct']:>9}%"
246
+ )
247
+ print(
248
+ f"{'last 7 days':<14}{week['compilations']:>14}{week['patch_tokens_total']:>14}"
249
+ f"{week['saved_tokens_total']:>14}{week['savings_pct']:>9}%"
250
+ )
251
+ if all_time["by_day"]:
252
+ print("\nby day:")
253
+ for day, row in all_time["by_day"].items():
254
+ print(f" {day} {row['compilations']:>4} compilations {row['saved_tokens']:>8} tokens saved")
255
+ return 0
256
+
257
+
258
+ def cmd_demo(args: argparse.Namespace) -> int:
259
+ from elephia.demo import seed_demo
260
+
261
+ engine = _engine(args)
262
+ result = seed_demo(engine)
263
+ print(f"Seeded {result['turns']} demo turns into {engine.store_dir}")
264
+ print(f" events added: {result['events_added']}, wiki pages now: {result['wiki_pages']}")
265
+ print("Try:")
266
+ print(' elephia branch "What database does Atlas use?"')
267
+ print(' elephia branch "What database does Atlas use?" --explain')
268
+ print(" elephia merges")
269
+ print(" elephia stats")
270
+ return 0
271
+
272
+
273
+ def cmd_install(args: argparse.Namespace) -> int:
274
+ if args.client == "print":
275
+ print(snippets(store=args.store, budget=args.budget))
276
+ return 0
277
+ installer = INSTALLERS[args.client]
278
+ kwargs = {"store": args.store, "budget": args.budget}
279
+ if args.client == "codex":
280
+ kwargs["force"] = args.force
281
+ print(installer(**kwargs))
282
+ return 0
283
+
284
+
285
+ def cmd_export(args: argparse.Namespace) -> int:
286
+ engine = _engine(args)
287
+ snapshot = engine.runtime.snapshot().model_dump()
288
+ payload = json.dumps(snapshot, indent=2, ensure_ascii=False)
289
+ if args.out:
290
+ with open(args.out, "w", encoding="utf-8") as f:
291
+ f.write(payload)
292
+ print(f"exported snapshot to {args.out}")
293
+ else:
294
+ print(payload)
295
+ return 0
296
+
297
+
298
+ def _add_common(parser: argparse.ArgumentParser, json_flag: bool = True) -> None:
299
+ parser.add_argument("--store", help="context store path (default: nearest .elephia/, else global)")
300
+ if json_flag:
301
+ parser.add_argument("--json", action="store_true", help="machine-readable output")
302
+
303
+
304
+ def build_parser() -> argparse.ArgumentParser:
305
+ parser = argparse.ArgumentParser(
306
+ prog="elephia",
307
+ description="git for your AI's context — local-first memory over MCP",
308
+ )
309
+ parser.add_argument("--version", action="version", version=f"elephia {__version__}")
310
+ sub = parser.add_subparsers(dest="command", required=True)
311
+
312
+ p = sub.add_parser("init", help="create a context store")
313
+ p.add_argument("--global", dest="global_store", action="store_true", help=f"use the global store ({GLOBAL_STORE})")
314
+ p.add_argument("--store", help="explicit store path")
315
+ p.set_defaults(func=cmd_init)
316
+
317
+ p = sub.add_parser("serve", aliases=["mcp"], help="run the MCP server on stdio")
318
+ p.add_argument("--store", help="context store path")
319
+ p.add_argument("--budget", type=int, default=700, help="token budget per context patch (default 700)")
320
+ p.add_argument("--full-history", action="store_true", help="append full history to every patch (debug)")
321
+ p.set_defaults(func=cmd_serve)
322
+
323
+ p = sub.add_parser("ui", help="open the point-and-click dashboard in your browser")
324
+ p.add_argument("--store", help="context store path (default: nearest .elephia/, else global)")
325
+ p.add_argument("--port", type=int, default=0, help="port to listen on (default: random free port)")
326
+ p.add_argument("--no-open", action="store_true", help="don't open the browser automatically")
327
+ p.add_argument("--budget", type=int, default=700, help="token budget for context previews (default 700)")
328
+ p.set_defaults(func=cmd_ui)
329
+
330
+ p = sub.add_parser("status", help="store overview + token counters")
331
+ _add_common(p)
332
+ p.set_defaults(func=cmd_status)
333
+
334
+ p = sub.add_parser("log", help="recent events, newest first")
335
+ _add_common(p)
336
+ p.add_argument("-n", "--number", type=int, default=20)
337
+ p.set_defaults(func=cmd_log)
338
+
339
+ p = sub.add_parser("show", help="show one record by ref")
340
+ _add_common(p, json_flag=False)
341
+ p.add_argument("ref", help="event:<id> | wiki:<title> | mut:<id>")
342
+ p.set_defaults(func=cmd_show)
343
+
344
+ p = sub.add_parser("search", help="BM25 search over events + wiki")
345
+ _add_common(p)
346
+ p.add_argument("query")
347
+ p.add_argument("-n", "--number", type=int, default=8)
348
+ p.set_defaults(func=cmd_search)
349
+
350
+ p = sub.add_parser("branch", help="compile the context branch for a prompt")
351
+ _add_common(p)
352
+ p.add_argument("prompt")
353
+ p.add_argument("--budget", type=int, help="token budget (default 700)")
354
+ p.add_argument("--explain", action="store_true", help="show selected/excluded with score components")
355
+ p.add_argument("--dry-run", action="store_true", help="don't record this compilation in the usage ledger")
356
+ p.set_defaults(func=cmd_branch)
357
+
358
+ p = sub.add_parser("merges", help="merge history + pending queue")
359
+ _add_common(p)
360
+ p.add_argument("-n", "--number", type=int, default=20)
361
+ p.set_defaults(func=cmd_merges)
362
+
363
+ p = sub.add_parser("pending", help="list/approve/reject pending merges")
364
+ _add_common(p)
365
+ p.add_argument("action", choices=["list", "approve", "reject"], nargs="?", default="list")
366
+ p.add_argument("content", nargs="?", help="exact content of the pending item (for approve/reject)")
367
+ p.set_defaults(func=cmd_pending)
368
+
369
+ p = sub.add_parser("remember", help="save a durable fact")
370
+ _add_common(p, json_flag=False)
371
+ p.add_argument("fact")
372
+ p.add_argument("--page", help="target wiki page title")
373
+ p.set_defaults(func=cmd_remember)
374
+
375
+ p = sub.add_parser("stale", help="mark a wiki page stale")
376
+ _add_common(p, json_flag=False)
377
+ p.add_argument("page")
378
+ p.add_argument("--superseded-by", help="what replaces it")
379
+ p.set_defaults(func=cmd_stale)
380
+
381
+ p = sub.add_parser("stats", help="token usage ledger")
382
+ _add_common(p)
383
+ p.set_defaults(func=cmd_stats)
384
+
385
+ p = sub.add_parser("demo", help="seed demo data")
386
+ _add_common(p, json_flag=False)
387
+ p.set_defaults(func=cmd_demo)
388
+
389
+ p = sub.add_parser("install", help="wire the MCP server into a client")
390
+ p.add_argument("client", choices=[*sorted(INSTALLERS), "print"])
391
+ p.add_argument("--store", help="pin the server to a specific store path")
392
+ p.add_argument("--budget", type=int, help="token budget per patch")
393
+ p.add_argument("--force", action="store_true", help="replace an existing elephia block when supported")
394
+ p.set_defaults(func=cmd_install)
395
+
396
+ p = sub.add_parser("export", help="dump a full JSON snapshot of the store")
397
+ _add_common(p, json_flag=False)
398
+ p.add_argument("--out", help="output file (default: stdout)")
399
+ p.set_defaults(func=cmd_export)
400
+
401
+ return parser
402
+
403
+
404
+ def main(argv=None) -> int:
405
+ parser = build_parser()
406
+ args = parser.parse_args(argv)
407
+ try:
408
+ return args.func(args)
409
+ except BrokenPipeError:
410
+ return 0
411
+ except KeyboardInterrupt:
412
+ return 130
413
+
414
+
415
+ if __name__ == "__main__":
416
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ """Deterministic context engine internals (vendored from branch-context-lab)."""