recallnest 1.4.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 (261) hide show
  1. package/.claude-plugin/CLAUDE.md +29 -0
  2. package/.claude-plugin/marketplace.json +18 -0
  3. package/.env.example +3 -0
  4. package/.github/workflows/ci.yml +28 -0
  5. package/.mcp.json +9 -0
  6. package/CHANGELOG.md +51 -0
  7. package/CLAUDE.md +42 -0
  8. package/DEMO.md +94 -0
  9. package/EVAL.md +89 -0
  10. package/FAILURES.md +75 -0
  11. package/LICENSE +21 -0
  12. package/OPERATIONS.md +59 -0
  13. package/README.md +427 -0
  14. package/README_CN.md +425 -0
  15. package/ROADMAP.md +198 -0
  16. package/assets/recallnest-workbench.png +0 -0
  17. package/assets/ui/app.js +807 -0
  18. package/assets/ui/index.html +154 -0
  19. package/assets/ui/styles.css +605 -0
  20. package/assets/wechat-qr.jpg +0 -0
  21. package/config.json.example +38 -0
  22. package/daily-recall.sh +87 -0
  23. package/daily-reflections/2026-03-05.md +15 -0
  24. package/daily-reflections/2026-03-06.md +15 -0
  25. package/daily-reflections/2026-03-07.md +7 -0
  26. package/daily-reflections/2026-03-08.md +34 -0
  27. package/daily-reflections/2026-03-09.md +35 -0
  28. package/daily-reflections/2026-03-10.md +36 -0
  29. package/daily-reflections/2026-03-11.md +35 -0
  30. package/daily-reflections/2026-03-12.md +36 -0
  31. package/daily-reflections/2026-03-13.md +35 -0
  32. package/daily-reflections/2026-03-14.md +35 -0
  33. package/daily-reflections/2026-03-15.md +35 -0
  34. package/daily-reflections/2026-03-16.md +41 -0
  35. package/daily-reflections/2026-03-17.md +10 -0
  36. package/daily-reflections/2026-03-18.md +10 -0
  37. package/daily-reflections/2026-03-19.md +10 -0
  38. package/daily-reflections/2026-03-20.md +10 -0
  39. package/daily-reflections/2026-03-21.md +10 -0
  40. package/daily-reflections/2026-03-22.md +10 -0
  41. package/daily-reflections/2026-03-23.md +10 -0
  42. package/daily-reflections/2026-03-24.md +10 -0
  43. package/daily-reflections/2026-03-25.md +32 -0
  44. package/daily-reflections/2026-03-26.md +37 -0
  45. package/daily-reflections/2026-03-27.md +35 -0
  46. package/daily-reflections/2026-03-28.md +35 -0
  47. package/daily-reflections/2026-03-29.md +35 -0
  48. package/daily-reflections/2026-03-30.md +36 -0
  49. package/daily-reflections/2026-03-31.md +35 -0
  50. package/daily-reflections/2026-04-01.md +35 -0
  51. package/docs/api-reference.md +1291 -0
  52. package/docs/architecture.md +168 -0
  53. package/docs/memory-boundary-contract.md +226 -0
  54. package/docs/memory-categories.md +119 -0
  55. package/docs/pipeline-audit-ultramemory.md +177 -0
  56. package/docs/plans/2026-03-08-query-expansion.md +270 -0
  57. package/docs/plans/2026-03-16-continuity-layer.md +196 -0
  58. package/docs/plans/mcp-tiering-plan.md +126 -0
  59. package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
  60. package/docs/self-evolution.md +198 -0
  61. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
  62. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
  63. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
  64. package/eval/cases.json +85 -0
  65. package/eval/continuity/case-seeds.json +90 -0
  66. package/eval/continuity/cases.json +302 -0
  67. package/eval/continuity/memory-seeds.json +26 -0
  68. package/eval/continuity/pattern-seeds.json +100 -0
  69. package/eval/continuity/results-2026-03-16.md +117 -0
  70. package/eval/continuity/results-2026-03-19.md +453 -0
  71. package/eval/continuity/runbook.md +110 -0
  72. package/eval/continuity/seed-cases.md +54 -0
  73. package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
  74. package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
  75. package/eval/reports/2026-03-06-baseline.md +82 -0
  76. package/eval/reports/query-expansion.md +138 -0
  77. package/integrations/README.md +57 -0
  78. package/integrations/claude-code/README.md +133 -0
  79. package/integrations/claude-code/claude-md-snippet.md +31 -0
  80. package/integrations/claude-code/setup.sh +66 -0
  81. package/integrations/claude-code/smoke-continuity.sh +452 -0
  82. package/integrations/codex/README.md +49 -0
  83. package/integrations/codex/agents-md-snippet.md +31 -0
  84. package/integrations/codex/setup.sh +58 -0
  85. package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
  86. package/integrations/examples/langchain/memory-chain.py +111 -0
  87. package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
  88. package/integrations/gemini-cli/README.md +57 -0
  89. package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
  90. package/integrations/gemini-cli/setup.sh +64 -0
  91. package/integrations/shared/install-managed-block.sh +51 -0
  92. package/lm +19 -0
  93. package/package.json +50 -0
  94. package/plan-v1.2-distributable.md +105 -0
  95. package/scripts/archive-distilled-junk.ts +116 -0
  96. package/scripts/backfill-l0l1l2.ts +98 -0
  97. package/scripts/blind-scan.ts +588 -0
  98. package/scripts/cleanup-step1-exact-dups.ts +117 -0
  99. package/scripts/cleanup-step2-template-dups.ts +113 -0
  100. package/scripts/cleanup-step3-stale-content.ts +149 -0
  101. package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
  102. package/scripts/delete-distilled-junk.ts +88 -0
  103. package/scripts/distill-facts.ts +305 -0
  104. package/scripts/health-check.ts +343 -0
  105. package/scripts/incremental-ingest.sh +71 -0
  106. package/scripts/mlp-health-check.ts +382 -0
  107. package/scripts/pull-claude-desktop.ts +515 -0
  108. package/scripts/re-ingest-cc.sh +66 -0
  109. package/scripts/rebalance-memory-health.ts +181 -0
  110. package/scripts/run-desktop-ingest.sh +11 -0
  111. package/scripts/start-server.sh +12 -0
  112. package/scripts/viz-dashboard.html +423 -0
  113. package/scripts/viz-export.ts +703 -0
  114. package/src/__tests__/access-tracker-novelty.test.ts +228 -0
  115. package/src/__tests__/auto-consolidation.test.ts +168 -0
  116. package/src/__tests__/auto-recall.test.ts +131 -0
  117. package/src/__tests__/batch-dedup.test.ts +65 -0
  118. package/src/__tests__/capture-engine.test.ts +1142 -0
  119. package/src/__tests__/capture-heuristic.test.ts +171 -0
  120. package/src/__tests__/cascade-forget.test.ts +210 -0
  121. package/src/__tests__/category-min-scores.test.ts +33 -0
  122. package/src/__tests__/confidence-tracker.test.ts +196 -0
  123. package/src/__tests__/conflict-advisor.test.ts +206 -0
  124. package/src/__tests__/conflict-escalation.test.ts +149 -0
  125. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  126. package/src/__tests__/conflict-output.test.ts +85 -0
  127. package/src/__tests__/conflict-store.test.ts +374 -0
  128. package/src/__tests__/consolidation-engine.test.ts +181 -0
  129. package/src/__tests__/context-composer-pins.test.ts +64 -0
  130. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  131. package/src/__tests__/context-renderer.test.ts +91 -0
  132. package/src/__tests__/continuity-eval.test.ts +275 -0
  133. package/src/__tests__/continuity-schema.test.ts +97 -0
  134. package/src/__tests__/core-recall-eval.test.ts +235 -0
  135. package/src/__tests__/core-summary.test.ts +109 -0
  136. package/src/__tests__/dedup-actions.test.ts +58 -0
  137. package/src/__tests__/deterministic-id.test.ts +39 -0
  138. package/src/__tests__/doctor.test.ts +303 -0
  139. package/src/__tests__/drill-down.test.ts +108 -0
  140. package/src/__tests__/embedder.test.ts +122 -0
  141. package/src/__tests__/entity-resolver.test.ts +58 -0
  142. package/src/__tests__/event-segmenter.test.ts +153 -0
  143. package/src/__tests__/hotness-blend.test.ts +76 -0
  144. package/src/__tests__/ingest.test.ts +360 -0
  145. package/src/__tests__/kg-extractor.test.ts +184 -0
  146. package/src/__tests__/kg-store.test.ts +198 -0
  147. package/src/__tests__/llm-consolidation.test.ts +226 -0
  148. package/src/__tests__/memory-boundaries.test.ts +237 -0
  149. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  150. package/src/__tests__/memory-output.test.ts +215 -0
  151. package/src/__tests__/multi-vector.test.ts +242 -0
  152. package/src/__tests__/ppr-traversal.test.ts +256 -0
  153. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  154. package/src/__tests__/preference-matcher.test.ts +230 -0
  155. package/src/__tests__/preference-slots.test.ts +106 -0
  156. package/src/__tests__/prospective-memory.test.ts +283 -0
  157. package/src/__tests__/query-entity-detector.test.ts +171 -0
  158. package/src/__tests__/query-expander.test.ts +34 -0
  159. package/src/__tests__/recall-mode.test.ts +64 -0
  160. package/src/__tests__/result-synthesizer.test.ts +198 -0
  161. package/src/__tests__/resume-context.test.ts +4743 -0
  162. package/src/__tests__/retrieval-trace.test.ts +213 -0
  163. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  164. package/src/__tests__/retroactive-boost.test.ts +223 -0
  165. package/src/__tests__/scope-inventory.test.ts +200 -0
  166. package/src/__tests__/scope-policy.test.ts +69 -0
  167. package/src/__tests__/session-checkpoint.test.ts +259 -0
  168. package/src/__tests__/session-gc.test.ts +215 -0
  169. package/src/__tests__/store-scope-audit.test.ts +81 -0
  170. package/src/__tests__/temporal-parser.test.ts +241 -0
  171. package/src/__tests__/term-registry.test.ts +121 -0
  172. package/src/__tests__/tool-output-compressor.test.ts +133 -0
  173. package/src/__tests__/version-manager.test.ts +213 -0
  174. package/src/__tests__/workflow-observation.test.ts +400 -0
  175. package/src/access-tracker.ts +316 -0
  176. package/src/adaptive-retrieval.ts +64 -0
  177. package/src/api-server.ts +667 -0
  178. package/src/asset-sync.ts +79 -0
  179. package/src/auto-consolidation.ts +133 -0
  180. package/src/auto-gc.ts +124 -0
  181. package/src/auto-recall.ts +95 -0
  182. package/src/capture-engine.ts +931 -0
  183. package/src/capture-heuristic.ts +177 -0
  184. package/src/cascade-forget.ts +135 -0
  185. package/src/chunker.ts +253 -0
  186. package/src/cli.ts +1917 -0
  187. package/src/confidence-tracker.ts +166 -0
  188. package/src/conflict-advisor.ts +433 -0
  189. package/src/conflict-engine.ts +181 -0
  190. package/src/conflict-escalation.ts +138 -0
  191. package/src/conflict-lifecycle.ts +67 -0
  192. package/src/conflict-output.ts +218 -0
  193. package/src/conflict-schema.ts +130 -0
  194. package/src/conflict-store.ts +127 -0
  195. package/src/consolidation-engine.ts +256 -0
  196. package/src/context-composer-pins.ts +183 -0
  197. package/src/context-composer-queries.ts +121 -0
  198. package/src/context-composer-scope.ts +56 -0
  199. package/src/context-composer-stable-selection.ts +200 -0
  200. package/src/context-composer-stable.ts +217 -0
  201. package/src/context-composer-task-fallbacks.ts +36 -0
  202. package/src/context-composer-task-ranking.ts +197 -0
  203. package/src/context-composer-task-results.ts +137 -0
  204. package/src/context-composer-task-selection.ts +342 -0
  205. package/src/context-composer-text.ts +57 -0
  206. package/src/context-composer.ts +300 -0
  207. package/src/context-renderer.ts +179 -0
  208. package/src/decay-engine.ts +189 -0
  209. package/src/doctor.ts +543 -0
  210. package/src/embedder.ts +557 -0
  211. package/src/entity-resolver.ts +131 -0
  212. package/src/eval.ts +632 -0
  213. package/src/event-segmenter.ts +216 -0
  214. package/src/ingest.ts +1822 -0
  215. package/src/kg-extractor.ts +217 -0
  216. package/src/kg-store.ts +353 -0
  217. package/src/legacy-scope-review.ts +53 -0
  218. package/src/llm-client.ts +629 -0
  219. package/src/llm-consolidation.ts +168 -0
  220. package/src/mcp-server.ts +1337 -0
  221. package/src/memory-assets.ts +475 -0
  222. package/src/memory-boundaries.ts +354 -0
  223. package/src/memory-health-rebalance.ts +220 -0
  224. package/src/memory-output.ts +474 -0
  225. package/src/memory-schema.ts +184 -0
  226. package/src/multi-vector.ts +234 -0
  227. package/src/noise-filter.ts +155 -0
  228. package/src/ppr-traversal.ts +279 -0
  229. package/src/preference-matcher.ts +174 -0
  230. package/src/preference-slots.ts +259 -0
  231. package/src/prospective-memory.ts +198 -0
  232. package/src/query-entity-detector.ts +207 -0
  233. package/src/query-expander.ts +98 -0
  234. package/src/result-synthesizer.ts +114 -0
  235. package/src/retrieval-profiles.ts +101 -0
  236. package/src/retrieval-trace.ts +94 -0
  237. package/src/retriever.ts +1338 -0
  238. package/src/retroactive-boost.ts +154 -0
  239. package/src/rif.ts +73 -0
  240. package/src/runtime-config.ts +172 -0
  241. package/src/schema-utils.ts +56 -0
  242. package/src/scope-inventory.ts +310 -0
  243. package/src/scope-policy.ts +142 -0
  244. package/src/session-engine.ts +168 -0
  245. package/src/session-output.ts +96 -0
  246. package/src/session-schema.ts +60 -0
  247. package/src/session-store.ts +293 -0
  248. package/src/stderr-log.ts +16 -0
  249. package/src/store.ts +903 -0
  250. package/src/temporal-parser.ts +318 -0
  251. package/src/term-registry.ts +791 -0
  252. package/src/tool-output-compressor.ts +115 -0
  253. package/src/tracker.ts +57 -0
  254. package/src/ui-server.ts +430 -0
  255. package/src/version-manager.ts +172 -0
  256. package/src/workflow-observation-engine.ts +316 -0
  257. package/src/workflow-observation-managed.ts +83 -0
  258. package/src/workflow-observation-output.ts +86 -0
  259. package/src/workflow-observation-schema.ts +87 -0
  260. package/src/workflow-observation-scope-review.ts +409 -0
  261. package/src/workflow-observation-store.ts +69 -0
@@ -0,0 +1,515 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * pull-claude-desktop.ts
4
+ *
5
+ * 从 claude.ai 拉取 Claude Desktop / 网页版对话历史,
6
+ * 转成 CC transcript .jsonl 格式,供 `lm ingest --source desktop` 消费。
7
+ *
8
+ * 用法:
9
+ * CLAUDE_SESSION_KEY="sk-ant-..." bun scripts/pull-claude-desktop.ts
10
+ * CLAUDE_SESSION_KEY="sk-ant-..." bun scripts/pull-claude-desktop.ts --limit 10
11
+ * CLAUDE_SESSION_KEY="sk-ant-..." bun scripts/pull-claude-desktop.ts --since 2025-01-01
12
+ */
13
+
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { parseArgs } from "node:util";
17
+
18
+ // ============================================================================
19
+ // Config
20
+ // ============================================================================
21
+
22
+ const BASE_URL = "https://claude.ai";
23
+ const PAGE_SIZE = 50;
24
+ const THROTTLE_MS = 200;
25
+ const MAX_RETRIES = 3;
26
+ const OUTPUT_DIR = join(import.meta.dir, "..", "data", "desktop-import");
27
+ const SYNC_STATE_PATH = join(OUTPUT_DIR, ".sync-state.json");
28
+
29
+ // ============================================================================
30
+ // Types
31
+ // ============================================================================
32
+
33
+ interface SyncState {
34
+ lastSyncedAt: string;
35
+ conversations: Record<
36
+ string,
37
+ {
38
+ messageCount: number;
39
+ updatedAt: string;
40
+ pulledAt: string;
41
+ name: string;
42
+ }
43
+ >;
44
+ }
45
+
46
+ interface ConversationSummary {
47
+ uuid: string;
48
+ name: string;
49
+ created_at: string;
50
+ updated_at: string;
51
+ }
52
+
53
+ interface ChatMessage {
54
+ uuid: string;
55
+ text: string;
56
+ sender: "human" | "assistant";
57
+ index: number;
58
+ created_at: string;
59
+ parent_message_uuid: string;
60
+ content: Array<{
61
+ type: "text" | "thinking" | "tool_use" | "tool_result";
62
+ text?: string;
63
+ name?: string;
64
+ input?: unknown;
65
+ }>;
66
+ attachments?: Array<{
67
+ file_name: string;
68
+ file_type?: string;
69
+ file_size?: number;
70
+ }>;
71
+ }
72
+
73
+ interface Conversation {
74
+ uuid: string;
75
+ name: string;
76
+ created_at: string;
77
+ updated_at: string;
78
+ chat_messages: ChatMessage[];
79
+ }
80
+
81
+ // ============================================================================
82
+ // HTTP Client
83
+ // ============================================================================
84
+
85
+ class ClaudeApiClient {
86
+ private sessionKey: string;
87
+ private orgId: string | null = null;
88
+ private throttleMs = THROTTLE_MS;
89
+
90
+ constructor(sessionKey: string) {
91
+ this.sessionKey = sessionKey;
92
+ }
93
+
94
+ private async request(path: string): Promise<any> {
95
+ let lastError: Error | null = null;
96
+
97
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
98
+ try {
99
+ const res = await fetch(`${BASE_URL}${path}`, {
100
+ headers: {
101
+ Cookie: `sessionKey=${this.sessionKey}`,
102
+ "Content-Type": "application/json",
103
+ Accept: "application/json",
104
+ },
105
+ });
106
+
107
+ if (res.status === 401 || res.status === 403) {
108
+ throw new Error(
109
+ `认证失败 (${res.status})。sessionKey 可能已过期,请从浏览器重新获取。`,
110
+ );
111
+ }
112
+
113
+ if (res.status === 429) {
114
+ this.throttleMs = Math.min(this.throttleMs * 2, 5000);
115
+ console.log(` ⚠️ 限流,等待 ${this.throttleMs}ms...`);
116
+ await sleep(this.throttleMs);
117
+ continue;
118
+ }
119
+
120
+ if (res.status >= 500) {
121
+ const waitMs = Math.pow(2, attempt) * 1000;
122
+ console.log(
123
+ ` ⚠️ 服务器错误 ${res.status},${waitMs}ms 后重试...`,
124
+ );
125
+ await sleep(waitMs);
126
+ continue;
127
+ }
128
+
129
+ if (!res.ok) {
130
+ throw new Error(`HTTP ${res.status}: ${await res.text()}`);
131
+ }
132
+
133
+ // Reset throttle on success
134
+ this.throttleMs = THROTTLE_MS;
135
+ return await res.json();
136
+ } catch (err: any) {
137
+ if (
138
+ err.message?.includes("认证失败") ||
139
+ err.message?.startsWith("HTTP ")
140
+ ) {
141
+ throw err;
142
+ }
143
+ lastError = err;
144
+ const waitMs = Math.pow(2, attempt) * 1000;
145
+ console.log(` ⚠️ 网络错误: ${err.message},${waitMs}ms 后重试...`);
146
+ await sleep(waitMs);
147
+ }
148
+ }
149
+
150
+ throw lastError || new Error("请求失败,已重试 3 次");
151
+ }
152
+
153
+ async getOrgId(): Promise<string> {
154
+ if (this.orgId) return this.orgId;
155
+ const orgs = await this.request("/api/organizations");
156
+ if (!Array.isArray(orgs) || orgs.length === 0) {
157
+ throw new Error("获取 organization 失败。请确认 sessionKey 有效。");
158
+ }
159
+ this.orgId = orgs[0].uuid;
160
+ return this.orgId!;
161
+ }
162
+
163
+ async listConversations(
164
+ offset: number = 0,
165
+ ): Promise<ConversationSummary[]> {
166
+ const orgId = await this.getOrgId();
167
+ return await this.request(
168
+ `/api/organizations/${orgId}/chat_conversations?limit=${PAGE_SIZE}&offset=${offset}`,
169
+ );
170
+ }
171
+
172
+ async getConversation(chatId: string): Promise<Conversation> {
173
+ const orgId = await this.getOrgId();
174
+ return await this.request(
175
+ `/api/organizations/${orgId}/chat_conversations/${chatId}?tree=True&rendering_mode=messages&render_all_tools=true`,
176
+ );
177
+ }
178
+
179
+ async throttle(): Promise<void> {
180
+ await sleep(this.throttleMs);
181
+ }
182
+ }
183
+
184
+ // ============================================================================
185
+ // Branch Extraction
186
+ // ============================================================================
187
+
188
+ function extractLatestBranch(messages: ChatMessage[]): ChatMessage[] {
189
+ if (messages.length === 0) return [];
190
+
191
+ const byUuid = new Map<string, ChatMessage>();
192
+ for (const msg of messages) {
193
+ byUuid.set(msg.uuid, msg);
194
+ }
195
+
196
+ // Find all parent UUIDs
197
+ const parentUuids = new Set<string>();
198
+ for (const msg of messages) {
199
+ if (msg.parent_message_uuid) {
200
+ parentUuids.add(msg.parent_message_uuid);
201
+ }
202
+ }
203
+
204
+ // Leaves = messages that are not anyone's parent
205
+ const leaves = messages.filter((m) => !parentUuids.has(m.uuid));
206
+ if (leaves.length === 0) return messages;
207
+
208
+ // Pick the leaf with the latest timestamp
209
+ const latestLeaf = leaves.reduce((a, b) =>
210
+ new Date(a.created_at).getTime() >= new Date(b.created_at).getTime()
211
+ ? a
212
+ : b,
213
+ );
214
+
215
+ // Walk backwards from leaf to root
216
+ const branch: ChatMessage[] = [];
217
+ let current: ChatMessage | undefined = latestLeaf;
218
+ const visited = new Set<string>();
219
+
220
+ while (current && !visited.has(current.uuid)) {
221
+ visited.add(current.uuid);
222
+ branch.push(current);
223
+ current = current.parent_message_uuid
224
+ ? byUuid.get(current.parent_message_uuid)
225
+ : undefined;
226
+ }
227
+
228
+ branch.reverse();
229
+ return branch;
230
+ }
231
+
232
+ // ============================================================================
233
+ // Format Conversion
234
+ // ============================================================================
235
+
236
+ function messageToText(msg: ChatMessage): string {
237
+ const parts: string[] = [];
238
+
239
+ if (Array.isArray(msg.content) && msg.content.length > 0) {
240
+ for (const block of msg.content) {
241
+ if (block.type === "text" && block.text) {
242
+ parts.push(block.text);
243
+ }
244
+ // Skip thinking, tool_use, tool_result — noise for memory
245
+ }
246
+ } else if (msg.text) {
247
+ // Fallback: use raw text field
248
+ parts.push(msg.text);
249
+ }
250
+
251
+ // Mark attachments
252
+ if (Array.isArray(msg.attachments)) {
253
+ for (const att of msg.attachments) {
254
+ parts.push(`[Attachment: ${att.file_name}]`);
255
+ }
256
+ }
257
+
258
+ return parts.join("\n").trim();
259
+ }
260
+
261
+ function convertToCCTranscript(
262
+ conversation: Conversation,
263
+ ): string[] {
264
+ const branch = extractLatestBranch(conversation.chat_messages || []);
265
+ const lines: string[] = [];
266
+
267
+ for (const msg of branch) {
268
+ const text = messageToText(msg);
269
+ if (!text || text.length < 5) continue;
270
+
271
+ const type = msg.sender === "human" ? "user" : "assistant";
272
+ const entry = {
273
+ type,
274
+ message: {
275
+ role: type,
276
+ content: text,
277
+ },
278
+ sessionId: conversation.uuid,
279
+ timestamp: msg.created_at,
280
+ uuid: msg.uuid,
281
+ parentUuid: msg.parent_message_uuid || undefined,
282
+ };
283
+
284
+ lines.push(JSON.stringify(entry));
285
+ }
286
+
287
+ return lines;
288
+ }
289
+
290
+ // ============================================================================
291
+ // Sync State
292
+ // ============================================================================
293
+
294
+ function loadSyncState(): SyncState {
295
+ if (existsSync(SYNC_STATE_PATH)) {
296
+ try {
297
+ return JSON.parse(readFileSync(SYNC_STATE_PATH, "utf-8"));
298
+ } catch {
299
+ // Corrupted, start fresh
300
+ }
301
+ }
302
+ return { lastSyncedAt: "", conversations: {} };
303
+ }
304
+
305
+ function saveSyncState(state: SyncState): void {
306
+ writeFileSync(SYNC_STATE_PATH, JSON.stringify(state, null, 2));
307
+ }
308
+
309
+ function needsPull(
310
+ state: SyncState,
311
+ conv: ConversationSummary,
312
+ ): boolean {
313
+ const existing = state.conversations[conv.uuid];
314
+ if (!existing) return true;
315
+ // Re-pull if conversation was updated since last pull
316
+ return conv.updated_at !== existing.updatedAt;
317
+ }
318
+
319
+ // ============================================================================
320
+ // Main
321
+ // ============================================================================
322
+
323
+ function sleep(ms: number): Promise<void> {
324
+ return new Promise((resolve) => setTimeout(resolve, ms));
325
+ }
326
+
327
+ async function main() {
328
+ const { values } = parseArgs({
329
+ args: process.argv.slice(2),
330
+ options: {
331
+ "session-key": { type: "string" },
332
+ limit: { type: "string" },
333
+ since: { type: "string" },
334
+ verbose: { type: "boolean", default: false },
335
+ },
336
+ strict: false,
337
+ });
338
+
339
+ const sessionKey =
340
+ values["session-key"] ||
341
+ process.env.CLAUDE_SESSION_KEY ||
342
+ process.env.CLAUDE_DESKTOP_SESSION_KEY;
343
+
344
+ if (!sessionKey) {
345
+ console.error(
346
+ "❌ 需要 sessionKey。用法:\n" +
347
+ " CLAUDE_SESSION_KEY='...' bun scripts/pull-claude-desktop.ts\n\n" +
348
+ "获取方式:浏览器登录 claude.ai → DevTools → Application → Cookies → sessionKey",
349
+ );
350
+ process.exit(1);
351
+ }
352
+
353
+ const limit = values.limit ? parseInt(values.limit, 10) : Infinity;
354
+ const sinceDate = values.since ? new Date(values.since) : null;
355
+ const verbose = values.verbose ?? false;
356
+
357
+ // Ensure output directory
358
+ mkdirSync(OUTPUT_DIR, { recursive: true });
359
+
360
+ const client = new ClaudeApiClient(sessionKey);
361
+ const state = loadSyncState();
362
+
363
+ // Step 1: Verify auth
364
+ console.log("🔑 验证 sessionKey...");
365
+ try {
366
+ const orgId = await client.getOrgId();
367
+ console.log(` ✅ 认证成功 (org: ${orgId.slice(0, 8)}...)`);
368
+ } catch (err: any) {
369
+ console.error(` ❌ ${err.message}`);
370
+ process.exit(1);
371
+ }
372
+
373
+ // Step 2: List all conversations (paginated)
374
+ console.log("\n📋 获取对话列表...");
375
+ const allConversations: ConversationSummary[] = [];
376
+ let offset = 0;
377
+ let pageCount = 0;
378
+
379
+ while (true) {
380
+ const page = await client.listConversations(offset);
381
+ if (!Array.isArray(page) || page.length === 0) break;
382
+
383
+ // Filter by --since if specified
384
+ for (const conv of page) {
385
+ if (sinceDate && new Date(conv.updated_at) < sinceDate) {
386
+ // API returns newest first; once we hit older, stop
387
+ console.log(
388
+ ` ⏭️ 跳过 ${conv.updated_at} 之前的对话 (--since ${values.since})`,
389
+ );
390
+ break;
391
+ }
392
+ allConversations.push(conv);
393
+ }
394
+
395
+ pageCount++;
396
+ process.stdout.write(
397
+ ` 已扫描 ${allConversations.length} 条对话 (第 ${pageCount} 页)...\r`,
398
+ );
399
+
400
+ // Stop if we hit the since boundary or got a short page
401
+ if (sinceDate) {
402
+ const lastOnPage = page[page.length - 1];
403
+ if (new Date(lastOnPage.updated_at) < sinceDate) break;
404
+ }
405
+ if (page.length < PAGE_SIZE) break;
406
+
407
+ offset += PAGE_SIZE;
408
+ await client.throttle();
409
+ }
410
+
411
+ console.log(` ✅ 共发现 ${allConversations.length} 条对话`);
412
+
413
+ // Step 3: Filter — skip already-synced
414
+ const toPull = allConversations.filter((c) => needsPull(state, c));
415
+ const toSkip = allConversations.length - toPull.length;
416
+
417
+ if (toSkip > 0) {
418
+ console.log(` ⏭️ ${toSkip} 条已同步,跳过`);
419
+ }
420
+
421
+ // Apply --limit
422
+ const batch = toPull.slice(0, limit);
423
+ if (batch.length === 0) {
424
+ console.log("\n✅ 没有需要拉取的新对话。");
425
+ return;
426
+ }
427
+
428
+ console.log(`\n🔄 开始拉取 ${batch.length} 条对话...\n`);
429
+
430
+ // Step 4: Pull each conversation
431
+ let pulled = 0;
432
+ let skipped = 0;
433
+ let failed = 0;
434
+ const errors: string[] = [];
435
+
436
+ for (let i = 0; i < batch.length; i++) {
437
+ const conv = batch[i];
438
+ const label = conv.name?.slice(0, 40) || conv.uuid.slice(0, 8);
439
+
440
+ try {
441
+ process.stdout.write(
442
+ ` [${i + 1}/${batch.length}] ${label}...`,
443
+ );
444
+
445
+ const full = await client.getConversation(conv.uuid);
446
+ const lines = convertToCCTranscript(full);
447
+
448
+ if (lines.length === 0) {
449
+ console.log(` ⏭️ 空对话`);
450
+ skipped++;
451
+ } else {
452
+ const outPath = join(OUTPUT_DIR, `${conv.uuid}.jsonl`);
453
+ writeFileSync(outPath, lines.join("\n") + "\n");
454
+ console.log(` ✅ ${lines.length} 条消息`);
455
+ pulled++;
456
+ }
457
+
458
+ // Update sync state
459
+ state.conversations[conv.uuid] = {
460
+ messageCount: lines.length,
461
+ updatedAt: conv.updated_at,
462
+ pulledAt: new Date().toISOString(),
463
+ name: conv.name || "",
464
+ };
465
+
466
+ // Save state periodically (every 10 conversations)
467
+ if ((i + 1) % 10 === 0) {
468
+ state.lastSyncedAt = new Date().toISOString();
469
+ saveSyncState(state);
470
+ }
471
+
472
+ await client.throttle();
473
+ } catch (err: any) {
474
+ console.log(` ❌ ${err.message}`);
475
+ errors.push(`${label}: ${err.message}`);
476
+ failed++;
477
+
478
+ // Don't abort on single conversation failure
479
+ await client.throttle();
480
+ }
481
+ }
482
+
483
+ // Final save
484
+ state.lastSyncedAt = new Date().toISOString();
485
+ saveSyncState(state);
486
+
487
+ // Step 5: Summary
488
+ console.log("\n📊 拉取汇总:");
489
+ console.log(` 总对话数: ${allConversations.length}`);
490
+ console.log(` 已同步跳过: ${toSkip}`);
491
+ console.log(` 本次拉取: ${pulled}`);
492
+ console.log(` 空对话跳过: ${skipped}`);
493
+ console.log(` 失败: ${failed}`);
494
+ console.log(` 输出目录: ${OUTPUT_DIR}`);
495
+
496
+ if (errors.length > 0) {
497
+ console.log("\n⚠️ 失败详情:");
498
+ for (const e of errors.slice(0, 10)) {
499
+ console.log(` - ${e}`);
500
+ }
501
+ }
502
+
503
+ if (pulled > 0) {
504
+ console.log(
505
+ "\n🎯 下一步:运行以下命令将对话导入 RecallNest:",
506
+ );
507
+ console.log(" bun run src/cli.ts ingest --source desktop");
508
+ }
509
+ }
510
+
511
+ main().catch((err) => {
512
+ console.error(`\n❌ 致命错误: ${err.message}`);
513
+ if (err.stack) console.error(err.stack);
514
+ process.exit(1);
515
+ });
@@ -0,0 +1,66 @@
1
+ #!/bin/bash
2
+ # re-ingest-cc.sh — 全量重建 RecallNest 索引
3
+ #
4
+ # 背景:旧 dedup 阈值 (hard=0.80, soft=0.68) 导致 72% 内容被误杀,
5
+ # 且旧数据中可能有噪音。最干净的做法是清空后用新阈值从头导入。
6
+ #
7
+ # 用法:
8
+ # cd ~/recallnest && bash scripts/re-ingest-cc.sh
9
+ #
10
+ # 步骤:
11
+ # 1. 备份 LanceDB + ingested-files.json + distill-progress.json
12
+ # 2. reset(清空 LanceDB)
13
+ # 3. 清空 ingest 和 distill 进度记录
14
+ # 4. 全量 ingest(所有源)
15
+ # 5. 完成后提示手动跑 distill-facts
16
+ #
17
+ # 预计耗时:3-5 小时(后台跑)
18
+ # 预计 API 成本:~$3-5
19
+
20
+ set -euo pipefail
21
+ cd "$(dirname "$0")/.."
22
+
23
+ TIMESTAMP=$(date +%Y%m%d-%H%M%S)
24
+ BACKUP_DIR="data/backup-$TIMESTAMP"
25
+ LOG_FILE="logs/re-ingest-$TIMESTAMP.log"
26
+
27
+ echo "=== RecallNest 全量重建 ===" | tee "$LOG_FILE"
28
+ echo "开始时间: $(date)" | tee -a "$LOG_FILE"
29
+
30
+ # Step 1: 备份
31
+ echo "[1/4] 备份现有数据..." | tee -a "$LOG_FILE"
32
+ mkdir -p "$BACKUP_DIR"
33
+ cp data/ingested-files.json "$BACKUP_DIR/" 2>/dev/null || true
34
+ cp data/distill-progress.json "$BACKUP_DIR/" 2>/dev/null || true
35
+ # LanceDB 太大不整体备份,只记录 stats
36
+ echo " LanceDB 当前大小: $(du -sh data/lancedb 2>/dev/null | cut -f1)" | tee -a "$LOG_FILE"
37
+ echo " 备份到: $BACKUP_DIR/" | tee -a "$LOG_FILE"
38
+
39
+ # Step 2: 清空 LanceDB
40
+ echo "[2/4] 清空 LanceDB 索引..." | tee -a "$LOG_FILE"
41
+ bun run src/cli.ts reset --yes 2>&1 | tee -a "$LOG_FILE"
42
+
43
+ # Step 3: 清空进度记录
44
+ echo "[3/4] 清空 ingest/distill 进度记录..." | tee -a "$LOG_FILE"
45
+ # Reset ingested-files.json — 让 ingest 认为所有文件都没处理过
46
+ echo '{"files":{}}' > data/ingested-files.json
47
+ # Reset distill progress
48
+ echo '{"startedAt":"","lastUpdated":"","completedScopes":[],"stats":{}}' > data/distill-progress.json
49
+ echo " 进度已重置" | tee -a "$LOG_FILE"
50
+
51
+ # Step 4: 全量 ingest
52
+ echo "[4/4] 开始全量导入(这一步需要 3-5 小时)..." | tee -a "$LOG_FILE"
53
+ echo " 日志实时写入: $LOG_FILE" | tee -a "$LOG_FILE"
54
+ echo " 查看进度: tail -f $LOG_FILE" | tee -a "$LOG_FILE"
55
+ echo "" | tee -a "$LOG_FILE"
56
+
57
+ bun run src/cli.ts ingest 2>&1 | tee -a "$LOG_FILE"
58
+
59
+ echo "" | tee -a "$LOG_FILE"
60
+ echo "=== 全量导入完成 ===" | tee -a "$LOG_FILE"
61
+ echo "结束时间: $(date)" | tee -a "$LOG_FILE"
62
+ echo "" | tee -a "$LOG_FILE"
63
+ echo "下一步:" | tee -a "$LOG_FILE"
64
+ echo " 1. 检查日志确认入库率(目标 50-65%,旧阈值是 27%)" | tee -a "$LOG_FILE"
65
+ echo " 2. 跑 distill: bun scripts/distill-facts.ts" | tee -a "$LOG_FILE"
66
+ echo " 3. 旧数据备份在: $BACKUP_DIR/" | tee -a "$LOG_FILE"