recallgate 0.1.0__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.
recallgate/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """RecallGate: token-efficient controllable memory for AI coding agents."""
2
+
3
+ __version__ = "0.1.0"
recallgate/cli.py ADDED
@@ -0,0 +1,325 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from . import __version__
10
+ from .core import (
11
+ add_memory,
12
+ apply_review_suggestions,
13
+ change_status,
14
+ delete_memory,
15
+ estimate_data,
16
+ estimate_report,
17
+ generate_brief,
18
+ generate_correction,
19
+ info_memory,
20
+ init_workspace,
21
+ list_memories,
22
+ promote_memory,
23
+ review_memories,
24
+ review_suggestions,
25
+ search_memories,
26
+ update_memory,
27
+ )
28
+
29
+
30
+ def print_json(payload: Any) -> None:
31
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
32
+
33
+
34
+ def root_path(value: Optional[str]) -> Optional[Path]:
35
+ return Path(value) if value else None
36
+
37
+
38
+ def add_common(parser: argparse.ArgumentParser, *, root: bool = True, json_output: bool = True) -> None:
39
+ if root:
40
+ parser.add_argument("--root", default=None, help="Project root. Default: nearest parent containing .recallgate, or current directory.")
41
+ if json_output:
42
+ parser.add_argument("--json", action="store_true", help="Print machine-readable JSON output.")
43
+
44
+
45
+ def memory_summary(memory: Dict[str, Any]) -> str:
46
+ roles = ",".join(memory.get("audience", []))
47
+ last_used = memory.get("last_used") or "never"
48
+ return (
49
+ f"{memory['id']} [{memory['status']}] {memory['scope']}/{memory['type']} "
50
+ f"priority={memory.get('priority')} used={memory.get('use_count', 0)} last={last_used} "
51
+ f"({roles}) - {memory.get('short')}"
52
+ )
53
+
54
+
55
+ def build_parser() -> argparse.ArgumentParser:
56
+ parser = argparse.ArgumentParser(
57
+ prog="recall-gate",
58
+ description="Token-efficient controllable memory gate for AI coding agents.",
59
+ )
60
+ parser.add_argument("--version", action="version", version=f"recallgate {__version__}")
61
+ sub = parser.add_subparsers(dest="command", required=True)
62
+
63
+ p_init = sub.add_parser("init", help="Create a local .recallgate memory workspace.")
64
+ p_init.add_argument("--root", default=".", help="Project root. Default: current directory.")
65
+ p_init.add_argument("--json", action="store_true", help="Print machine-readable JSON output.")
66
+
67
+ p_add = sub.add_parser("add", help="Add a memory.")
68
+ add_common(p_add)
69
+ p_add.add_argument("content", help="Memory content.")
70
+ p_add.add_argument("--scope", default="project", help="project, conversation, global, task.")
71
+ p_add.add_argument("--type", default="note", choices=["rule", "preference", "decision", "pitfall", "task", "fact", "correction", "note"])
72
+ p_add.add_argument("--role", default="all", help="Comma-separated roles: coder,tester,writer,reviewer,planner,security,all.")
73
+ p_add.add_argument("--priority", default="medium", choices=["low", "medium", "high", "critical"], help="low, medium, high, critical.")
74
+ p_add.add_argument("--importance", default=5, type=int, help="1-10.")
75
+ p_add.add_argument("--short", default=None, help="Short model-facing version.")
76
+ p_add.add_argument("--keywords", default=None, help="Comma-separated keywords.")
77
+
78
+ p_update = sub.add_parser("update", aliases=["edit"], help="Edit an existing memory.")
79
+ add_common(p_update)
80
+ p_update.add_argument("memory_id")
81
+ p_update.add_argument("--content", default=None, help="Replace full memory content.")
82
+ p_update.add_argument("--short", default=None, help="Replace short model-facing memory.")
83
+ p_update.add_argument("--scope", default=None, help="project, conversation, global, task.")
84
+ p_update.add_argument("--type", dest="type_", default=None, choices=["rule", "preference", "decision", "pitfall", "task", "fact", "correction", "note"])
85
+ p_update.add_argument("--role", default=None, help="Comma-separated roles.")
86
+ p_update.add_argument("--priority", default=None, choices=["low", "medium", "high", "critical"])
87
+ p_update.add_argument("--importance", default=None, type=int, help="1-10.")
88
+ p_update.add_argument("--confidence", default=None, type=float, help="0.0-1.0.")
89
+ p_update.add_argument("--keywords", default=None, help="Comma-separated keywords. Pass empty string to clear.")
90
+ p_update.add_argument("--source", default=None, help="Memory source label.")
91
+
92
+ p_info = sub.add_parser("info", help="Show full memory details.")
93
+ add_common(p_info)
94
+ p_info.add_argument("memory_id")
95
+
96
+ p_list = sub.add_parser("list", help="List memories.")
97
+ add_common(p_list)
98
+ p_list.add_argument("--status", default=None, help="active, archived, trash, deleted.")
99
+ p_list.add_argument("--scope", default=None, help="project, conversation, global, task.")
100
+ p_list.add_argument("--role", default=None, help="Filter by role. Use 'all' or omit this option to show every memory.")
101
+ p_list.add_argument("--sort", default="created_at", choices=["id", "created_at", "updated_at", "last_used", "use_count", "priority", "importance", "status", "scope", "type"], help="Sort field.")
102
+ p_list.add_argument("--reverse", action="store_true", help="Reverse sort order.")
103
+
104
+ for command, help_text in [
105
+ ("archive", "Move memory to archive so it is hidden but recoverable."),
106
+ ("trash", "Move memory to trash so agents cannot read it, but it can be restored."),
107
+ ("restore", "Restore archived or trashed memory back to active."),
108
+ ]:
109
+ p = sub.add_parser(command, help=help_text)
110
+ add_common(p)
111
+ p.add_argument("memory_id")
112
+
113
+ p_delete = sub.add_parser("delete", help="Permanently remove a memory from the index and disk.")
114
+ add_common(p_delete)
115
+ p_delete.add_argument("memory_id")
116
+ p_delete.add_argument("--yes", action="store_true", help="Confirm permanent deletion.")
117
+
118
+ p_brief = sub.add_parser("brief", help="Generate a short role-specific memory brief.")
119
+ add_common(p_brief)
120
+ p_brief.add_argument("task", help="Current task or question.")
121
+ p_brief.add_argument("--role", default=None, help="coder, tester, writer, reviewer, planner, security.")
122
+ p_brief.add_argument("--budget", type=int, default=None, help="Approx max token budget for the brief before token report. Defaults to .recallgate/config.toml default_budget.")
123
+ p_brief.add_argument("--no-estimate", action="store_true", help="Hide token estimate.")
124
+
125
+ p_correct = sub.add_parser("correct", help="Generate a tiny correction brief for stale model memory.")
126
+ add_common(p_correct)
127
+ p_correct.add_argument("issue", help="What the model is getting wrong.")
128
+ p_correct.add_argument("--budget", type=int, default=None, help="Approx max token budget. Defaults to .recallgate/config.toml default_budget.")
129
+
130
+ p_est = sub.add_parser("estimate", help="Estimate local memory and injected brief token cost.")
131
+ add_common(p_est)
132
+ p_est.add_argument("--task", default="general task")
133
+ p_est.add_argument("--role", default=None)
134
+ p_est.add_argument("--budget", type=int, default=None, help="Defaults to .recallgate/config.toml default_budget.")
135
+
136
+ p_search = sub.add_parser("search", help="Search memory content, short text, keywords, and metadata.")
137
+ add_common(p_search)
138
+ p_search.add_argument("query", help="Search query.")
139
+ p_search.add_argument("--status", default=None, help="active, archived, trash, deleted.")
140
+ p_search.add_argument("--scope", default=None, help="project, conversation, global, task.")
141
+ p_search.add_argument("--role", default=None, help="Filter by role. Use 'all' or omit this option to search every memory.")
142
+ p_search.add_argument("--limit", type=int, default=20, help="Maximum results to print.")
143
+
144
+ p_review = sub.add_parser("review", help="Suggest memories to archive, trash, dedupe, or promote.")
145
+ add_common(p_review)
146
+ p_review.add_argument("--apply", action="store_true", help="Apply supported archive/promote suggestions automatically.")
147
+
148
+ p_promote = sub.add_parser("promote", help="Promote a memory to project scope.")
149
+ add_common(p_promote)
150
+ p_promote.add_argument("memory_id")
151
+ p_promote.add_argument("--to", default="project", help="Target scope. Default: project.")
152
+
153
+ return parser
154
+
155
+
156
+ def main(argv: Optional[List[str]] = None) -> int:
157
+ parser = build_parser()
158
+ args = parser.parse_args(argv)
159
+ root = root_path(getattr(args, "root", None))
160
+ json_mode = bool(getattr(args, "json", False))
161
+ try:
162
+ if args.command == "init":
163
+ path = init_workspace(Path(args.root))
164
+ if json_mode:
165
+ print_json({"ok": True, "workspace": str(path)})
166
+ else:
167
+ print(f"RecallGate workspace ready: {path}")
168
+ return 0
169
+
170
+ if args.command == "add":
171
+ memory = add_memory(
172
+ args.content,
173
+ root=root,
174
+ scope=args.scope,
175
+ type_=args.type,
176
+ roles=args.role,
177
+ priority=args.priority,
178
+ short=args.short,
179
+ keywords=args.keywords,
180
+ importance=args.importance,
181
+ )
182
+ if json_mode:
183
+ print_json(memory)
184
+ else:
185
+ print(f"Added {memory['id']}: {memory['short']}")
186
+ return 0
187
+
188
+ if args.command in {"update", "edit"}:
189
+ memory = update_memory(
190
+ args.memory_id,
191
+ root=root,
192
+ content=args.content,
193
+ short=args.short,
194
+ scope=args.scope,
195
+ type_=args.type_,
196
+ roles=args.role,
197
+ priority=args.priority,
198
+ importance=args.importance,
199
+ confidence=args.confidence,
200
+ keywords=args.keywords,
201
+ source=args.source,
202
+ )
203
+ if json_mode:
204
+ print_json(memory)
205
+ else:
206
+ print(f"Updated {memory['id']}: {memory['short']}")
207
+ return 0
208
+
209
+ if args.command == "info":
210
+ memory = info_memory(args.memory_id, root=root)
211
+ if json_mode:
212
+ print_json(memory)
213
+ else:
214
+ print(memory_summary(memory))
215
+ print(f"keywords: {', '.join(memory.get('keywords', [])) or '-'}")
216
+ print(f"importance: {memory.get('importance')} confidence: {memory.get('confidence')} source: {memory.get('source')}")
217
+ print(f"created_at: {memory.get('created_at')}")
218
+ print(f"updated_at: {memory.get('updated_at')}")
219
+ print(f"file: {memory.get('file')}")
220
+ print("\ncontent:")
221
+ print(memory.get("content", ""))
222
+ return 0
223
+
224
+ if args.command == "list":
225
+ memories = list_memories(status=args.status, scope=args.scope, role=args.role, sort_by=args.sort, reverse=args.reverse, root=root)
226
+ if json_mode:
227
+ print_json(memories)
228
+ elif not memories:
229
+ print("No memories found.")
230
+ else:
231
+ for memory in memories:
232
+ print(memory_summary(memory))
233
+ return 0
234
+
235
+ if args.command in {"archive", "trash", "restore", "delete"}:
236
+ if args.command == "delete" and not args.yes:
237
+ print("Refusing to delete without --yes. Consider 'trash' first.", file=sys.stderr)
238
+ return 2
239
+ if args.command == "delete":
240
+ memory = delete_memory(args.memory_id, root=root)
241
+ if json_mode:
242
+ print_json({"ok": True, "action": "delete", "memory": memory})
243
+ else:
244
+ print(f"Deleted {memory['id']} permanently.")
245
+ return 0
246
+ status = {"archive": "archived", "trash": "trash", "restore": "active"}[args.command]
247
+ memory = change_status(args.memory_id, status, root=root)
248
+ if json_mode:
249
+ print_json({"ok": True, "action": args.command, "memory": memory})
250
+ else:
251
+ print(f"Moved {memory['id']} to {status}.")
252
+ return 0
253
+
254
+ if args.command == "brief":
255
+ text = generate_brief(args.task, root=root, role=args.role, budget=args.budget, include_estimate=not args.no_estimate)
256
+ if json_mode:
257
+ print_json({"task": args.task, "role": args.role, "brief": text})
258
+ else:
259
+ print(text, end="")
260
+ return 0
261
+
262
+ if args.command == "correct":
263
+ text = generate_correction(args.issue, root=root, budget=args.budget)
264
+ if json_mode:
265
+ print_json({"issue": args.issue, "correction": text})
266
+ else:
267
+ print(text, end="")
268
+ return 0
269
+
270
+ if args.command == "estimate":
271
+ text = estimate_report(root=root, query=args.task, role=args.role, budget=args.budget)
272
+ if json_mode:
273
+ print_json(estimate_data(root=root, query=args.task, role=args.role, budget=args.budget))
274
+ else:
275
+ print(text, end="")
276
+ return 0
277
+
278
+ if args.command == "search":
279
+ memories = search_memories(
280
+ args.query,
281
+ root=root,
282
+ status=args.status,
283
+ scope=args.scope,
284
+ role=args.role,
285
+ limit=args.limit,
286
+ )
287
+ if json_mode:
288
+ print_json(memories)
289
+ elif not memories:
290
+ print("No matching memories found.")
291
+ else:
292
+ for memory in memories:
293
+ print(memory_summary(memory))
294
+ return 0
295
+
296
+ if args.command == "review":
297
+ if json_mode:
298
+ if args.apply:
299
+ print_json({"applied": apply_review_suggestions(root=root), "suggestions_after_apply": review_suggestions(root=root)})
300
+ else:
301
+ print_json(review_suggestions(root=root))
302
+ else:
303
+ print(review_memories(root=root, apply=args.apply), end="")
304
+ return 0
305
+
306
+ if args.command == "promote":
307
+ memory = promote_memory(args.memory_id, root=root, to_scope=args.to)
308
+ if json_mode:
309
+ print_json(memory)
310
+ else:
311
+ print(f"Promoted {memory['id']} to {memory['scope']}.")
312
+ return 0
313
+
314
+ parser.print_help()
315
+ return 1
316
+ except Exception as exc: # intentionally broad for CLI ergonomics
317
+ if json_mode:
318
+ print_json({"ok": False, "error": str(exc)})
319
+ else:
320
+ print(f"Error: {exc}", file=sys.stderr)
321
+ return 1
322
+
323
+
324
+ if __name__ == "__main__":
325
+ raise SystemExit(main())