agents-memory 0.42__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.
- agent_memory/__init__.py +18 -0
- agent_memory/__main__.py +92 -0
- agent_memory/cli_help.py +166 -0
- agent_memory/consolidate.py +22 -0
- agent_memory/extract_openai.py +102 -0
- agent_memory/ingest.py +110 -0
- agent_memory/ingest_catalog.py +346 -0
- agent_memory/ingest_chats.py +14 -0
- agent_memory/ingest_common.py +170 -0
- agent_memory/ingest_config.py +245 -0
- agent_memory/ingest_extractors.py +374 -0
- agent_memory/inventory.py +101 -0
- agent_memory/mcp_server.py +313 -0
- agent_memory/store.py +2211 -0
- agent_memory/sync.py +67 -0
- agents_memory-0.42.dist-info/METADATA +148 -0
- agents_memory-0.42.dist-info/RECORD +21 -0
- agents_memory-0.42.dist-info/WHEEL +5 -0
- agents_memory-0.42.dist-info/entry_points.txt +2 -0
- agents_memory-0.42.dist-info/licenses/LICENSE +21 -0
- agents_memory-0.42.dist-info/top_level.txt +1 -0
agent_memory/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Local markdown memory — reference implementation of the agent-memory ABI."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
__all__ = ["ROOT", "__version__"]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _engine_root() -> Path:
|
|
10
|
+
here = Path(__file__).resolve().parent
|
|
11
|
+
for path in [here, *here.parents]:
|
|
12
|
+
if (path / "abi" / "VERSION").is_file():
|
|
13
|
+
return path
|
|
14
|
+
return here.parents[2]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ROOT = _engine_root()
|
|
18
|
+
__version__ = (ROOT / "abi" / "VERSION").read_text(encoding="utf-8").strip()
|
agent_memory/__main__.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
# Bootstrap src/ on sys.path if invoked without editable install
|
|
5
|
+
_SRC_DIR = str(Path(__file__).resolve().parent.parent)
|
|
6
|
+
if _SRC_DIR not in sys.path:
|
|
7
|
+
sys.path.insert(0, _SRC_DIR)
|
|
8
|
+
|
|
9
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
10
|
+
try:
|
|
11
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
12
|
+
except Exception:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
USAGE = """Usage: python -m agent_memory COMMAND [args]
|
|
16
|
+
|
|
17
|
+
Commands:
|
|
18
|
+
sync Rewrite always-on injection
|
|
19
|
+
inventory Disk vs PROJECTS.md
|
|
20
|
+
ingest Catalog / extract pipeline
|
|
21
|
+
consolidate Move clone leaks into ~/.agents/memory
|
|
22
|
+
extract-openai Filter Open AI GDPR export into staging
|
|
23
|
+
distill Inspect staging inbox for distillation
|
|
24
|
+
mcp stdio MCP server
|
|
25
|
+
help-json Machine-readable CLI + injection spec
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main(argv: list[str] | None = None) -> int:
|
|
30
|
+
args = list(argv if argv is not None else sys.argv[1:])
|
|
31
|
+
if not args or args[0] in ("-h", "--help"):
|
|
32
|
+
print(USAGE, end="")
|
|
33
|
+
return 0 if args else 2
|
|
34
|
+
if args[0] in ("--help-json", "help-json"):
|
|
35
|
+
from .cli_help import main as help_main
|
|
36
|
+
|
|
37
|
+
return help_main(["--help-json"])
|
|
38
|
+
cmd, rest = args[0], args[1:]
|
|
39
|
+
if cmd == "sync":
|
|
40
|
+
from .sync import main as run
|
|
41
|
+
|
|
42
|
+
return run(rest)
|
|
43
|
+
if cmd == "inventory":
|
|
44
|
+
from .inventory import main as run
|
|
45
|
+
|
|
46
|
+
return run(rest)
|
|
47
|
+
if cmd == "ingest":
|
|
48
|
+
from .ingest import main as run
|
|
49
|
+
|
|
50
|
+
return run(rest)
|
|
51
|
+
if cmd == "distill":
|
|
52
|
+
from .store import get_staging_inbox
|
|
53
|
+
|
|
54
|
+
inbox = get_staging_inbox(limit=15)
|
|
55
|
+
if inbox["total"] == 0:
|
|
56
|
+
print("Staging inbox is empty (all caught up).")
|
|
57
|
+
return 0
|
|
58
|
+
print(f"Staging inbox: {inbox['total']} bullets ({inbox['shown']} shown)")
|
|
59
|
+
for group in inbox["groups"]:
|
|
60
|
+
label = group.get("source") or group.get("file")
|
|
61
|
+
extra = ""
|
|
62
|
+
if group.get("truncated"):
|
|
63
|
+
extra = f" (showing {group['count']} of {group['count']}+)"
|
|
64
|
+
print(f"\n## {label}{extra}")
|
|
65
|
+
for item in group["bullets"]:
|
|
66
|
+
title = item.get("title") or ""
|
|
67
|
+
prefix = f"[{title}] " if title else ""
|
|
68
|
+
print(f"- {prefix}{item.get('text') or item.get('bullet')}")
|
|
69
|
+
print("\nTo distill, tell your Agent: 'run memory-distill' or use the memory-distill skill.")
|
|
70
|
+
return 0
|
|
71
|
+
if cmd in ("ingest-chats", "ingest_chats"):
|
|
72
|
+
from .ingest_chats import main as run
|
|
73
|
+
|
|
74
|
+
return run()
|
|
75
|
+
if cmd == "consolidate":
|
|
76
|
+
from .consolidate import main as run
|
|
77
|
+
|
|
78
|
+
return run()
|
|
79
|
+
if cmd in ("extract-openai", "extract_openai"):
|
|
80
|
+
from .extract_openai import main as run
|
|
81
|
+
|
|
82
|
+
return run(rest)
|
|
83
|
+
if cmd in ("mcp", "mcp-server", "mcp_server"):
|
|
84
|
+
from .mcp_server import main as run
|
|
85
|
+
|
|
86
|
+
return run()
|
|
87
|
+
print(f"unknown command: {cmd}\n{USAGE}", file=sys.stderr)
|
|
88
|
+
return 2
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
raise SystemExit(main())
|
agent_memory/cli_help.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Machine-readable CLI specs derived from argparse (source of truth)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any, List
|
|
8
|
+
|
|
9
|
+
from . import __version__ as ABI_VERSION
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _json_safe(value: Any) -> Any:
|
|
13
|
+
if value is None or isinstance(value, (bool, int, float, str)):
|
|
14
|
+
return value
|
|
15
|
+
if isinstance(value, (list, tuple)):
|
|
16
|
+
return [_json_safe(v) for v in value]
|
|
17
|
+
return str(value)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _action_spec(action: argparse.Action) -> dict[str, Any] | None:
|
|
21
|
+
if action.help is argparse.SUPPRESS:
|
|
22
|
+
return None
|
|
23
|
+
if action.option_strings and action.option_strings[0] in ("-h", "--help"):
|
|
24
|
+
return None
|
|
25
|
+
if action.dest in ("help", "func"):
|
|
26
|
+
return None
|
|
27
|
+
if isinstance(action, argparse._SubParsersAction):
|
|
28
|
+
return None
|
|
29
|
+
flags = list(action.option_strings)
|
|
30
|
+
is_option = bool(flags)
|
|
31
|
+
is_flag = action.nargs == 0 or isinstance(action, argparse._StoreTrueAction)
|
|
32
|
+
spec: dict[str, Any] = {
|
|
33
|
+
"name": action.metavar or action.dest,
|
|
34
|
+
"dest": action.dest,
|
|
35
|
+
"help": action.help or "",
|
|
36
|
+
"required": bool(action.required),
|
|
37
|
+
}
|
|
38
|
+
if is_option:
|
|
39
|
+
spec["flags"] = flags
|
|
40
|
+
spec["kind"] = "flag" if is_flag else "option"
|
|
41
|
+
else:
|
|
42
|
+
spec["kind"] = "argument"
|
|
43
|
+
if action.nargs is not None:
|
|
44
|
+
spec["nargs"] = str(action.nargs)
|
|
45
|
+
default = action.default
|
|
46
|
+
if default is not argparse.SUPPRESS and default is not None:
|
|
47
|
+
spec["default"] = _json_safe(default)
|
|
48
|
+
if action.choices:
|
|
49
|
+
spec["choices"] = [_json_safe(c) for c in action.choices]
|
|
50
|
+
return spec
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def cli_spec(parser: argparse.ArgumentParser, *, name: str, description: str = "") -> dict[str, Any]:
|
|
54
|
+
options: List[dict[str, Any]] = []
|
|
55
|
+
arguments: List[dict[str, Any]] = []
|
|
56
|
+
for action in parser._actions:
|
|
57
|
+
item = _action_spec(action)
|
|
58
|
+
if not item:
|
|
59
|
+
continue
|
|
60
|
+
if item["kind"] == "argument":
|
|
61
|
+
arguments.append(item)
|
|
62
|
+
else:
|
|
63
|
+
options.append(item)
|
|
64
|
+
return {
|
|
65
|
+
"name": name,
|
|
66
|
+
"abi_version": ABI_VERSION,
|
|
67
|
+
"description": description or parser.description or "",
|
|
68
|
+
"usage": parser.format_usage().strip(),
|
|
69
|
+
"options": options,
|
|
70
|
+
"arguments": arguments,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def injection_spec() -> dict[str, Any]:
|
|
75
|
+
return {
|
|
76
|
+
"scan_skips": [".agents", ".cursor", ".git", "node_modules"],
|
|
77
|
+
"no_empty_folders": True,
|
|
78
|
+
"generated_on_sync": [
|
|
79
|
+
{
|
|
80
|
+
"path": "~/.agents/AGENTS.md",
|
|
81
|
+
"from": ["~/.agents/memory/USER.md", "~/.agents/memory/PROJECTS.md"],
|
|
82
|
+
"edit": "Edit USER.md / PROJECTS.md, then re-run python -m agent_memory sync",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"path": "~/.agents/CLAUDE.md",
|
|
86
|
+
"bind": "~/.agents/AGENTS.md",
|
|
87
|
+
"edit": "Edit AGENTS.md only",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"path": "~/.agents/rules/<agent_rule_name>.mdc",
|
|
91
|
+
"hosts": ["~/.cursor/rules/<agent_rule_name>.mdc (bound on sync)"],
|
|
92
|
+
"config": "scan.json agent_rule_name (default user-rules.mdc)",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"path": "<repo>/.agents/AGENTS.md + CLAUDE.md",
|
|
96
|
+
"when": "registered project with path on disk",
|
|
97
|
+
"marker": "<!-- agent-memory-sync -->",
|
|
98
|
+
"note": "No <repo>/.cursor/ — scan skips .cursor; no empty memory subfolders",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"path": "<repo>/.agents/memory/README.md + staging/captured.md",
|
|
102
|
+
"when": "register_project",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"path": "~/.agents/skills/memory-sync/SKILL.md",
|
|
106
|
+
"also": ["host skill dirs per INSTALL.md"],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
"merged_on_init": [
|
|
110
|
+
{"path": "~/.cursor/mcp.json", "key": "mcpServers.agent-memory"},
|
|
111
|
+
{"path": "Zed settings.json", "key": "context_servers"},
|
|
112
|
+
],
|
|
113
|
+
"copied_not_generated": [
|
|
114
|
+
{"path": "~/.agents/memory/LAYOUT.md", "source": "abi/LAYOUT.md in engine clone"},
|
|
115
|
+
],
|
|
116
|
+
"marker": "<!-- agent-memory-sync -->",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def full_spec() -> dict[str, Any]:
|
|
121
|
+
from .sync import build_parser as sync_parser
|
|
122
|
+
from .inventory import build_parser as inventory_parser
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
"name": "agent-memory",
|
|
126
|
+
"abi_version": ABI_VERSION,
|
|
127
|
+
"scripts": {
|
|
128
|
+
"sync": cli_spec(sync_parser(), name="sync", description="Rewrite always-on injection for your Agent."),
|
|
129
|
+
"inventory": cli_spec(
|
|
130
|
+
inventory_parser(),
|
|
131
|
+
name="inventory",
|
|
132
|
+
description="Compare scan.json roots to PROJECTS.md.",
|
|
133
|
+
),
|
|
134
|
+
},
|
|
135
|
+
"scripts_no_flags": {
|
|
136
|
+
"ingest": "Serial ingest: catalog (references) -> extract (staging) -> distill via MCP add_memory.",
|
|
137
|
+
"consolidate": "Move live markdown leaked into the engine clone into ~/.agents/memory.",
|
|
138
|
+
"extract-openai": "Thin wrapper: ingest extract for Open AI GDPR export (openai-export; --out = legacy JSON).",
|
|
139
|
+
"mcp": "MCP stdio server. Tools: see abi/MCP.md.",
|
|
140
|
+
},
|
|
141
|
+
"injection": injection_spec(),
|
|
142
|
+
"discover": "python -m agent_memory --help-json | python -m agent_memory sync --help-json | python -m agent_memory inventory --help-json",
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def emit_help_json(argv: list[str], parser: argparse.ArgumentParser, *, name: str, description: str = "") -> None:
|
|
147
|
+
payload = cli_spec(parser, name=name, description=description)
|
|
148
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def main(argv: list[str] | None = None) -> int:
|
|
152
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
153
|
+
try:
|
|
154
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
args = list(argv if argv is not None else sys.argv[1:])
|
|
158
|
+
if "--help-json" in args:
|
|
159
|
+
print(json.dumps(full_spec(), indent=2, ensure_ascii=False))
|
|
160
|
+
return 0
|
|
161
|
+
print("Usage: python -m agent_memory --help-json", file=sys.stderr)
|
|
162
|
+
return 2
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
if __name__ == "__main__":
|
|
166
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Move live markdown out of the engine clone into ~/.agents/memory."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from .store import consolidate_repo_leaks, ensure_memory_layout
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> int:
|
|
10
|
+
ensure_memory_layout()
|
|
11
|
+
moved = consolidate_repo_leaks()
|
|
12
|
+
if not moved:
|
|
13
|
+
print("nothing to consolidate")
|
|
14
|
+
return 0
|
|
15
|
+
print(f"consolidated {len(moved)} paths:")
|
|
16
|
+
for line in moved:
|
|
17
|
+
print(f" {line}")
|
|
18
|
+
return 0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
sys.exit(main())
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Filter Open AI GDPR export: durable user lines → staging/ingest/openai-export/captured.md."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import tempfile
|
|
8
|
+
import zipfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .ingest_common import keep_user_line, scrub, write_staging
|
|
12
|
+
from .ingest_config import discover_openai_exports, get_source, load_ingest
|
|
13
|
+
from .ingest_extractors import extract_openai, user_messages
|
|
14
|
+
|
|
15
|
+
keep_message = keep_user_line # tests + legacy name
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def unzip_export(src: Path, dest: Path | None = None) -> Path:
|
|
19
|
+
dest = dest or Path(tempfile.gettempdir()) / "agent-memory-openai-export"
|
|
20
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
if src.suffix.lower() == ".zip" and src.is_file():
|
|
22
|
+
with zipfile.ZipFile(src) as zf:
|
|
23
|
+
zf.extractall(dest)
|
|
24
|
+
return dest
|
|
25
|
+
if src.is_dir():
|
|
26
|
+
return src
|
|
27
|
+
raise FileNotFoundError(f"not an Open AI GDPR export zip or folder: {src}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def extract_facts(root: Path) -> list[dict]:
|
|
31
|
+
rows: list[dict] = []
|
|
32
|
+
seen: set[str] = set()
|
|
33
|
+
for shard in sorted(root.glob("conversations-*.json")):
|
|
34
|
+
data = json.loads(shard.read_text(encoding="utf-8"))
|
|
35
|
+
for conv in data if isinstance(data, list) else []:
|
|
36
|
+
if not isinstance(conv, dict):
|
|
37
|
+
continue
|
|
38
|
+
title = str(conv.get("title") or "(untitled)")
|
|
39
|
+
for raw in user_messages(conv):
|
|
40
|
+
text = scrub(raw)
|
|
41
|
+
if not keep_message(title, text):
|
|
42
|
+
continue
|
|
43
|
+
key = re.sub(r"\s+", " ", text.lower())
|
|
44
|
+
if key in seen:
|
|
45
|
+
continue
|
|
46
|
+
seen.add(key)
|
|
47
|
+
rows.append({"title": title, "text": text[:400], "shard": shard.name})
|
|
48
|
+
return rows
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def resolve_export(path: str) -> Path:
|
|
52
|
+
if path:
|
|
53
|
+
p = Path(path).expanduser()
|
|
54
|
+
if p.exists():
|
|
55
|
+
return p
|
|
56
|
+
raise FileNotFoundError(path)
|
|
57
|
+
found = discover_openai_exports()
|
|
58
|
+
if not found:
|
|
59
|
+
raise FileNotFoundError(
|
|
60
|
+
"no Open AI GDPR export found — configure openai-export in ~/.agents/memory/ingest.json"
|
|
61
|
+
)
|
|
62
|
+
return found[0]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main(argv: list[str] | None = None) -> int:
|
|
66
|
+
parser = argparse.ArgumentParser(description="Filter Open AI GDPR export to ingest staging")
|
|
67
|
+
parser.add_argument("--zip", help="export zip or unpacked folder")
|
|
68
|
+
parser.add_argument("--out", help="legacy: write JSON instead of markdown staging")
|
|
69
|
+
args = parser.parse_args(argv)
|
|
70
|
+
|
|
71
|
+
if args.out:
|
|
72
|
+
src = resolve_export(args.zip or "")
|
|
73
|
+
root = unzip_export(src)
|
|
74
|
+
rows = extract_facts(root)
|
|
75
|
+
out = Path(args.out)
|
|
76
|
+
out.write_text(json.dumps(rows, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
77
|
+
print(f"source {src}")
|
|
78
|
+
print(f"kept {len(rows)} user statements")
|
|
79
|
+
print(f"staging {out}")
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
cfg = load_ingest()
|
|
83
|
+
src = get_source("openai-export", cfg) or {
|
|
84
|
+
"id": "openai-export",
|
|
85
|
+
"kind": "openai-export",
|
|
86
|
+
"label": "Open AI — GDPR export",
|
|
87
|
+
"paths": [str(resolve_export(args.zip or ""))],
|
|
88
|
+
"extract": True,
|
|
89
|
+
}
|
|
90
|
+
if args.zip:
|
|
91
|
+
src = dict(src)
|
|
92
|
+
src["paths"] = [str(Path(args.zip).expanduser())]
|
|
93
|
+
lines = extract_openai(src)
|
|
94
|
+
label = str(src.get("label") or "Open AI — GDPR export")
|
|
95
|
+
path = write_staging("openai-export", label, lines)
|
|
96
|
+
print(f"kept {len(lines)} user statements")
|
|
97
|
+
print(f"staging {path}")
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
raise SystemExit(main())
|
agent_memory/ingest.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Serial ingest pipeline: catalog -> extract -> (distill via MCP add_memory)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from .ingest_catalog import run_catalog
|
|
9
|
+
from .ingest_common import ingest_state_path, load_state
|
|
10
|
+
from .ingest_config import list_sources, load_ingest
|
|
11
|
+
from .ingest_extractors import run_extract
|
|
12
|
+
from .store import ensure_memory_layout
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def cmd_catalog(_args: argparse.Namespace) -> int:
|
|
16
|
+
result = run_catalog()
|
|
17
|
+
print(f"catalog: {result['chats_index']}")
|
|
18
|
+
return 0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def cmd_extract(args: argparse.Namespace) -> int:
|
|
22
|
+
result = run_extract(source_id=args.source or "")
|
|
23
|
+
active = 0
|
|
24
|
+
for sid, info in result.get("sources", {}).items():
|
|
25
|
+
if info.get("count", 0) > 0:
|
|
26
|
+
print(f"extract {sid}: {info['count']} bullets -> {info['staging']}")
|
|
27
|
+
active += 1
|
|
28
|
+
if active == 0:
|
|
29
|
+
print("extract: no new bullets extracted from active sources")
|
|
30
|
+
return 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cmd_run(_args: argparse.Namespace) -> int:
|
|
34
|
+
run_catalog()
|
|
35
|
+
result = run_extract()
|
|
36
|
+
active_sources = [v for v in result.get("sources", {}).values() if v.get("count", 0) > 0]
|
|
37
|
+
total = sum(v["count"] for v in active_sources)
|
|
38
|
+
if total > 0:
|
|
39
|
+
print(f"run: catalog refreshed, {total} staging bullets across {len(active_sources)} sources")
|
|
40
|
+
else:
|
|
41
|
+
print("run: catalog refreshed, no new staging bullets found")
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cmd_status(_args: argparse.Namespace) -> int:
|
|
46
|
+
from .store import staging_status_summary
|
|
47
|
+
|
|
48
|
+
cfg = load_ingest()
|
|
49
|
+
state = load_state()
|
|
50
|
+
rows = []
|
|
51
|
+
for src in list_sources(cfg):
|
|
52
|
+
sid = str(src["id"])
|
|
53
|
+
entry = state.get("sources", {}).get(sid, {})
|
|
54
|
+
rows.append(
|
|
55
|
+
{
|
|
56
|
+
"id": sid,
|
|
57
|
+
"kind": src.get("kind"),
|
|
58
|
+
"catalog": src.get("catalog", True),
|
|
59
|
+
"extract": src.get("extract", True),
|
|
60
|
+
"last_catalog": entry.get("last_catalog"),
|
|
61
|
+
"last_extract": entry.get("last_extract"),
|
|
62
|
+
"catalog_count": entry.get("catalog_count"),
|
|
63
|
+
"extract_count": entry.get("extract_count"),
|
|
64
|
+
"extract_capped": entry.get("extract_capped"),
|
|
65
|
+
"extract_total_before_cap": entry.get("extract_total_before_cap"),
|
|
66
|
+
"staging": entry.get("staging"),
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
payload = {
|
|
70
|
+
"state_file": str(ingest_state_path()),
|
|
71
|
+
"staging": staging_status_summary(),
|
|
72
|
+
"sources": rows,
|
|
73
|
+
}
|
|
74
|
+
print(json.dumps(payload, indent=2))
|
|
75
|
+
if payload["staging"].get("nag"):
|
|
76
|
+
print(payload["staging"]["nag"], file=sys.stderr)
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
81
|
+
parser = argparse.ArgumentParser(
|
|
82
|
+
description="Ingest pipeline: catalog references, extract to staging, distill with MCP add_memory"
|
|
83
|
+
)
|
|
84
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
85
|
+
sub.add_parser("catalog", help="Rebuild chats-index.md and entity reference cards")
|
|
86
|
+
p_extract = sub.add_parser("extract", help="Filter durable lines into staging/ingest/<id>/")
|
|
87
|
+
p_extract.add_argument("--source", help="single source id from ingest.json")
|
|
88
|
+
sub.add_parser("run", help="catalog then extract (all enabled sources)")
|
|
89
|
+
sub.add_parser("status", help="Print ingest/state.json summary")
|
|
90
|
+
return parser
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main(argv: list[str] | None = None) -> int:
|
|
94
|
+
ensure_memory_layout()
|
|
95
|
+
parser = build_parser()
|
|
96
|
+
args = parser.parse_args(argv)
|
|
97
|
+
if args.command == "catalog":
|
|
98
|
+
return cmd_catalog(args)
|
|
99
|
+
if args.command == "extract":
|
|
100
|
+
return cmd_extract(args)
|
|
101
|
+
if args.command == "run":
|
|
102
|
+
return cmd_run(args)
|
|
103
|
+
if args.command == "status":
|
|
104
|
+
return cmd_status(args)
|
|
105
|
+
parser.error(f"unknown command {args.command}")
|
|
106
|
+
return 2
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
raise SystemExit(main())
|