pi-para 0.6.6 → 0.7.0
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.
- package/ARCHITECTURE.md +4 -6
- package/CHANGELOG.md +22 -0
- package/CONFIGURATION.md +1 -16
- package/README.md +2 -13
- package/SETUP.md +1 -7
- package/STRUCTURE.md +7 -18
- package/dist/cli-legacy.d.ts +2 -1
- package/dist/cli-legacy.d.ts.map +1 -1
- package/dist/cli-legacy.js +13 -25
- package/dist/cli-legacy.js.map +1 -1
- package/dist/cli.d.ts +2 -24
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +29 -271
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +1 -28
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts +1 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -5
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +2 -1
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -64
- package/dist/index.js.map +1 -1
- package/dist/link-utils.d.ts +1 -1
- package/dist/link-utils.js +1 -1
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +9 -3
- package/dist/scheduler/index.js.map +1 -1
- package/dist/settings.d.ts +0 -3
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +0 -11
- package/dist/settings.js.map +1 -1
- package/dist/store.d.ts +11 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +48 -16
- package/dist/store.js.map +1 -1
- package/dist/tools/edit-read.d.ts +18 -0
- package/dist/tools/edit-read.d.ts.map +1 -0
- package/dist/tools/edit-read.js +161 -0
- package/dist/tools/edit-read.js.map +1 -0
- package/dist/tools/ingest.d.ts +12 -0
- package/dist/tools/ingest.d.ts.map +1 -0
- package/dist/tools/ingest.js +48 -0
- package/dist/tools/ingest.js.map +1 -0
- package/dist/tools/lint.d.ts +10 -0
- package/dist/tools/lint.d.ts.map +1 -0
- package/dist/tools/lint.js +57 -0
- package/dist/tools/lint.js.map +1 -0
- package/dist/tools/migrate.d.ts +6 -0
- package/dist/tools/migrate.d.ts.map +1 -0
- package/dist/tools/migrate.js +36 -0
- package/dist/tools/migrate.js.map +1 -0
- package/dist/tools/move.d.ts +9 -0
- package/dist/tools/move.d.ts.map +1 -0
- package/dist/tools/move.js +61 -0
- package/dist/tools/move.js.map +1 -0
- package/dist/tools/query.d.ts +12 -0
- package/dist/tools/query.d.ts.map +1 -0
- package/dist/tools/query.js +47 -0
- package/dist/tools/query.js.map +1 -0
- package/dist/tools/schemas.d.ts +109 -0
- package/dist/tools/schemas.d.ts.map +1 -0
- package/dist/tools/schemas.js +80 -0
- package/dist/tools/schemas.js.map +1 -0
- package/dist/tools/shared.d.ts +14 -0
- package/dist/tools/shared.d.ts.map +1 -0
- package/dist/tools/shared.js +25 -0
- package/dist/tools/shared.js.map +1 -0
- package/dist/tools/summarize.d.ts +8 -0
- package/dist/tools/summarize.d.ts.map +1 -0
- package/dist/tools/summarize.js +82 -0
- package/dist/tools/summarize.js.map +1 -0
- package/dist/tools/write.d.ts +23 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +215 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/tools.d.ts +1 -8
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +13 -795
- package/dist/tools.js.map +1 -1
- package/dist/wiki-tool-guidance.js +2 -2
- package/dist/wiki-tool-guidance.js.map +1 -1
- package/package.json +2 -6
- package/src/cli-legacy.ts +14 -26
- package/src/cli.ts +29 -293
- package/src/commands.ts +0 -21
- package/src/config.ts +1 -6
- package/src/context.ts +2 -1
- package/src/index.ts +5 -66
- package/src/link-utils.ts +1 -1
- package/src/scheduler/index.ts +9 -3
- package/src/settings.ts +0 -14
- package/src/store.ts +52 -15
- package/src/tools/edit-read.ts +193 -0
- package/src/tools/ingest.ts +63 -0
- package/src/tools/lint.ts +74 -0
- package/src/tools/migrate.ts +44 -0
- package/src/tools/move.ts +79 -0
- package/src/tools/query.ts +63 -0
- package/src/tools/schemas.ts +157 -0
- package/src/tools/shared.ts +35 -0
- package/src/tools/summarize.ts +89 -0
- package/src/tools/write.ts +252 -0
- package/src/tools.ts +18 -1046
- package/src/wiki-tool-guidance.ts +2 -2
- package/dist/daemon.d.ts +0 -49
- package/dist/daemon.d.ts.map +0 -1
- package/dist/daemon.js +0 -226
- package/dist/daemon.js.map +0 -1
- package/dist/link-discovery.d.ts +0 -40
- package/dist/link-discovery.d.ts.map +0 -1
- package/dist/link-discovery.js +0 -122
- package/dist/link-discovery.js.map +0 -1
- package/dist/maintainer.d.ts +0 -24
- package/dist/maintainer.d.ts.map +0 -1
- package/dist/maintainer.js +0 -358
- package/dist/maintainer.js.map +0 -1
- package/dist/watcher.d.ts +0 -25
- package/dist/watcher.d.ts.map +0 -1
- package/dist/watcher.js +0 -84
- package/dist/watcher.js.map +0 -1
- package/dist/webui/client/dist/index.html +0 -1885
- package/dist/webui/server.d.ts +0 -31
- package/dist/webui/server.d.ts.map +0 -1
- package/dist/webui/server.js +0 -566
- package/dist/webui/server.js.map +0 -1
- package/scripts/build-assets.mjs +0 -17
- package/src/daemon.ts +0 -288
- package/src/link-discovery.ts +0 -170
- package/src/maintainer.ts +0 -404
- package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
- package/src/python/minimax_lm.py +0 -198
- package/src/python/rlm_capture.py +0 -215
- package/src/watcher.ts +0 -101
- package/src/webui/client/dist/index.html +0 -1885
- package/src/webui/server.ts +0 -765
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
RLM-based session knowledge capture for pi-para.
|
|
4
|
-
|
|
5
|
-
Uses dspy.RLM with MiniMaxLM to extract knowledge from arbitrarily
|
|
6
|
-
large pi sessions. RLM handles chunking itself via sandboxed Python REPL.
|
|
7
|
-
|
|
8
|
-
Usage:
|
|
9
|
-
python3 rlm_capture.py <session_jsonl> <wiki_dir> <scope_name> [--already-captured slug1,slug2]
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import argparse
|
|
13
|
-
import json
|
|
14
|
-
import os
|
|
15
|
-
import sys
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
import dspy
|
|
19
|
-
import yaml
|
|
20
|
-
|
|
21
|
-
sys.path.insert(0, str(Path(__file__).parent))
|
|
22
|
-
from minimax_lm import MiniMaxLM
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def load_session(session_path: str) -> str:
|
|
26
|
-
"""Load and serialize a session JSONL file."""
|
|
27
|
-
entries = []
|
|
28
|
-
with open(session_path) as f:
|
|
29
|
-
for line in f:
|
|
30
|
-
line = line.strip()
|
|
31
|
-
if not line:
|
|
32
|
-
continue
|
|
33
|
-
try:
|
|
34
|
-
entries.append(json.loads(line))
|
|
35
|
-
except json.JSONDecodeError:
|
|
36
|
-
continue
|
|
37
|
-
|
|
38
|
-
parts = []
|
|
39
|
-
for entry in entries:
|
|
40
|
-
if entry.get("type") != "message":
|
|
41
|
-
if entry.get("type") == "compaction" and entry.get("summary"):
|
|
42
|
-
parts.append(f"[Compaction Summary]:\n{entry['summary']}")
|
|
43
|
-
continue
|
|
44
|
-
|
|
45
|
-
msg = entry.get("message", {})
|
|
46
|
-
role = msg.get("role", "")
|
|
47
|
-
content = msg.get("content", "")
|
|
48
|
-
if role not in ("user", "assistant", "toolResult"):
|
|
49
|
-
continue
|
|
50
|
-
|
|
51
|
-
if isinstance(content, str):
|
|
52
|
-
text = content
|
|
53
|
-
elif isinstance(content, list):
|
|
54
|
-
text_parts = []
|
|
55
|
-
tool_calls = []
|
|
56
|
-
for block in content:
|
|
57
|
-
if not isinstance(block, dict):
|
|
58
|
-
continue
|
|
59
|
-
if block.get("type") == "text" and block.get("text"):
|
|
60
|
-
text_parts.append(block["text"])
|
|
61
|
-
elif block.get("type") == "toolCall":
|
|
62
|
-
name = block.get("name", "?")
|
|
63
|
-
args_str = json.dumps(block.get("arguments", {}))
|
|
64
|
-
if len(args_str) > 300:
|
|
65
|
-
args_str = args_str[:300] + "..."
|
|
66
|
-
tool_calls.append(f"{name}({args_str})")
|
|
67
|
-
text = "\n".join(text_parts)
|
|
68
|
-
if tool_calls:
|
|
69
|
-
text += f"\n[Tools]: {'; '.join(tool_calls)}"
|
|
70
|
-
else:
|
|
71
|
-
continue
|
|
72
|
-
|
|
73
|
-
if not text.strip():
|
|
74
|
-
continue
|
|
75
|
-
if role == "toolResult" and len(text) > 1000:
|
|
76
|
-
text = text[:1000] + "\n[truncated]"
|
|
77
|
-
|
|
78
|
-
label = {"user": "User", "assistant": "Assistant", "toolResult": "Tool"}.get(role, role)
|
|
79
|
-
parts.append(f"[{label}]: {text}")
|
|
80
|
-
|
|
81
|
-
return "\n\n".join(parts)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def load_wiki_pages(wiki_dir: str, slugs: list[str]) -> str:
|
|
85
|
-
if not slugs:
|
|
86
|
-
return ""
|
|
87
|
-
pages = []
|
|
88
|
-
for slug in slugs:
|
|
89
|
-
for cat in ["projects", "areas", "resources", "archives"]:
|
|
90
|
-
path = Path(wiki_dir) / cat / f"{slug}.md"
|
|
91
|
-
if path.exists():
|
|
92
|
-
content = path.read_text()
|
|
93
|
-
if content.startswith("---"):
|
|
94
|
-
end = content.find("---", 3)
|
|
95
|
-
if end > 0:
|
|
96
|
-
content = content[end + 3:].strip()
|
|
97
|
-
pages.append(f"[[{slug}]]: {content[:2000]}")
|
|
98
|
-
break
|
|
99
|
-
return "\n\n".join(pages) if pages else ""
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def setup_lm() -> MiniMaxLM:
|
|
103
|
-
api_key = os.environ.get("MINIMAX_CN_API_KEY")
|
|
104
|
-
if not api_key:
|
|
105
|
-
config_path = Path.home() / ".config" / "qmd" / "index.yml"
|
|
106
|
-
if config_path.exists():
|
|
107
|
-
with open(config_path) as f:
|
|
108
|
-
cfg = yaml.safe_load(f) or {}
|
|
109
|
-
chat = cfg.get("providers", {}).get("chat", {})
|
|
110
|
-
if chat.get("key") and "minimaxi.com" in chat.get("url", ""):
|
|
111
|
-
api_key = chat["key"]
|
|
112
|
-
if not api_key:
|
|
113
|
-
print("Error: No MiniMax API key.", file=sys.stderr)
|
|
114
|
-
sys.exit(1)
|
|
115
|
-
|
|
116
|
-
os.environ["MINIMAX_CN_API_KEY"] = api_key
|
|
117
|
-
return MiniMaxLM(model="MiniMax-M2.7-highspeed", china=True, max_tokens=8192)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def run_capture(session_text: str, wiki_context: str, scope: str, lm: MiniMaxLM) -> dict:
|
|
121
|
-
dspy.configure(lm=lm)
|
|
122
|
-
|
|
123
|
-
rlm = dspy.RLM(
|
|
124
|
-
"context, query -> answer",
|
|
125
|
-
max_iterations=50,
|
|
126
|
-
max_llm_calls=100,
|
|
127
|
-
max_output_chars=30_000,
|
|
128
|
-
sub_lm=lm,
|
|
129
|
-
verbose=True,
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
query = f"""You have a large coding session transcript in the `context` variable ({len(session_text)} chars).
|
|
133
|
-
It's too large to process in one llm_query() call. Use Python to work with it efficiently:
|
|
134
|
-
|
|
135
|
-
1. Use Python string slicing to examine sections: context[:5000], context[5000:10000], etc.
|
|
136
|
-
2. Use Python's `in` operator or string methods to search: "keyword" in context, context.find("...")
|
|
137
|
-
3. For each interesting section found, use llm_query() on SMALL excerpts (under 20000 chars)
|
|
138
|
-
4. Accumulate findings in a Python list
|
|
139
|
-
|
|
140
|
-
{f"Already captured (skip these topics): {wiki_context}" if wiki_context else ""}
|
|
141
|
-
|
|
142
|
-
Extract ALL valuable knowledge:
|
|
143
|
-
- Architecture decisions, debugging solutions, server details
|
|
144
|
-
- Build/deploy procedures, tool configs, package names+versions
|
|
145
|
-
- File paths, conventions, operational knowledge
|
|
146
|
-
|
|
147
|
-
When done, SUBMIT a JSON array:
|
|
148
|
-
[{{"category":"projects|areas|resources|archives","slug":"name","title":"Title","scope":["{scope}"],"tags":["t1"],"body":"## Topic\\n...\\n## Key Facts\\n- ..."}}]
|
|
149
|
-
|
|
150
|
-
If nothing valuable found, SUBMIT: []"""
|
|
151
|
-
|
|
152
|
-
try:
|
|
153
|
-
result = rlm(context=session_text, query=query)
|
|
154
|
-
answer = result.answer.strip()
|
|
155
|
-
except Exception as e:
|
|
156
|
-
return {"pages": [], "error": str(e)}
|
|
157
|
-
|
|
158
|
-
# Extract JSON
|
|
159
|
-
if "```" in answer:
|
|
160
|
-
lines = answer.split("\n")
|
|
161
|
-
json_lines = []
|
|
162
|
-
in_block = False
|
|
163
|
-
for line in lines:
|
|
164
|
-
if line.strip().startswith("```"):
|
|
165
|
-
in_block = not in_block
|
|
166
|
-
continue
|
|
167
|
-
if in_block:
|
|
168
|
-
json_lines.append(line)
|
|
169
|
-
if json_lines:
|
|
170
|
-
answer = "\n".join(json_lines)
|
|
171
|
-
|
|
172
|
-
start = answer.find("[")
|
|
173
|
-
end = answer.rfind("]")
|
|
174
|
-
if start >= 0 and end > start:
|
|
175
|
-
answer = answer[start:end + 1]
|
|
176
|
-
|
|
177
|
-
try:
|
|
178
|
-
pages = json.loads(answer)
|
|
179
|
-
return {"pages": pages, "error": None} if isinstance(pages, list) else {"pages": [], "error": "not array"}
|
|
180
|
-
except json.JSONDecodeError as e:
|
|
181
|
-
return {"pages": [], "error": f"JSON: {e}\nRaw: {answer[:500]}"}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def main():
|
|
185
|
-
parser = argparse.ArgumentParser()
|
|
186
|
-
parser.add_argument("session_jsonl")
|
|
187
|
-
parser.add_argument("wiki_dir")
|
|
188
|
-
parser.add_argument("scope_name")
|
|
189
|
-
parser.add_argument("--already-captured", default="")
|
|
190
|
-
args = parser.parse_args()
|
|
191
|
-
|
|
192
|
-
lm = setup_lm()
|
|
193
|
-
|
|
194
|
-
print(f"Loading: {args.session_jsonl}", file=sys.stderr)
|
|
195
|
-
session_text = load_session(args.session_jsonl)
|
|
196
|
-
print(f"Serialized: {len(session_text)} chars", file=sys.stderr)
|
|
197
|
-
|
|
198
|
-
if not session_text.strip():
|
|
199
|
-
json.dump({"pages": [], "error": "empty"}, sys.stdout)
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
already = [s.strip() for s in args.already_captured.split(",") if s.strip()]
|
|
203
|
-
wiki_context = load_wiki_pages(args.wiki_dir, already)
|
|
204
|
-
|
|
205
|
-
print("Running RLM capture...", file=sys.stderr)
|
|
206
|
-
result = run_capture(session_text, wiki_context, args.scope_name, lm)
|
|
207
|
-
|
|
208
|
-
if result.get("error"):
|
|
209
|
-
print(f"Error: {result['error']}", file=sys.stderr)
|
|
210
|
-
print(f"Result: {len(result.get('pages', []))} pages", file=sys.stderr)
|
|
211
|
-
json.dump(result, sys.stdout, indent=2)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if __name__ == "__main__":
|
|
215
|
-
main()
|
package/src/watcher.ts
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Watch the .completed-sessions registry for new entries.
|
|
3
|
-
* Uses polling (simpler and more reliable than inotify for append-only files).
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
|
|
9
|
-
export interface CompletedSession {
|
|
10
|
-
timestamp: string;
|
|
11
|
-
sessionPath: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class RegistryWatcher {
|
|
15
|
-
private registryPath: string;
|
|
16
|
-
private lastSize: number = 0;
|
|
17
|
-
private pollInterval: ReturnType<typeof setInterval> | null = null;
|
|
18
|
-
private onNewEntries: (entries: CompletedSession[]) => void;
|
|
19
|
-
|
|
20
|
-
constructor(
|
|
21
|
-
wikiDir: string,
|
|
22
|
-
onNewEntries: (entries: CompletedSession[]) => void,
|
|
23
|
-
private intervalMs: number = 5000,
|
|
24
|
-
) {
|
|
25
|
-
this.registryPath = join(wikiDir, ".completed-sessions");
|
|
26
|
-
this.onNewEntries = onNewEntries;
|
|
27
|
-
|
|
28
|
-
// Initialize with current file size
|
|
29
|
-
if (existsSync(this.registryPath)) {
|
|
30
|
-
this.lastSize = statSync(this.registryPath).size;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Start watching for new entries. */
|
|
35
|
-
start(): void {
|
|
36
|
-
if (this.pollInterval) return;
|
|
37
|
-
this.pollInterval = setInterval(() => this.poll(), this.intervalMs);
|
|
38
|
-
console.log(`[daemon] Watching ${this.registryPath} (poll every ${this.intervalMs}ms)`);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Stop watching. */
|
|
42
|
-
stop(): void {
|
|
43
|
-
if (this.pollInterval) {
|
|
44
|
-
clearInterval(this.pollInterval);
|
|
45
|
-
this.pollInterval = null;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Check for new entries since last poll. */
|
|
50
|
-
private poll(): void {
|
|
51
|
-
if (!existsSync(this.registryPath)) return;
|
|
52
|
-
|
|
53
|
-
const currentSize = statSync(this.registryPath).size;
|
|
54
|
-
if (currentSize <= this.lastSize) return;
|
|
55
|
-
|
|
56
|
-
// Read only the new bytes
|
|
57
|
-
const content = readFileSync(this.registryPath, "utf-8");
|
|
58
|
-
const allLines = content.split("\n").filter((l) => l.trim());
|
|
59
|
-
|
|
60
|
-
// Parse lines that are after our last known position
|
|
61
|
-
// Simple approach: re-parse all lines, only emit ones we haven't seen
|
|
62
|
-
const newEntries: CompletedSession[] = [];
|
|
63
|
-
let bytesProcessed = 0;
|
|
64
|
-
|
|
65
|
-
for (const line of allLines) {
|
|
66
|
-
bytesProcessed += line.length + 1; // +1 for newline
|
|
67
|
-
if (bytesProcessed <= this.lastSize) continue;
|
|
68
|
-
|
|
69
|
-
const parts = line.split("|", 2);
|
|
70
|
-
if (parts.length === 2) {
|
|
71
|
-
newEntries.push({
|
|
72
|
-
timestamp: parts[0],
|
|
73
|
-
sessionPath: parts[1],
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
this.lastSize = currentSize;
|
|
79
|
-
|
|
80
|
-
if (newEntries.length > 0) {
|
|
81
|
-
this.onNewEntries(newEntries);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Process all entries in the registry (for manual/initial scan). */
|
|
86
|
-
getAllEntries(): CompletedSession[] {
|
|
87
|
-
if (!existsSync(this.registryPath)) return [];
|
|
88
|
-
|
|
89
|
-
const content = readFileSync(this.registryPath, "utf-8");
|
|
90
|
-
return content
|
|
91
|
-
.split("\n")
|
|
92
|
-
.filter((l) => l.trim())
|
|
93
|
-
.map((line) => {
|
|
94
|
-
const parts = line.split("|", 2);
|
|
95
|
-
return parts.length === 2
|
|
96
|
-
? { timestamp: parts[0], sessionPath: parts[1] }
|
|
97
|
-
: null;
|
|
98
|
-
})
|
|
99
|
-
.filter((e): e is CompletedSession => e !== null);
|
|
100
|
-
}
|
|
101
|
-
}
|