engramkeep 0.5.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.
- engramkeep/__init__.py +7 -0
- engramkeep/__main__.py +8 -0
- engramkeep/adapters/__init__.py +9 -0
- engramkeep/cli.py +488 -0
- engramkeep/concepts.py +198 -0
- engramkeep/config.py +79 -0
- engramkeep/dashboard.py +1158 -0
- engramkeep/errors.py +31 -0
- engramkeep/installer.py +561 -0
- engramkeep/mcp_server.py +649 -0
- engramkeep/models.py +260 -0
- engramkeep/paths.py +303 -0
- engramkeep/redact.py +76 -0
- engramkeep/search.py +160 -0
- engramkeep/store.py +600 -0
- engramkeep/troubleshoot.py +253 -0
- engramkeep-0.5.1.dist-info/METADATA +557 -0
- engramkeep-0.5.1.dist-info/RECORD +21 -0
- engramkeep-0.5.1.dist-info/WHEEL +4 -0
- engramkeep-0.5.1.dist-info/entry_points.txt +2 -0
- engramkeep-0.5.1.dist-info/licenses/LICENSE +21 -0
engramkeep/__init__.py
ADDED
engramkeep/__main__.py
ADDED
engramkeep/cli.py
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
"""Command-line interface for Engramkeep."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import __version__, dashboard, mcp_server
|
|
11
|
+
from .config import resolve_home
|
|
12
|
+
from .errors import EngramkeepError, InstallerError, StoreError
|
|
13
|
+
from .installer import available_agents, format_plan, install_agents, plan_installation
|
|
14
|
+
from .models import render_card
|
|
15
|
+
from .paths import atomic_write_text
|
|
16
|
+
from .search import SearchHit, search_cards
|
|
17
|
+
from .store import Store
|
|
18
|
+
from .troubleshoot import FixHit
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _print_json(data: object) -> None:
|
|
22
|
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _open_store(args: argparse.Namespace, *, create: bool = False) -> Store:
|
|
26
|
+
store = Store(resolve_home(args.home))
|
|
27
|
+
if create and not store.is_initialized():
|
|
28
|
+
store.init()
|
|
29
|
+
if not store.is_initialized():
|
|
30
|
+
raise StoreError(f"no Engramkeep store at {store.home}; run 'engramkeep init' first")
|
|
31
|
+
return store
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _format_hit(hit: SearchHit) -> str:
|
|
35
|
+
lines = [f"{hit.title} ({hit.id})"]
|
|
36
|
+
if hit.tags:
|
|
37
|
+
lines.append(f" tags: {', '.join(hit.tags)}")
|
|
38
|
+
if hit.snippet:
|
|
39
|
+
lines.append(f" {hit.snippet}")
|
|
40
|
+
return "\n".join(lines)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _cmd_init(args: argparse.Namespace) -> int:
|
|
44
|
+
result = Store(resolve_home(args.home)).init()
|
|
45
|
+
if result.created:
|
|
46
|
+
action = "Repaired" if result.already_initialized else "Initialized"
|
|
47
|
+
print(f"{action} Engramkeep at {result.home}")
|
|
48
|
+
for path in result.created:
|
|
49
|
+
if path == result.home:
|
|
50
|
+
print(f" created {path}")
|
|
51
|
+
continue
|
|
52
|
+
try:
|
|
53
|
+
relative = path.relative_to(result.home)
|
|
54
|
+
except ValueError:
|
|
55
|
+
relative = path
|
|
56
|
+
print(f" created {relative}")
|
|
57
|
+
else:
|
|
58
|
+
print(f"Engramkeep is already initialized at {result.home}")
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _cmd_remember(args: argparse.Namespace) -> int:
|
|
63
|
+
store = _open_store(args, create=True)
|
|
64
|
+
result = store.remember(args.project, args.decision, tags=args.tag)
|
|
65
|
+
if args.json:
|
|
66
|
+
_print_json(result.to_dict())
|
|
67
|
+
elif result.added:
|
|
68
|
+
print(f"Remembered in {result.card.id}: {result.card.title}")
|
|
69
|
+
elif result.tags_added:
|
|
70
|
+
print(f"Updated tags for {result.card.id}: {', '.join(result.tags_added)}")
|
|
71
|
+
else:
|
|
72
|
+
print(f"Decision already recorded in {result.card.id}")
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _cmd_search(args: argparse.Namespace) -> int:
|
|
77
|
+
store = _open_store(args)
|
|
78
|
+
scan = store.scan_cards()
|
|
79
|
+
if scan.errors:
|
|
80
|
+
print(f"warning: skipped {len(scan.errors)} malformed card(s)", file=sys.stderr)
|
|
81
|
+
hits = search_cards(scan.cards, args.query, tag=args.tag, limit=args.limit)
|
|
82
|
+
if args.json:
|
|
83
|
+
_print_json([hit.to_dict() for hit in hits])
|
|
84
|
+
return 0
|
|
85
|
+
if not hits:
|
|
86
|
+
print("No matches.")
|
|
87
|
+
return 0
|
|
88
|
+
print("\n\n".join(_format_hit(hit) for hit in hits))
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _cmd_show(args: argparse.Namespace) -> int:
|
|
93
|
+
store = _open_store(args)
|
|
94
|
+
card = store.get_card(args.project)
|
|
95
|
+
if card is None:
|
|
96
|
+
raise StoreError(f"project not found: {args.project}")
|
|
97
|
+
if args.json:
|
|
98
|
+
_print_json(card.to_dict())
|
|
99
|
+
else:
|
|
100
|
+
print(render_card(card), end="")
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _cmd_list(args: argparse.Namespace) -> int:
|
|
105
|
+
store = _open_store(args)
|
|
106
|
+
scan = store.scan_cards()
|
|
107
|
+
cards = sorted(scan.cards, key=lambda card: (card.updated, card.title.lower()), reverse=True)
|
|
108
|
+
if args.json:
|
|
109
|
+
_print_json([card.to_dict() for card in cards])
|
|
110
|
+
return 0
|
|
111
|
+
if not cards:
|
|
112
|
+
print("No projects yet.")
|
|
113
|
+
return 0
|
|
114
|
+
for card in cards:
|
|
115
|
+
tags = f" [{', '.join(card.tags)}]" if card.tags else ""
|
|
116
|
+
updated = f" ({card.updated[:10]})" if card.updated else ""
|
|
117
|
+
print(f"- {card.id}: {card.title}{tags}{updated}")
|
|
118
|
+
if scan.errors:
|
|
119
|
+
print(f"warning: skipped {len(scan.errors)} malformed card(s)", file=sys.stderr)
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _cmd_recent(args: argparse.Namespace) -> int:
|
|
124
|
+
store = _open_store(args)
|
|
125
|
+
lines = store.recent(args.limit)
|
|
126
|
+
if args.json:
|
|
127
|
+
_print_json(lines)
|
|
128
|
+
return 0
|
|
129
|
+
if not lines:
|
|
130
|
+
print("No activity yet.")
|
|
131
|
+
return 0
|
|
132
|
+
print("\n".join(lines))
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _format_fix(hit: FixHit) -> str:
|
|
137
|
+
payload = hit.entry
|
|
138
|
+
workspace = payload.workspace or "General"
|
|
139
|
+
lines = [f"{payload.project} ({payload.date})"]
|
|
140
|
+
lines.append(f" workspace: {workspace}")
|
|
141
|
+
lines.append(f" problem: {payload.error}")
|
|
142
|
+
lines.append(f" fix: {payload.fix}")
|
|
143
|
+
return "\n".join(lines)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _cmd_fix(args: argparse.Namespace) -> int:
|
|
147
|
+
store = _open_store(args, create=True)
|
|
148
|
+
entry = store.record_fix(
|
|
149
|
+
args.error,
|
|
150
|
+
args.solution,
|
|
151
|
+
project=args.project,
|
|
152
|
+
workspace=args.workspace,
|
|
153
|
+
)
|
|
154
|
+
if args.json:
|
|
155
|
+
_print_json(entry.to_dict())
|
|
156
|
+
else:
|
|
157
|
+
print(f"Recorded fix for {entry.project}: {entry.error[:80]}")
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _cmd_fixes(args: argparse.Namespace) -> int:
|
|
162
|
+
store = _open_store(args)
|
|
163
|
+
hits = store.search_fixes(args.query, limit=args.limit)
|
|
164
|
+
if args.json:
|
|
165
|
+
_print_json([hit.to_dict() for hit in hits])
|
|
166
|
+
return 0
|
|
167
|
+
if not hits:
|
|
168
|
+
print("No fixes recorded yet." if not args.query else "No matching fixes.")
|
|
169
|
+
return 0
|
|
170
|
+
print("\n\n".join(_format_fix(hit) for hit in hits))
|
|
171
|
+
return 0
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _cmd_forget(args: argparse.Namespace) -> int:
|
|
175
|
+
if not args.yes:
|
|
176
|
+
print("refusing to forget without --yes", file=sys.stderr)
|
|
177
|
+
return 2
|
|
178
|
+
store = _open_store(args)
|
|
179
|
+
if not store.forget(args.project):
|
|
180
|
+
raise StoreError(f"project not found: {args.project}")
|
|
181
|
+
print(f"Moved {args.project} to trash.")
|
|
182
|
+
return 0
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _cmd_export(args: argparse.Namespace) -> int:
|
|
186
|
+
store = _open_store(args)
|
|
187
|
+
document = store.export_cards()
|
|
188
|
+
cards = document.get("cards")
|
|
189
|
+
if not isinstance(cards, list):
|
|
190
|
+
raise StoreError("export document is missing its card list")
|
|
191
|
+
payload = json.dumps(document, ensure_ascii=False, indent=2) + "\n"
|
|
192
|
+
if not args.output:
|
|
193
|
+
print(payload, end="")
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
output = Path(args.output).expanduser().resolve()
|
|
197
|
+
if output.exists() and not args.force:
|
|
198
|
+
raise StoreError(f"refusing to overwrite {output}; pass --force to replace it")
|
|
199
|
+
atomic_write_text(output, payload)
|
|
200
|
+
print(f"Exported {len(cards)} card(s) to {output}")
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _cmd_reindex(args: argparse.Namespace) -> int:
|
|
205
|
+
store = _open_store(args)
|
|
206
|
+
scan = store.reindex()
|
|
207
|
+
print(f"Reindexed {len(scan.cards)} card(s).")
|
|
208
|
+
if scan.errors:
|
|
209
|
+
print(f"warning: skipped {len(scan.errors)} malformed card(s)", file=sys.stderr)
|
|
210
|
+
return 0
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _cmd_doctor(args: argparse.Namespace) -> int:
|
|
214
|
+
report = Store(resolve_home(args.home)).doctor()
|
|
215
|
+
if args.json:
|
|
216
|
+
_print_json(report.to_dict())
|
|
217
|
+
return 0 if report.ok else 1
|
|
218
|
+
|
|
219
|
+
print("Engramkeep doctor")
|
|
220
|
+
print(f"Home: {report.home}")
|
|
221
|
+
print(f"Initialized: {'yes' if report.initialized else 'no'}")
|
|
222
|
+
print(f"Schema: {report.schema_version if report.schema_version is not None else 'unknown'}")
|
|
223
|
+
print(f"Cards: {report.cards}")
|
|
224
|
+
print(f"Index: {'ok' if report.index_present else 'missing'}")
|
|
225
|
+
print(f"Recent activity: {'ok' if report.recent_present else 'missing'}")
|
|
226
|
+
print(f"Audit log: {'ok' if report.audit_present else 'missing'}")
|
|
227
|
+
print(f"Permissions: {'ok' if report.permissions_ok else 'too broad'}")
|
|
228
|
+
for warning in report.warnings:
|
|
229
|
+
print(f"warning: {warning}")
|
|
230
|
+
for error in report.errors:
|
|
231
|
+
print(f"error: {error}")
|
|
232
|
+
print(f"Result: {'OK' if report.ok else 'FAILED'}")
|
|
233
|
+
return 0 if report.ok else 1
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _cmd_path(args: argparse.Namespace) -> int:
|
|
237
|
+
print(resolve_home(args.home))
|
|
238
|
+
return 0
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _cmd_mcp(args: argparse.Namespace) -> int:
|
|
242
|
+
if args.status:
|
|
243
|
+
info = mcp_server.status()
|
|
244
|
+
print(f"MCP: {info['status']}")
|
|
245
|
+
print(f"Transport: {info['transport']}")
|
|
246
|
+
print(f"Protocol: {info['protocol_version']}")
|
|
247
|
+
print(f"Writes: {info['writes']}")
|
|
248
|
+
return 0
|
|
249
|
+
return mcp_server.serve(resolve_home(args.home), allow_write=args.allow_write)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _cmd_dashboard(args: argparse.Namespace) -> int:
|
|
253
|
+
if args.status:
|
|
254
|
+
info = dashboard.status()
|
|
255
|
+
print(f"Dashboard: {info['status']}")
|
|
256
|
+
print(f"Bind: {info['bind']}")
|
|
257
|
+
print(f"Auth: {info['auth']}")
|
|
258
|
+
print(f"Writes: {info['writes']}")
|
|
259
|
+
print(f"CSP: {info['csp']}")
|
|
260
|
+
return 0
|
|
261
|
+
return dashboard.serve(
|
|
262
|
+
resolve_home(args.home),
|
|
263
|
+
port=args.port,
|
|
264
|
+
allow_write=args.allow_write,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _cmd_install(args: argparse.Namespace) -> int:
|
|
269
|
+
if args.list_agents:
|
|
270
|
+
for name in available_agents():
|
|
271
|
+
print(format_plan(plan_installation(name)))
|
|
272
|
+
print()
|
|
273
|
+
return 0
|
|
274
|
+
if not args.agent:
|
|
275
|
+
print("error: --agent is required (or use --list)", file=sys.stderr)
|
|
276
|
+
return 2
|
|
277
|
+
|
|
278
|
+
project_dir = Path(args.project_dir).expanduser().resolve() if args.project_dir else Path.cwd()
|
|
279
|
+
backup_root = resolve_home(args.home) / "backups" / "installer"
|
|
280
|
+
try:
|
|
281
|
+
result = install_agents(
|
|
282
|
+
args.agent,
|
|
283
|
+
user_home=Path.home(),
|
|
284
|
+
project_dir=project_dir,
|
|
285
|
+
backup_root=backup_root,
|
|
286
|
+
scope=args.scope,
|
|
287
|
+
mcp=args.mcp,
|
|
288
|
+
dry_run=not (args.apply or args.uninstall),
|
|
289
|
+
remove=args.uninstall,
|
|
290
|
+
)
|
|
291
|
+
except KeyError as exc:
|
|
292
|
+
print(f"error: {exc.args[0]}", file=sys.stderr)
|
|
293
|
+
print(f"available agents: {', '.join(available_agents())}", file=sys.stderr)
|
|
294
|
+
return 1
|
|
295
|
+
except InstallerError as exc:
|
|
296
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
297
|
+
return 1
|
|
298
|
+
|
|
299
|
+
if args.json:
|
|
300
|
+
_print_json(result.to_dict())
|
|
301
|
+
return 0
|
|
302
|
+
|
|
303
|
+
for operation in result.operations:
|
|
304
|
+
line = f"{operation.action}: {operation.target}"
|
|
305
|
+
if operation.backup:
|
|
306
|
+
line += f" (backup: {operation.backup})"
|
|
307
|
+
if operation.reason:
|
|
308
|
+
line += f" ({operation.reason})"
|
|
309
|
+
print(line)
|
|
310
|
+
if result.mode == "dry-run":
|
|
311
|
+
print("Dry run: no files were modified. Pass --apply to write.")
|
|
312
|
+
if result.backup_dir:
|
|
313
|
+
print(f"Backups: {result.backup_dir}")
|
|
314
|
+
return 0
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
318
|
+
"""Build the command-line parser."""
|
|
319
|
+
parser = argparse.ArgumentParser(
|
|
320
|
+
prog="engramkeep",
|
|
321
|
+
description="Local-first, Markdown-native memory for AI coding agents.",
|
|
322
|
+
)
|
|
323
|
+
parser.add_argument(
|
|
324
|
+
"--home",
|
|
325
|
+
metavar="DIR",
|
|
326
|
+
help="memory directory (default: ENGRAMKEEP_HOME or the platform data directory)",
|
|
327
|
+
)
|
|
328
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
329
|
+
|
|
330
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
331
|
+
|
|
332
|
+
init_parser = subparsers.add_parser("init", help="create the memory directory")
|
|
333
|
+
init_parser.set_defaults(handler=_cmd_init)
|
|
334
|
+
|
|
335
|
+
remember = subparsers.add_parser("remember", help="record a durable decision")
|
|
336
|
+
remember.add_argument("project", help="project name or id")
|
|
337
|
+
remember.add_argument("decision", help="decision text")
|
|
338
|
+
remember.add_argument("--tag", action="append", default=[], help="tag to add (repeatable)")
|
|
339
|
+
remember.add_argument("--json", action="store_true", help="print JSON")
|
|
340
|
+
remember.set_defaults(handler=_cmd_remember)
|
|
341
|
+
|
|
342
|
+
search = subparsers.add_parser("search", help="search cards")
|
|
343
|
+
search.add_argument("query", help="search terms")
|
|
344
|
+
search.add_argument("--tag", help="filter by tag")
|
|
345
|
+
search.add_argument("--limit", type=int, default=10, help="maximum results (default: 10)")
|
|
346
|
+
search.add_argument("--json", action="store_true", help="print JSON")
|
|
347
|
+
search.set_defaults(handler=_cmd_search)
|
|
348
|
+
|
|
349
|
+
show = subparsers.add_parser("show", help="print one card")
|
|
350
|
+
show.add_argument("project", help="project name or id")
|
|
351
|
+
show.add_argument("--json", action="store_true", help="print JSON")
|
|
352
|
+
show.set_defaults(handler=_cmd_show)
|
|
353
|
+
|
|
354
|
+
list_parser = subparsers.add_parser("list", help="list projects")
|
|
355
|
+
list_parser.add_argument("--json", action="store_true", help="print JSON")
|
|
356
|
+
list_parser.set_defaults(handler=_cmd_list)
|
|
357
|
+
|
|
358
|
+
recent = subparsers.add_parser("recent", help="show recent activity")
|
|
359
|
+
recent.add_argument("--limit", type=int, default=10, help="maximum entries (default: 10)")
|
|
360
|
+
recent.add_argument("--json", action="store_true", help="print JSON")
|
|
361
|
+
recent.set_defaults(handler=_cmd_recent)
|
|
362
|
+
|
|
363
|
+
forget = subparsers.add_parser("forget", help="move a card to trash")
|
|
364
|
+
forget.add_argument("project", help="project name or id")
|
|
365
|
+
forget.add_argument("--yes", action="store_true", help="confirm the move")
|
|
366
|
+
forget.set_defaults(handler=_cmd_forget)
|
|
367
|
+
|
|
368
|
+
fix = subparsers.add_parser("fix", help="record a resolved error and its verified fix")
|
|
369
|
+
fix.add_argument("error", help="the error or problem that was resolved")
|
|
370
|
+
fix.add_argument("--solution", required=True, help="the verified fix that worked")
|
|
371
|
+
fix.add_argument("--project", help="project name (default: General)")
|
|
372
|
+
fix.add_argument("--workspace", help="workspace path or label")
|
|
373
|
+
fix.add_argument("--json", action="store_true", help="print JSON")
|
|
374
|
+
fix.set_defaults(handler=_cmd_fix)
|
|
375
|
+
|
|
376
|
+
fixes = subparsers.add_parser("fixes", help="search the troubleshooting registry")
|
|
377
|
+
fixes.add_argument(
|
|
378
|
+
"query",
|
|
379
|
+
nargs="?",
|
|
380
|
+
default=None,
|
|
381
|
+
help="search terms (run without it to list the newest fixes)",
|
|
382
|
+
)
|
|
383
|
+
fixes.add_argument("--limit", type=int, default=10, help="maximum results (default: 10)")
|
|
384
|
+
fixes.add_argument("--json", action="store_true", help="print JSON")
|
|
385
|
+
fixes.set_defaults(handler=_cmd_fixes)
|
|
386
|
+
|
|
387
|
+
export = subparsers.add_parser("export", help="export all cards as JSON")
|
|
388
|
+
export.add_argument("--output", metavar="FILE", help="write to FILE instead of stdout")
|
|
389
|
+
export.add_argument("--force", action="store_true", help="overwrite an existing output file")
|
|
390
|
+
export.set_defaults(handler=_cmd_export)
|
|
391
|
+
|
|
392
|
+
reindex = subparsers.add_parser("reindex", help="regenerate INDEX.md")
|
|
393
|
+
reindex.set_defaults(handler=_cmd_reindex)
|
|
394
|
+
|
|
395
|
+
doctor = subparsers.add_parser("doctor", help="check the local store")
|
|
396
|
+
doctor.add_argument("--json", action="store_true", help="print JSON")
|
|
397
|
+
doctor.set_defaults(handler=_cmd_doctor)
|
|
398
|
+
|
|
399
|
+
path_parser = subparsers.add_parser("path", help="print the memory directory")
|
|
400
|
+
path_parser.set_defaults(handler=_cmd_path)
|
|
401
|
+
|
|
402
|
+
mcp = subparsers.add_parser("mcp", help="run the MCP stdio server")
|
|
403
|
+
mcp.add_argument("--status", action="store_true", help="print server status and exit")
|
|
404
|
+
mcp.add_argument(
|
|
405
|
+
"--allow-write",
|
|
406
|
+
action="store_true",
|
|
407
|
+
help="enable the engramkeep_remember and engramkeep_record_fix tools",
|
|
408
|
+
)
|
|
409
|
+
mcp.set_defaults(handler=_cmd_mcp)
|
|
410
|
+
|
|
411
|
+
dashboard_parser = subparsers.add_parser("dashboard", help="run the local dashboard")
|
|
412
|
+
dashboard_parser.add_argument("--status", action="store_true", help="print status and exit")
|
|
413
|
+
dashboard_parser.add_argument(
|
|
414
|
+
"--port",
|
|
415
|
+
type=int,
|
|
416
|
+
default=dashboard.DEFAULT_PORT,
|
|
417
|
+
help=f"loopback port (default: {dashboard.DEFAULT_PORT}; 0 picks a free port)",
|
|
418
|
+
)
|
|
419
|
+
dashboard_parser.add_argument(
|
|
420
|
+
"--allow-write",
|
|
421
|
+
action="store_true",
|
|
422
|
+
help="enable the remember form",
|
|
423
|
+
)
|
|
424
|
+
dashboard_parser.set_defaults(handler=_cmd_dashboard)
|
|
425
|
+
|
|
426
|
+
install = subparsers.add_parser(
|
|
427
|
+
"install",
|
|
428
|
+
help="plan or apply agent integration (dry run by default)",
|
|
429
|
+
)
|
|
430
|
+
install.add_argument("--agent", help="agent identifier, or 'all'")
|
|
431
|
+
install.add_argument(
|
|
432
|
+
"--list",
|
|
433
|
+
dest="list_agents",
|
|
434
|
+
action="store_true",
|
|
435
|
+
help="list supported agents and exit",
|
|
436
|
+
)
|
|
437
|
+
install.add_argument(
|
|
438
|
+
"--scope",
|
|
439
|
+
choices=["auto", "global", "project"],
|
|
440
|
+
default="auto",
|
|
441
|
+
help="target scope (default: global when documented, otherwise project)",
|
|
442
|
+
)
|
|
443
|
+
install.add_argument(
|
|
444
|
+
"--project-dir",
|
|
445
|
+
metavar="DIR",
|
|
446
|
+
help="project root for project-scoped targets (default: current directory)",
|
|
447
|
+
)
|
|
448
|
+
mode = install.add_mutually_exclusive_group()
|
|
449
|
+
mode.add_argument(
|
|
450
|
+
"--apply",
|
|
451
|
+
action="store_true",
|
|
452
|
+
help="write the managed block and back up existing files",
|
|
453
|
+
)
|
|
454
|
+
mode.add_argument(
|
|
455
|
+
"--uninstall",
|
|
456
|
+
action="store_true",
|
|
457
|
+
help="remove the Engramkeep managed block",
|
|
458
|
+
)
|
|
459
|
+
install.add_argument(
|
|
460
|
+
"--mcp",
|
|
461
|
+
action="store_true",
|
|
462
|
+
help="also register the MCP server in the agent JSON config",
|
|
463
|
+
)
|
|
464
|
+
install.add_argument("--json", action="store_true", help="print JSON")
|
|
465
|
+
install.set_defaults(handler=_cmd_install)
|
|
466
|
+
|
|
467
|
+
return parser
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def main(argv: list[str] | None = None) -> int:
|
|
471
|
+
"""Run the CLI and return an exit code."""
|
|
472
|
+
parser = build_parser()
|
|
473
|
+
args = parser.parse_args(argv)
|
|
474
|
+
try:
|
|
475
|
+
return int(args.handler(args))
|
|
476
|
+
except EngramkeepError as exc:
|
|
477
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
478
|
+
return 1
|
|
479
|
+
except OSError as exc:
|
|
480
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
481
|
+
return 1
|
|
482
|
+
except KeyboardInterrupt:
|
|
483
|
+
print("interrupted", file=sys.stderr)
|
|
484
|
+
return 130
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
if __name__ == "__main__":
|
|
488
|
+
raise SystemExit(main())
|