pi-para 0.6.7 → 0.7.1

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.
Files changed (133) hide show
  1. package/ARCHITECTURE.md +4 -6
  2. package/CHANGELOG.md +19 -0
  3. package/CONFIGURATION.md +1 -16
  4. package/README.md +2 -13
  5. package/SETUP.md +1 -7
  6. package/STRUCTURE.md +7 -18
  7. package/dist/cli-legacy.d.ts +2 -1
  8. package/dist/cli-legacy.d.ts.map +1 -1
  9. package/dist/cli-legacy.js +13 -25
  10. package/dist/cli-legacy.js.map +1 -1
  11. package/dist/cli.d.ts +2 -24
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +29 -271
  14. package/dist/cli.js.map +1 -1
  15. package/dist/commands.d.ts.map +1 -1
  16. package/dist/commands.js +1 -28
  17. package/dist/commands.js.map +1 -1
  18. package/dist/config.d.ts +1 -5
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +0 -5
  21. package/dist/config.js.map +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +6 -64
  24. package/dist/index.js.map +1 -1
  25. package/dist/link-utils.d.ts +1 -1
  26. package/dist/link-utils.js +1 -1
  27. package/dist/processor.d.ts +7 -0
  28. package/dist/processor.d.ts.map +1 -1
  29. package/dist/processor.js +29 -15
  30. package/dist/processor.js.map +1 -1
  31. package/dist/settings.d.ts +0 -3
  32. package/dist/settings.d.ts.map +1 -1
  33. package/dist/settings.js +0 -11
  34. package/dist/settings.js.map +1 -1
  35. package/dist/tools/edit-read.d.ts +18 -0
  36. package/dist/tools/edit-read.d.ts.map +1 -0
  37. package/dist/tools/edit-read.js +161 -0
  38. package/dist/tools/edit-read.js.map +1 -0
  39. package/dist/tools/ingest.d.ts +12 -0
  40. package/dist/tools/ingest.d.ts.map +1 -0
  41. package/dist/tools/ingest.js +48 -0
  42. package/dist/tools/ingest.js.map +1 -0
  43. package/dist/tools/lint.d.ts +10 -0
  44. package/dist/tools/lint.d.ts.map +1 -0
  45. package/dist/tools/lint.js +57 -0
  46. package/dist/tools/lint.js.map +1 -0
  47. package/dist/tools/migrate.d.ts +6 -0
  48. package/dist/tools/migrate.d.ts.map +1 -0
  49. package/dist/tools/migrate.js +36 -0
  50. package/dist/tools/migrate.js.map +1 -0
  51. package/dist/tools/move.d.ts +9 -0
  52. package/dist/tools/move.d.ts.map +1 -0
  53. package/dist/tools/move.js +61 -0
  54. package/dist/tools/move.js.map +1 -0
  55. package/dist/tools/query.d.ts +12 -0
  56. package/dist/tools/query.d.ts.map +1 -0
  57. package/dist/tools/query.js +47 -0
  58. package/dist/tools/query.js.map +1 -0
  59. package/dist/tools/schemas.d.ts +109 -0
  60. package/dist/tools/schemas.d.ts.map +1 -0
  61. package/dist/tools/schemas.js +80 -0
  62. package/dist/tools/schemas.js.map +1 -0
  63. package/dist/tools/shared.d.ts +14 -0
  64. package/dist/tools/shared.d.ts.map +1 -0
  65. package/dist/tools/shared.js +25 -0
  66. package/dist/tools/shared.js.map +1 -0
  67. package/dist/tools/summarize.d.ts +8 -0
  68. package/dist/tools/summarize.d.ts.map +1 -0
  69. package/dist/tools/summarize.js +82 -0
  70. package/dist/tools/summarize.js.map +1 -0
  71. package/dist/tools/write.d.ts +23 -0
  72. package/dist/tools/write.d.ts.map +1 -0
  73. package/dist/tools/write.js +215 -0
  74. package/dist/tools/write.js.map +1 -0
  75. package/dist/tools.d.ts +1 -8
  76. package/dist/tools.d.ts.map +1 -1
  77. package/dist/tools.js +13 -795
  78. package/dist/tools.js.map +1 -1
  79. package/dist/wiki.d.ts.map +1 -1
  80. package/dist/wiki.js +1 -0
  81. package/dist/wiki.js.map +1 -1
  82. package/package.json +2 -6
  83. package/src/cli-legacy.ts +14 -26
  84. package/src/cli.ts +29 -293
  85. package/src/commands.ts +0 -21
  86. package/src/config.ts +1 -6
  87. package/src/index.ts +5 -66
  88. package/src/link-utils.ts +1 -1
  89. package/src/processor.ts +28 -13
  90. package/src/settings.ts +0 -14
  91. package/src/tools/edit-read.ts +193 -0
  92. package/src/tools/ingest.ts +63 -0
  93. package/src/tools/lint.ts +74 -0
  94. package/src/tools/migrate.ts +44 -0
  95. package/src/tools/move.ts +79 -0
  96. package/src/tools/query.ts +63 -0
  97. package/src/tools/schemas.ts +157 -0
  98. package/src/tools/shared.ts +35 -0
  99. package/src/tools/summarize.ts +89 -0
  100. package/src/tools/write.ts +252 -0
  101. package/src/tools.ts +18 -1046
  102. package/src/wiki.ts +1 -0
  103. package/dist/daemon.d.ts +0 -49
  104. package/dist/daemon.d.ts.map +0 -1
  105. package/dist/daemon.js +0 -226
  106. package/dist/daemon.js.map +0 -1
  107. package/dist/link-discovery.d.ts +0 -40
  108. package/dist/link-discovery.d.ts.map +0 -1
  109. package/dist/link-discovery.js +0 -122
  110. package/dist/link-discovery.js.map +0 -1
  111. package/dist/maintainer.d.ts +0 -24
  112. package/dist/maintainer.d.ts.map +0 -1
  113. package/dist/maintainer.js +0 -358
  114. package/dist/maintainer.js.map +0 -1
  115. package/dist/watcher.d.ts +0 -25
  116. package/dist/watcher.d.ts.map +0 -1
  117. package/dist/watcher.js +0 -84
  118. package/dist/watcher.js.map +0 -1
  119. package/dist/webui/client/dist/index.html +0 -1885
  120. package/dist/webui/server.d.ts +0 -31
  121. package/dist/webui/server.d.ts.map +0 -1
  122. package/dist/webui/server.js +0 -566
  123. package/dist/webui/server.js.map +0 -1
  124. package/scripts/build-assets.mjs +0 -17
  125. package/src/daemon.ts +0 -288
  126. package/src/link-discovery.ts +0 -170
  127. package/src/maintainer.ts +0 -404
  128. package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
  129. package/src/python/minimax_lm.py +0 -198
  130. package/src/python/rlm_capture.py +0 -215
  131. package/src/watcher.ts +0 -101
  132. package/src/webui/client/dist/index.html +0 -1885
  133. 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
- }