squish-memory 1.0.2 → 1.1.5

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 (341) hide show
  1. package/.env.example +130 -0
  2. package/CHANGELOG.md +55 -0
  3. package/README.md +150 -287
  4. package/config/hooks/claude-code-hooks.json +39 -0
  5. package/config/hooks/cursor-hooks.json +30 -0
  6. package/config/hooks/opencode-hooks.json +30 -0
  7. package/config/hooks/windsurf-hooks.json +30 -0
  8. package/config/mcp-mode-semantics.json +23 -21
  9. package/config/plugin-manifest.json +101 -152
  10. package/{plugin.json → config/plugin.json} +2 -2
  11. package/config/settings.json +52 -51
  12. package/{commands → core/commands}/init.md +39 -39
  13. package/dist/config.d.ts +28 -4
  14. package/dist/config.js +97 -29
  15. package/dist/core/adapters/config/claude-code.d.ts +45 -0
  16. package/dist/core/adapters/config/claude-code.js +113 -0
  17. package/dist/core/adapters/config/cursor.d.ts +26 -0
  18. package/dist/core/adapters/config/cursor.js +74 -0
  19. package/dist/core/adapters/config/opencode.d.ts +23 -0
  20. package/dist/core/adapters/config/opencode.js +73 -0
  21. package/dist/core/adapters/config/windsurf.d.ts +26 -0
  22. package/dist/core/adapters/config/windsurf.js +74 -0
  23. package/dist/core/adapters/index.d.ts +45 -0
  24. package/dist/core/adapters/index.js +84 -0
  25. package/dist/core/adapters/scripts/install-adapter.d.ts +19 -0
  26. package/dist/core/adapters/scripts/install-adapter.js +149 -0
  27. package/dist/core/adapters/timeline.d.ts +23 -0
  28. package/dist/core/adapters/timeline.js +88 -0
  29. package/dist/core/adapters/types.d.ts +157 -0
  30. package/dist/core/adapters/types.js +50 -0
  31. package/dist/{algorithms → core/algorithms}/analytics/token-estimator.d.ts +1 -1
  32. package/dist/{algorithms → core/algorithms}/analytics/token-estimator.js +3 -3
  33. package/dist/{algorithms → core/algorithms}/detection/semantic-ranker.d.ts +1 -1
  34. package/dist/{algorithms → core/algorithms}/detection/semantic-ranker.js +1 -1
  35. package/dist/{algorithms → core/algorithms}/detection/two-stage-detector.d.ts +1 -1
  36. package/dist/{algorithms → core/algorithms}/detection/two-stage-detector.js +7 -10
  37. package/dist/{algorithms → core/algorithms}/handlers/approve-merge.js +4 -4
  38. package/dist/{algorithms → core/algorithms}/handlers/detect-duplicates.js +3 -3
  39. package/dist/{algorithms → core/algorithms}/handlers/get-stats.js +3 -3
  40. package/dist/{algorithms → core/algorithms}/handlers/list-proposals.js +3 -3
  41. package/dist/{algorithms → core/algorithms}/handlers/preview-merge.js +3 -3
  42. package/dist/{algorithms → core/algorithms}/handlers/reject-merge.js +3 -3
  43. package/dist/{algorithms → core/algorithms}/handlers/reverse-merge.js +3 -3
  44. package/dist/core/algorithms/index.d.ts +21 -0
  45. package/dist/core/algorithms/index.js +26 -0
  46. package/dist/core/algorithms/operations/cache-maintenance.d.ts +12 -0
  47. package/dist/core/algorithms/operations/cache-maintenance.js +157 -0
  48. package/dist/{algorithms → core/algorithms}/safety/safety-checks.d.ts +1 -1
  49. package/dist/{algorithms → core/algorithms}/strategies/merge-strategies.d.ts +19 -1
  50. package/dist/{algorithms → core/algorithms}/strategies/merge-strategies.js +74 -123
  51. package/dist/core/algorithms/types.d.ts +133 -0
  52. package/dist/core/algorithms/types.js +5 -0
  53. package/dist/core/associations.d.ts +1 -2
  54. package/dist/core/associations.js +1 -2
  55. package/dist/core/autosave.d.ts +19 -0
  56. package/dist/core/autosave.js +16 -0
  57. package/dist/{commands → core/commands}/managed-sync.js +5 -5
  58. package/dist/core/commands/mcp-server.js +739 -0
  59. package/dist/core/context/agent-context.d.ts +106 -0
  60. package/dist/core/context/agent-context.js +274 -0
  61. package/dist/core/{context-paging.d.ts → context/context-paging.d.ts} +2 -12
  62. package/dist/core/{context-paging.js → context/context-paging.js} +19 -39
  63. package/dist/core/context/context-window.d.ts +40 -0
  64. package/dist/core/context/context-window.js +177 -0
  65. package/dist/core/context/context.js +22 -0
  66. package/dist/core/embeddings.d.ts +1 -1
  67. package/dist/core/embeddings.js +54 -2
  68. package/dist/core/error-handling.d.ts +63 -0
  69. package/dist/core/error-handling.js +173 -0
  70. package/dist/core/external-folder/index.d.ts +102 -0
  71. package/dist/core/external-folder/index.js +294 -0
  72. package/dist/core/hooks/agent-hooks.d.ts +74 -0
  73. package/dist/core/hooks/agent-hooks.js +244 -0
  74. package/dist/core/hooks/auto-tagger.d.ts +19 -0
  75. package/dist/core/hooks/auto-tagger.js +155 -0
  76. package/dist/core/hooks/capture-filter.d.ts +41 -0
  77. package/dist/core/hooks/capture-filter.js +128 -0
  78. package/dist/core/index.d.ts +6 -6
  79. package/dist/core/index.js +6 -6
  80. package/dist/core/{agent-memory.js → ingestion/agent-memory.js} +5 -7
  81. package/dist/core/{core-memory.js → ingestion/core-memory.js} +4 -4
  82. package/dist/core/ingestion/learnings.d.ts +57 -0
  83. package/dist/core/ingestion/learnings.js +202 -0
  84. package/dist/core/lib/db-client.d.ts +114 -0
  85. package/dist/core/lib/db-client.js +130 -0
  86. package/dist/core/lib/schemas.d.ts +129 -0
  87. package/dist/core/lib/schemas.js +87 -0
  88. package/dist/core/{utils.d.ts → lib/utils.d.ts} +1 -0
  89. package/dist/core/{utils.js → lib/utils.js} +31 -15
  90. package/dist/core/lib/validation.d.ts +38 -0
  91. package/dist/core/lib/validation.js +151 -0
  92. package/dist/core/lifecycle.d.ts +7 -0
  93. package/dist/core/lifecycle.js +140 -20
  94. package/dist/core/local-embeddings.d.ts +6 -1
  95. package/dist/core/local-embeddings.js +6 -15
  96. package/dist/core/logger.js +7 -1
  97. package/dist/core/mcp/tools.js +35 -3
  98. package/dist/core/memory/categorizer.js +1 -0
  99. package/dist/core/memory/conflict-detector.js +1 -1
  100. package/dist/core/memory/consolidation.d.ts +1 -10
  101. package/dist/core/memory/consolidation.js +2 -11
  102. package/dist/core/memory/context-collector.js +1 -1
  103. package/dist/core/memory/edit-workflow.js +1 -1
  104. package/dist/core/memory/entity-resolver.js +7 -7
  105. package/dist/core/memory/fact-extractor.js +12 -12
  106. package/dist/core/memory/feedback-tracker.js +1 -1
  107. package/dist/core/memory/hooks.d.ts +88 -0
  108. package/dist/core/memory/hooks.js +174 -0
  109. package/dist/core/memory/hybrid-retrieval.js +2 -2
  110. package/dist/core/memory/hybrid-search.d.ts +1 -6
  111. package/dist/core/memory/hybrid-search.js +70 -84
  112. package/dist/core/memory/importance.d.ts +8 -13
  113. package/dist/core/memory/importance.js +47 -74
  114. package/dist/core/memory/loader.d.ts +31 -0
  115. package/dist/core/memory/loader.js +141 -0
  116. package/dist/core/memory/markdown/markdown-storage.d.ts +72 -0
  117. package/dist/core/memory/markdown/markdown-storage.js +243 -0
  118. package/dist/core/memory/memories.d.ts +12 -4
  119. package/dist/core/memory/memories.js +192 -180
  120. package/dist/core/memory/memory-lifecycle.d.ts +8 -0
  121. package/dist/core/memory/memory-lifecycle.js +55 -0
  122. package/dist/core/memory/migrate.d.ts +21 -0
  123. package/dist/core/memory/migrate.js +134 -0
  124. package/dist/core/memory/normalization.d.ts +22 -0
  125. package/dist/core/memory/normalization.js +26 -0
  126. package/dist/core/memory/progressive-disclosure.js +1 -1
  127. package/dist/core/memory/query-rewriter.js +9 -9
  128. package/dist/core/memory/serialization.d.ts +4 -0
  129. package/dist/core/memory/serialization.js +49 -0
  130. package/dist/core/memory/stats.d.ts +5 -0
  131. package/dist/core/memory/stats.js +63 -12
  132. package/dist/core/memory/temporal-facts.js +21 -0
  133. package/dist/core/memory/write-gate.js +1 -1
  134. package/dist/core/obsidian-vault.d.ts +30 -0
  135. package/dist/core/obsidian-vault.js +94 -0
  136. package/dist/core/places/index.d.ts +14 -0
  137. package/dist/core/places/index.js +14 -0
  138. package/dist/core/places/memory-places.d.ts +68 -0
  139. package/dist/core/places/memory-places.js +261 -0
  140. package/dist/core/places/places.d.ts +88 -0
  141. package/dist/core/places/places.js +314 -0
  142. package/dist/core/places/rules.d.ts +74 -0
  143. package/dist/core/places/rules.js +240 -0
  144. package/dist/core/places/walking.d.ts +56 -0
  145. package/dist/core/places/walking.js +121 -0
  146. package/dist/core/projects.d.ts +5 -0
  147. package/dist/core/projects.js +39 -18
  148. package/dist/core/responses.d.ts +96 -0
  149. package/dist/core/responses.js +122 -0
  150. package/dist/core/scheduler/cron-scheduler.js +29 -7
  151. package/dist/core/scheduler/index.d.ts +1 -1
  152. package/dist/core/scheduler/index.js +1 -1
  153. package/dist/core/scheduler/job-runner.js +1 -1
  154. package/dist/core/search/conversations.js +40 -42
  155. package/dist/core/search/entities.js +6 -9
  156. package/dist/core/search/graph-boost.d.ts +7 -0
  157. package/dist/core/search/graph-boost.js +23 -0
  158. package/dist/core/search/qmd-search.js +4 -4
  159. package/dist/core/security/encrypt.d.ts +6 -0
  160. package/dist/core/security/encrypt.js +47 -0
  161. package/dist/core/{governance.d.ts → security/governance.d.ts} +6 -1
  162. package/dist/core/security/governance.js +79 -0
  163. package/dist/core/session/auto-load.js +6 -6
  164. package/dist/core/session/index.d.ts +1 -1
  165. package/dist/core/session/index.js +1 -1
  166. package/dist/core/session/self-iteration-job.d.ts +20 -0
  167. package/dist/core/session/self-iteration-job.js +282 -0
  168. package/dist/core/session/session-hooks.d.ts +18 -0
  169. package/dist/core/session/session-hooks.js +58 -0
  170. package/dist/core/session-hooks/self-iteration-job.js +35 -35
  171. package/dist/core/{cache.js → storage/cache.js} +2 -2
  172. package/dist/core/sync/qmd-sync.d.ts +1 -13
  173. package/dist/core/sync/qmd-sync.js +1 -13
  174. package/dist/core/toon.d.ts +43 -0
  175. package/dist/core/toon.js +160 -0
  176. package/dist/core/utils/memory-operations.js +1 -1
  177. package/dist/core/utils/vector-operations.d.ts +71 -0
  178. package/dist/core/utils/vector-operations.js +129 -0
  179. package/dist/db/adapter.d.ts +3 -3
  180. package/dist/db/adapter.js +99 -88
  181. package/dist/db/bootstrap.js +820 -522
  182. package/dist/{drizzle → db/drizzle}/schema-sqlite.d.ts +74 -25
  183. package/dist/{drizzle → db/drizzle}/schema-sqlite.js +91 -24
  184. package/dist/{drizzle → db/drizzle}/schema.d.ts +79 -32
  185. package/dist/{drizzle → db/drizzle}/schema.js +106 -35
  186. package/dist/db/drizzle.config.d.ts +3 -0
  187. package/dist/db/drizzle.config.js +12 -0
  188. package/dist/db/index.d.ts +1 -5
  189. package/dist/db/index.js +51 -8
  190. package/dist/db/neon.d.ts +8 -0
  191. package/dist/db/neon.js +20 -0
  192. package/dist/db/schema/index.d.ts +40 -0
  193. package/dist/db/schema/index.js +105 -0
  194. package/dist/db/schema/tables/context-sessions.d.ts +9 -0
  195. package/dist/db/schema/tables/context-sessions.js +37 -0
  196. package/dist/db/schema/tables/conversations.d.ts +9 -0
  197. package/dist/db/schema/tables/conversations.js +47 -0
  198. package/dist/db/schema/tables/core-memory.d.ts +9 -0
  199. package/dist/db/schema/tables/core-memory.js +41 -0
  200. package/dist/db/schema/tables/entities.d.ts +9 -0
  201. package/dist/db/schema/tables/entities.js +39 -0
  202. package/dist/db/schema/tables/entity-relations.d.ts +9 -0
  203. package/dist/db/schema/tables/entity-relations.js +31 -0
  204. package/dist/db/schema/tables/learnings.d.ts +9 -0
  205. package/dist/db/schema/tables/learnings.js +66 -0
  206. package/dist/db/schema/tables/memories.d.ts +9 -0
  207. package/dist/db/schema/tables/memories.js +161 -0
  208. package/dist/db/schema/tables/memory-associations.d.ts +9 -0
  209. package/dist/db/schema/tables/memory-associations.js +39 -0
  210. package/dist/db/schema/tables/memory-hash-cache.d.ts +9 -0
  211. package/dist/db/schema/tables/memory-hash-cache.js +29 -0
  212. package/dist/db/schema/tables/memory-merge-history.d.ts +9 -0
  213. package/dist/db/schema/tables/memory-merge-history.js +33 -0
  214. package/dist/db/schema/tables/memory-merge-proposals.d.ts +9 -0
  215. package/dist/db/schema/tables/memory-merge-proposals.js +39 -0
  216. package/dist/db/schema/tables/messages.d.ts +9 -0
  217. package/dist/db/schema/tables/messages.js +41 -0
  218. package/dist/db/schema/tables/namespaces.d.ts +9 -0
  219. package/dist/db/schema/tables/namespaces.js +37 -0
  220. package/dist/db/schema/tables/projects.d.ts +9 -0
  221. package/dist/db/schema/tables/projects.js +31 -0
  222. package/dist/db/schema/tables/users.d.ts +9 -0
  223. package/dist/db/schema/tables/users.js +27 -0
  224. package/dist/db/schema.d.ts +1 -1
  225. package/dist/db/schema.js +2 -2
  226. package/dist/db/supabase.d.ts +9 -0
  227. package/dist/db/supabase.js +24 -0
  228. package/dist/index.d.ts +2 -14
  229. package/dist/index.js +1320 -640
  230. package/dist/vendor/sql.js/sql-wasm.wasm +0 -0
  231. package/dist/webui/server.d.ts +5 -0
  232. package/dist/{api/web/web.js → webui/server.js} +511 -508
  233. package/generated/mcp/manifest.json +1 -1
  234. package/{.mcp.json → mcp.json.example} +1 -1
  235. package/package.json +159 -181
  236. package/scripts/README.md +60 -0
  237. package/scripts/copy-runtime-assets.mjs +26 -0
  238. package/scripts/generate-mcp.mjs +264 -264
  239. package/scripts/github-release.sh +4 -4
  240. package/scripts/install-claude-code.sh +85 -0
  241. package/scripts/install-cursor.sh +56 -0
  242. package/scripts/install-hooks.sh +73 -0
  243. package/scripts/install-interactive.mjs +357 -677
  244. package/scripts/install-opencode.sh +75 -0
  245. package/scripts/install-windsurf.sh +67 -0
  246. package/skills/squish-memory/SKILL.md +104 -114
  247. package/skills/squish-memory/{install.mjs → scripts/install.mjs} +2 -2
  248. package/skills/squish-memory/{install.sh → scripts/install.sh} +2 -2
  249. package/skills/squish-memory/write_skill.js +2 -0
  250. package/.claude-plugin/marketplace.json +0 -20
  251. package/.claude-plugin/plugin.json +0 -32
  252. package/.env.mcp.example +0 -60
  253. package/QUICK-START.md +0 -71
  254. package/bin/squish-add.mjs +0 -32
  255. package/bin/squish-rm.mjs +0 -21
  256. package/commands/observe.md +0 -5
  257. package/dist/api/web/index.d.ts +0 -3
  258. package/dist/api/web/index.js +0 -4
  259. package/dist/api/web/web-server.d.ts +0 -3
  260. package/dist/api/web/web-server.js +0 -6
  261. package/dist/api/web/web.d.ts +0 -4
  262. package/dist/commands/mcp-server.js +0 -393
  263. package/dist/core/context.js +0 -24
  264. package/dist/core/governance.js +0 -64
  265. package/dist/core/observations.d.ts +0 -26
  266. package/dist/core/observations.js +0 -110
  267. package/dist/core/requirements.d.ts +0 -20
  268. package/dist/core/requirements.js +0 -35
  269. package/hooks/hooks.json +0 -52
  270. package/hooks/post-tool-use.js +0 -26
  271. package/hooks/session-end.js +0 -28
  272. package/hooks/session-start.js +0 -33
  273. package/hooks/user-prompt-submit.js +0 -26
  274. package/hooks/utils.js +0 -153
  275. package/npx-installer.js +0 -208
  276. package/packages/plugin-claude-code/README.md +0 -73
  277. package/packages/plugin-claude-code/dist/plugin-wrapper.d.ts +0 -35
  278. package/packages/plugin-claude-code/dist/plugin-wrapper.js +0 -191
  279. package/packages/plugin-claude-code/package.json +0 -31
  280. package/packages/plugin-openclaw/README.md +0 -70
  281. package/packages/plugin-openclaw/dist/index.d.ts +0 -49
  282. package/packages/plugin-openclaw/dist/index.js +0 -262
  283. package/packages/plugin-openclaw/openclaw.plugin.json +0 -94
  284. package/packages/plugin-openclaw/package.json +0 -31
  285. package/packages/plugin-opencode/install.mjs +0 -217
  286. package/packages/plugin-opencode/package.json +0 -21
  287. package/scripts/db/check-db.mjs +0 -88
  288. package/scripts/db/fix-all-columns.mjs +0 -52
  289. package/scripts/db/fix-schema-all.mjs +0 -55
  290. package/scripts/db/fix-schema-full.mjs +0 -46
  291. package/scripts/db/fix-schema.mjs +0 -38
  292. package/scripts/db/init-db.mjs +0 -13
  293. package/scripts/db/recreate-db.mjs +0 -14
  294. package/scripts/install-mcp.mjs +0 -116
  295. package/scripts/install-web.sh +0 -120
  296. package/scripts/install.mjs +0 -340
  297. package/scripts/openclaw-bootstrap.mjs +0 -127
  298. package/scripts/package-release.sh +0 -71
  299. package/scripts/test/test-all-systems.mjs +0 -139
  300. package/scripts/test/test-memory-system.mjs +0 -139
  301. package/scripts/test/test-v0.5.0.mjs +0 -210
  302. package/skills/memory-guide/SKILL.md +0 -332
  303. package/skills/squish-cli/SKILL.md +0 -240
  304. package/skills/squish-mcp/SKILL.md +0 -355
  305. package/skills/squish-memory/claude-desktop.json +0 -12
  306. package/skills/squish-memory/openclaw.json +0 -13
  307. package/skills/squish-memory/opencode.json +0 -14
  308. package/skills/squish-memory/skill.json +0 -32
  309. /package/{commands → core/commands}/context-paging.md +0 -0
  310. /package/{commands → core/commands}/context-status.md +0 -0
  311. /package/{commands → core/commands}/context.md +0 -0
  312. /package/{commands → core/commands}/core-memory.md +0 -0
  313. /package/{commands → core/commands}/health.md +0 -0
  314. /package/{commands → core/commands}/merge.md +0 -0
  315. /package/{commands → core/commands}/recall.md +0 -0
  316. /package/{commands → core/commands}/remember.md +0 -0
  317. /package/{commands → core/commands}/search.md +0 -0
  318. /package/dist/{algorithms → core/algorithms}/detection/hash-filters.d.ts +0 -0
  319. /package/dist/{algorithms → core/algorithms}/detection/hash-filters.js +0 -0
  320. /package/dist/{algorithms → core/algorithms}/handlers/approve-merge.d.ts +0 -0
  321. /package/dist/{algorithms → core/algorithms}/handlers/detect-duplicates.d.ts +0 -0
  322. /package/dist/{algorithms → core/algorithms}/handlers/get-stats.d.ts +0 -0
  323. /package/dist/{algorithms → core/algorithms}/handlers/list-proposals.d.ts +0 -0
  324. /package/dist/{algorithms → core/algorithms}/handlers/preview-merge.d.ts +0 -0
  325. /package/dist/{algorithms → core/algorithms}/handlers/reject-merge.d.ts +0 -0
  326. /package/dist/{algorithms → core/algorithms}/handlers/reverse-merge.d.ts +0 -0
  327. /package/dist/{algorithms → core/algorithms}/safety/safety-checks.js +0 -0
  328. /package/dist/{algorithms → core/algorithms}/utils/response-builder.d.ts +0 -0
  329. /package/dist/{algorithms → core/algorithms}/utils/response-builder.js +0 -0
  330. /package/dist/{commands → core/commands}/managed-sync.d.ts +0 -0
  331. /package/dist/{commands → core/commands}/mcp-server.d.ts +0 -0
  332. /package/dist/core/{context.d.ts → context/context.d.ts} +0 -0
  333. /package/dist/core/{agent-memory.d.ts → ingestion/agent-memory.d.ts} +0 -0
  334. /package/dist/core/{core-memory.d.ts → ingestion/core-memory.d.ts} +0 -0
  335. /package/dist/core/{privacy.d.ts → security/privacy.d.ts} +0 -0
  336. /package/dist/core/{privacy.js → security/privacy.js} +0 -0
  337. /package/dist/core/{secret-detector.d.ts → security/secret-detector.d.ts} +0 -0
  338. /package/dist/core/{secret-detector.js → security/secret-detector.js} +0 -0
  339. /package/dist/core/{cache.d.ts → storage/cache.d.ts} +0 -0
  340. /package/dist/core/{database.d.ts → storage/database.d.ts} +0 -0
  341. /package/dist/core/{database.js → storage/database.js} +0 -0
package/dist/index.js CHANGED
@@ -1,19 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Squish v1.0.2 - Universal Memory Plugin System
4
- *
5
- * Modes:
6
- * - CLI Mode: For any MCP client bash execution (e.g., `squish remember "text"`)
7
- * - MCP Mode: For AI assistants (Claude Code, OpenClaw, OpenCode, Codex, etc.)
8
- *
9
- * Features:
10
- * - Hybrid Search: BM25 + vector search with RRF
11
- * - Importance Scoring: Auto-score memories with temporal decay
12
- * - Consolidation: Summarize old, low-importance memory clusters
13
- * - 16 MCP tools
14
- * - Local mode: SQLite with FTS5
15
- * - Team mode: PostgreSQL + pgvector
16
- * - Universal Plugin: Works with 7+ AI assistants
3
+ * Squish - Universal Memory Plugin System
4
+ * CLI + MCP server for persistent memory with hybrid search and encryption
17
5
  */
18
6
  import 'dotenv/config';
19
7
  import fs from 'node:fs';
@@ -21,44 +9,157 @@ import { existsSync } from 'node:fs';
21
9
  import os from 'node:os';
22
10
  import path from 'node:path';
23
11
  import { fileURLToPath } from 'node:url';
24
- import { spawnSync } from 'node:child_process';
12
+ import { spawn, spawnSync } from 'node:child_process';
25
13
  import { Command } from 'commander';
26
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
27
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
28
- import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
29
14
  import { logger } from './core/logger.js';
30
- import { checkDatabaseHealth, config } from './db/index.js';
31
- import { checkRedisHealth, closeCache } from './core/cache.js';
32
- import { rememberMemory, getMemoryById, searchMemories } from './core/memory/memories.js';
33
- import { createObservation } from './core/observations.js';
34
- import { getProjectContext } from './core/context.js';
35
- import { setImportanceScore } from './core/memory/importance.js';
15
+ import { checkDatabaseHealth, config, getDb } from './db/index.js';
16
+ import { getSchema } from './db/schema.js';
17
+ import { eq } from 'drizzle-orm';
18
+ import { checkRedisHealth } from './core/storage/cache.js';
19
+ import { rememberMemory, getMemory, search, getRecent, setConfidence } from './core/memory/memories.js';
20
+ import { serializeTags } from './core/memory/serialization.js';
21
+ import { createLearning } from './core/ingestion/learnings.js';
36
22
  import { getMemoryStats } from './core/memory/stats.js';
37
- import { ensureProject } from './core/projects.js';
38
- import { consolidateMemories as consolidateMemoriesImpl, getConsolidationStats } from './core/memory/consolidation.js';
39
- import { startWebServer } from './api/web/web.js';
40
- import { handleDetectDuplicates } from './algorithms/handlers/detect-duplicates.js';
41
- import { handleListProposals } from './algorithms/handlers/list-proposals.js';
42
- import { handlePreviewMerge } from './algorithms/handlers/preview-merge.js';
43
- import { handleApproveMerge } from './algorithms/handlers/approve-merge.js';
44
- import { handleRejectMerge } from './algorithms/handlers/reject-merge.js';
45
- import { handleReverseMerge } from './algorithms/handlers/reverse-merge.js';
46
- import { handleGetMergeStats } from './algorithms/handlers/get-stats.js';
47
- import { pinMemory, unpinMemory } from './core/governance.js';
48
- import { searchWithQMD, isQMDAvailable } from './core/search/qmd-search.js';
49
- import { initializeCoreMemory, getCoreMemory, editCoreMemorySection, appendCoreMemorySection, getCoreMemoryStats, } from './core/core-memory.js';
50
- import { loadMemoryToContext, evictMemoryFromContext, viewLoadedMemories, getContextStatus, } from './core/context-paging.js';
23
+ import { ensureProject, getAllProjects, getOrCreateProject } from './core/projects.js';
24
+ import { startWebServer } from './webui/server.js';
25
+ import { getRelatedMemories, createAssociation } from './core/associations.js';
26
+ import { pinMemory, unpinMemory } from './core/security/governance.js';
27
+ import { determineOverallStatus } from './core/lib/utils.js';
28
+ import { validateLimit } from './core/lib/validation.js';
29
+ import { runDeduplicationJob, runFullConsolidationJob } from './core/consolidation.js';
51
30
  import { ensureDataDirectory } from './db/bootstrap.js';
52
31
  import { getDataDir } from './config.js';
53
- import { performAutoLoad, shouldAutoLoad, getAutoLoadConfig } from './core/session/auto-load.js';
54
- import { initializeScheduler, registerJobHandler } from './core/scheduler/cron-scheduler.js';
55
- import { startHeartbeatChecking, heartbeat } from './core/scheduler/heartbeat.js';
56
- import { runNightlyJob, runWeeklyJob } from './core/scheduler/job-runner.js';
57
- const VERSION = '1.0.2';
32
+ import { handleSessionStart, handlePostToolUse, handleSessionEnd, handlePreCompact, } from './core/hooks/agent-hooks.js';
33
+ import { initializeDefaultPlaces, walkPlace, walkAllPlaces, quickTour, } from './core/places/index.js';
34
+ const VERSION = '1.1.5';
35
+ // Output Formatting Utilities
36
+ // ============================================================================
37
+ function formatOutput(data, pretty = false) {
38
+ if (!pretty) {
39
+ return JSON.stringify(data, null, 2);
40
+ }
41
+ if (Array.isArray(data)) {
42
+ return data.map((item, i) => `${i + 1}. ${formatItem(item)}`).join('\n');
43
+ }
44
+ if (data.results) {
45
+ return data.results.map((item, i) => `${i + 1}. ${formatItem(item)}`).join('\n');
46
+ }
47
+ if (data.matches) {
48
+ return data.matches.map((item, i) => `${i + 1}. ${formatItem(item)}`).join('\n');
49
+ }
50
+ if (data.count !== undefined) {
51
+ let output = `\nFound ${data.count} results:\n`;
52
+ if (data.results) {
53
+ output += data.results.map((item, i) => ` ${i + 1}. ${formatItem(item)}`).join('\n');
54
+ }
55
+ return output;
56
+ }
57
+ return JSON.stringify(data, null, 2);
58
+ }
59
+ function formatItem(item) {
60
+ if (typeof item === 'string')
61
+ return item.substring(0, 100);
62
+ const content = item.content || item.summary || item.memory?.content || '';
63
+ const type = item.type || '';
64
+ return `[${type}] ${content.substring(0, 80)}${content.length > 80 ? '...' : ''}`;
65
+ }
66
+ function printSuccess(message) {
67
+ console.log(`\n ✓ ${message}\n`);
68
+ }
69
+ function printError(message) {
70
+ console.error(`\n ✗ ${message}\n`);
71
+ }
72
+ function printInfo(message) {
73
+ console.log(`\n ℹ ${message}\n`);
74
+ }
75
+ // Config Management (for project path persistence)
76
+ // ============================================================================
77
+ const USER_CONFIG_PATH = path.join(os.homedir(), '.squish', 'config.json');
78
+ function loadUserConfig() {
79
+ try {
80
+ if (fs.existsSync(USER_CONFIG_PATH)) {
81
+ return JSON.parse(fs.readFileSync(USER_CONFIG_PATH, 'utf-8'));
82
+ }
83
+ }
84
+ catch (e) { }
85
+ return {};
86
+ }
87
+ function saveUserConfig(config) {
88
+ const dir = path.dirname(USER_CONFIG_PATH);
89
+ if (!fs.existsSync(dir))
90
+ fs.mkdirSync(dir, { recursive: true });
91
+ fs.writeFileSync(USER_CONFIG_PATH, JSON.stringify(config, null, 2));
92
+ }
93
+ function getDefaultProjectPath() {
94
+ const userConfig = loadUserConfig();
95
+ if (userConfig.project)
96
+ return userConfig.project;
97
+ return process.cwd();
98
+ }
99
+ function resolveProjectPath(projectOption) {
100
+ if (projectOption)
101
+ return projectOption;
102
+ return getDefaultProjectPath();
103
+ }
104
+ // Date parsing for time-based queries
105
+ // ============================================================================
106
+ function parseDate(input) {
107
+ if (!input)
108
+ return null;
109
+ const now = new Date();
110
+ const lower = input.toLowerCase().trim();
111
+ // Direct date parse
112
+ const parsed = new Date(input);
113
+ if (!isNaN(parsed.getTime()))
114
+ return parsed;
115
+ // Relative parsing
116
+ const dayMatch = lower.match(/(\d+)\s*day/i);
117
+ const weekMatch = lower.match(/(\d+)\s*week/i);
118
+ const monthMatch = lower.match(/(\d+)\s*month/i);
119
+ if (lower === 'today') {
120
+ const d = new Date(now);
121
+ d.setHours(0, 0, 0, 0);
122
+ return d;
123
+ }
124
+ if (lower === 'yesterday')
125
+ return new Date(now.getTime() - 86400000);
126
+ if (lower === 'thisweek' || lower === 'this week') {
127
+ const d = new Date(now);
128
+ d.setDate(d.getDate() - d.getDay());
129
+ d.setHours(0, 0, 0, 0);
130
+ return d;
131
+ }
132
+ if (lower === 'lastweek' || lower === 'last week') {
133
+ const d = new Date(now);
134
+ d.setDate(d.getDate() - d.getDay() - 7);
135
+ return d;
136
+ }
137
+ if (dayMatch)
138
+ return new Date(now.getTime() - parseInt(dayMatch[1]) * 86400000);
139
+ if (weekMatch)
140
+ return new Date(now.getTime() - parseInt(weekMatch[1]) * 604800000);
141
+ if (monthMatch)
142
+ return new Date(now.getTime() - parseInt(monthMatch[1]) * 2592000000);
143
+ return null;
144
+ }
145
+ function filterByDateRange(items, since, until) {
146
+ const sinceDate = parseDate(since || '');
147
+ const untilDate = parseDate(until || '');
148
+ return items.filter(item => {
149
+ if (!item.createdAt)
150
+ return true;
151
+ const created = new Date(item.createdAt);
152
+ if (sinceDate && created < sinceDate)
153
+ return false;
154
+ if (untilDate && created > untilDate)
155
+ return false;
156
+ return true;
157
+ });
158
+ }
58
159
  // Load plugin manifest for self-verification
59
160
  function loadPluginManifest() {
60
161
  try {
61
- const manifestPath = path.join(process.cwd(), 'config', 'plugin-manifest.json');
162
+ const manifestPath = path.join(getDefaultProjectPath(), 'config', 'plugin-manifest.json');
62
163
  if (fs.existsSync(manifestPath)) {
63
164
  return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
64
165
  }
@@ -90,33 +191,71 @@ function verifyManifest(manifest) {
90
191
  });
91
192
  return { ok: errors.length === 0, errors };
92
193
  }
93
- // ============================================================================
194
+ async function buildHealthStatus() {
195
+ const dbHealth = await checkDatabaseHealth();
196
+ const redisHealth = await checkRedisHealth();
197
+ const database = dbHealth ? 'ok' : 'error';
198
+ const cache = config.redisEnabled ? (redisHealth ? 'ok' : 'error') : 'unavailable';
199
+ const overallStatus = config.redisEnabled
200
+ ? determineOverallStatus(database, redisHealth)
201
+ : (dbHealth ? 'ok' : 'error');
202
+ return {
203
+ ok: overallStatus === 'ok',
204
+ version: VERSION,
205
+ mode: config.isTeamMode ? 'team' : 'local',
206
+ database,
207
+ cache,
208
+ dataDirectory: config.dataDir,
209
+ dataDirectoryExists: fs.existsSync(config.dataDir),
210
+ status: overallStatus,
211
+ timestamp: new Date().toISOString(),
212
+ };
213
+ }
94
214
  // HELPER FUNCTIONS
95
215
  // ============================================================================
96
216
  function showHelp() {
97
- console.log(`
98
- Squish Memory v${VERSION} - Universal Memory Plugin System
99
-
100
- Usage:
101
- squish Start interactive wizard
102
- squish run mcp Start MCP server
103
- squish run web Start Web UI only
104
- squish <command> [options] Run CLI commands for agents
105
-
106
- CLI Commands (for agents):
107
- squish remember <content> Store a memory
108
- squish search <query> Search memories
109
- squish health Check system health
110
- squish stats View statistics
111
- squish core_memory Manage core memory
112
-
113
- Examples:
114
- squish run mcp # Start MCP server (for Claude Code)
115
- squish run web # Start Web UI only
116
- squish remember "Hello" # Store memory via CLI
117
- squish search "query" # Search memories via CLI
118
-
119
- For more info: https://github.com/michielhdoteth/squish
217
+ console.log(`
218
+ Squish Memory v${VERSION} - Universal Memory Plugin System
219
+
220
+ Usage:
221
+ squish Start interactive wizard
222
+ squish run mcp Start MCP server
223
+ squish run web Start Web UI only
224
+ squish <command> [options] Run CLI commands for agents
225
+
226
+ CLI Commands (for agents):
227
+ squish config [action] Manage Squish configuration
228
+ squish remember <content> Store a memory
229
+ squish note <content> Save a quick note
230
+ squish learn <type> <text> Record learning: success, failure, fix, insight
231
+ squish search <query> Search memories (--pretty for human output)
232
+ squish recall <query> Search or get by ID (--pretty for human output)
233
+ squish recent --period Recent memories (today/yesterday/thisweek/7days/30days)
234
+ squish update <memoryId> Update memory
235
+ squish forget <memoryId> Delete memory (single or bulk with --older-than --search)
236
+ squish pin <memoryId> Pin/unpin memory
237
+ squish confidence <id> Set confidence
238
+ squish tag <action> Manage tags
239
+ squish stale Show stale memories
240
+ squish link <action> Manage links (find/add/list)
241
+ squish migrate Migrate memories between .squish directories
242
+ squish clean Dedup + consolidate (maintenance)
243
+ squish context Show context or list projects
244
+ squish stats View memory statistics
245
+
246
+ Examples:
247
+ squish run mcp # Start MCP server (for agents)
248
+ squish run web # Start Web UI only
249
+ squish config set project /repo/path
250
+ squish remember "Hello" # Store memory
251
+ squish note "Ship v1 first" # Save a quick note
252
+ squish learn observation "Updated auth flow" --action edit
253
+ squish learn fix "Patched auth middleware" --target middleware.ts
254
+ squish search "query" # Search memories
255
+ squish context --list-projects
256
+ squish clean # Run deduplication and consolidation
257
+
258
+ For more info: https://github.com/michielhdoteth/squish
120
259
  `);
121
260
  }
122
261
  async function runInteractiveInstaller() {
@@ -144,7 +283,8 @@ async function runInteractiveInstaller() {
144
283
  switch (selected) {
145
284
  case 'mcp':
146
285
  log.step('Starting MCP server...');
147
- await runMcpMode();
286
+ const { spawn } = await import('child_process');
287
+ spawn('npx', ['squish-mcp'], { stdio: 'inherit', shell: true });
148
288
  break;
149
289
  case 'web':
150
290
  log.step('Starting Web UI...');
@@ -172,20 +312,17 @@ async function runCliCommand(command) {
172
312
  await ensureDataDirectory();
173
313
  });
174
314
  if (command === 'health') {
175
- const dbHealth = await checkDatabaseHealth();
176
- const redisHealth = await checkRedisHealth();
177
- const dataDir = process.env.SQUISH_DATA_DIR || path.join(os.homedir(), '.squish');
178
- const dirExists = fs.existsSync(dataDir);
315
+ const status = await buildHealthStatus();
179
316
  console.log(`\n Squish Memory v${VERSION}`);
180
317
  console.log(` ====================`);
181
- console.log(` Mode: ${config.isTeamMode ? 'team' : 'local'}`);
182
- console.log(` Database: ${dbHealth ? 'ok' : 'error'}`);
183
- console.log(` Cache: ${redisHealth ? 'ok' : 'unavailable'}`);
184
- console.log(` Data Dir: ${dataDir}`);
185
- console.log(` Status: ${dbHealth ? 'HEALTHY' : 'UNHEALTHY'}\n`);
318
+ console.log(` Mode: ${status.mode}`);
319
+ console.log(` Database: ${status.database}`);
320
+ console.log(` Cache: ${status.cache}`);
321
+ console.log(` Data Dir: ${status.dataDirectory}`);
322
+ console.log(` Status: ${status.ok ? 'HEALTHY' : 'UNHEALTHY'}\n`);
186
323
  }
187
324
  else if (command === 'stats') {
188
- const stats = await getMemoryStats(process.cwd());
325
+ const stats = await getMemoryStats(getDefaultProjectPath());
189
326
  console.log(JSON.stringify({ ok: true, ...stats }, null, 2));
190
327
  }
191
328
  }
@@ -218,9 +355,8 @@ function isDatabaseInitialized() {
218
355
  async function runWebOnly() {
219
356
  console.log(`[squish] Starting Web UI only...`);
220
357
  await ensureDataDirectory();
221
- startWebServer();
358
+ await startWebServer();
222
359
  }
223
- // ============================================================================
224
360
  // CLI MODE DETECTION
225
361
  // ============================================================================
226
362
  const args = process.argv.slice(2);
@@ -236,7 +372,7 @@ if (isNoArgs) {
236
372
  await spawnInstallerWizard();
237
373
  }
238
374
  else {
239
- // === INTERACTIVE WIZARD (default when no args) ===
375
+ // INTERACTIVE WIZARD (default when no args) ===
240
376
  runInteractiveInstaller().catch((e) => {
241
377
  console.error('Installer error:', e.message);
242
378
  process.exit(1);
@@ -244,13 +380,35 @@ if (isNoArgs) {
244
380
  }
245
381
  }
246
382
  else if (isRunCommand) {
247
- // === RUN SUBCOMMAND ===
383
+ // RUN SUBCOMMAND ===
248
384
  const subcommand = args[1];
249
385
  if (subcommand === 'mcp') {
250
- runMcpMode().catch((e) => {
251
- logger.error('Fatal error', e);
252
- process.exit(1);
253
- });
386
+ (async () => {
387
+ try {
388
+ // Initialize data directory before starting MCP
389
+ await ensureDataDirectory();
390
+ // Start MCP server as child process (stdio mode for agents)
391
+ const mcpProcess = spawn('npx', ['squish-mcp'], {
392
+ stdio: 'inherit',
393
+ shell: true
394
+ });
395
+ // Forward MCP exit code when it exits
396
+ mcpProcess.on('exit', (code) => {
397
+ process.exit(code ?? 0);
398
+ });
399
+ // Clean shutdown: forward signals to MCP child
400
+ const cleanup = () => {
401
+ mcpProcess.kill('SIGTERM');
402
+ setTimeout(() => process.exit(0), 100);
403
+ };
404
+ process.on('SIGINT', cleanup);
405
+ process.on('SIGTERM', cleanup);
406
+ }
407
+ catch (error) {
408
+ console.error('[squish] Failed to start MCP server:', error.message);
409
+ process.exit(1);
410
+ }
411
+ })();
254
412
  }
255
413
  else if (subcommand === 'web') {
256
414
  runWebOnly().catch((e) => {
@@ -259,33 +417,32 @@ else if (isRunCommand) {
259
417
  });
260
418
  }
261
419
  else {
262
- console.log(`
263
- Usage: squish run <command>
264
-
265
- Commands:
266
- mcp Start MCP server
267
- web Start Web UI only
268
-
269
- Examples:
270
- squish run mcp # Start MCP server with web UI
271
- squish run web # Start Web UI only
420
+ console.log(`
421
+ Usage: squish run <command>
422
+
423
+ Commands:
424
+ mcp Start MCP server (for agents like Claude Code)
425
+ web Start Web UI only
426
+
427
+ Examples:
428
+ squish run mcp # Start MCP server (agents connect automatically)
429
+ squish run web # Start Web UI at http://localhost:37777
272
430
  `);
273
431
  process.exit(subcommand ? 1 : 0);
274
432
  }
275
433
  }
276
434
  else if (isHelpCommand) {
277
- // === SHOW HELP ===
435
+ // SHOW HELP ===
278
436
  showHelp();
279
437
  process.exit(0);
280
438
  }
281
439
  else {
282
- // === CLI MODE (for agents/OpenClaw) ===
440
+ // CLI MODE (for agents/OpenClaw) ===
283
441
  runCliMode().catch((e) => {
284
442
  console.error(JSON.stringify({ error: e.message }, null, 2));
285
443
  process.exit(1);
286
444
  });
287
445
  }
288
- // ============================================================================
289
446
  // CLI MODE (for OpenClaw bash execution)
290
447
  // ============================================================================
291
448
  async function runCliMode() {
@@ -298,20 +455,136 @@ async function runCliMode() {
298
455
  program.hook('preAction', async () => {
299
456
  await ensureDataDirectory();
300
457
  });
458
+ // squish config [get] [key] or squish config set <key> <value>
459
+ program
460
+ .command('config')
461
+ .description('Manage Squish configuration')
462
+ .argument('[action]', 'get, set, or list', 'list')
463
+ .argument('[key]', 'Config key (e.g., project)')
464
+ .argument('[value]', 'Config value (for set action)')
465
+ .action(async (action, key, value) => {
466
+ const userConfig = loadUserConfig();
467
+ if (action === 'set') {
468
+ if (!key || value === undefined) {
469
+ console.log(JSON.stringify({ ok: false, error: 'Usage: squish config set <key> <value>' }, null, 2));
470
+ process.exit(1);
471
+ }
472
+ userConfig[key] = value;
473
+ saveUserConfig(userConfig);
474
+ console.log(JSON.stringify({ ok: true, message: `Set ${key} = ${value}` }, null, 2));
475
+ }
476
+ else if (action === 'get') {
477
+ if (!key) {
478
+ console.log(JSON.stringify({ ok: false, error: 'Usage: squish config get <key>' }, null, 2));
479
+ process.exit(1);
480
+ }
481
+ console.log(JSON.stringify({ ok: true, [key]: userConfig[key] || null }, null, 2));
482
+ }
483
+ else {
484
+ console.log(JSON.stringify({ ok: true, config: userConfig }, null, 2));
485
+ }
486
+ });
487
+ // squish mount /path/to/folder - Enable external memory
488
+ program
489
+ .command('mount')
490
+ .description('Mount an external folder as memory storage')
491
+ .argument('[path]', 'Path to external folder (or "status" or "unmount")')
492
+ .action(async (pathOrAction) => {
493
+ const { getExternalMemory } = await import('./core/external-folder/index.js');
494
+ const externalMemory = getExternalMemory();
495
+ if (pathOrAction === 'status') {
496
+ // Show mount status
497
+ const status = await externalMemory.getStatus();
498
+ console.log(JSON.stringify({ ok: true, status }, null, 2));
499
+ }
500
+ else if (pathOrAction === 'unmount') {
501
+ // Unmount
502
+ externalMemory.unmount();
503
+ console.log(JSON.stringify({ ok: true, message: 'External memory unmounted' }, null, 2));
504
+ }
505
+ else if (pathOrAction) {
506
+ // Mount at path
507
+ const result = await externalMemory.mount(pathOrAction);
508
+ if (result.success) {
509
+ console.log(JSON.stringify({ ok: true, message: `Mounted at ${pathOrAction}` }, null, 2));
510
+ }
511
+ else {
512
+ console.log(JSON.stringify({ ok: false, error: result.error }, null, 2));
513
+ }
514
+ }
515
+ else {
516
+ console.log(JSON.stringify({ ok: false, error: 'Usage: squish mount <path> or squish mount status' }, null, 2));
517
+ }
518
+ });
301
519
  // squish remember "content" --type fact --tags tag1,tag2
302
520
  program
303
521
  .command('remember <content>')
304
522
  .description('Store a memory')
305
523
  .option('-t, --type <type>', 'Memory type (observation, fact, decision, context, preference)', 'observation')
306
524
  .option('-T, --tags <tags>', 'Comma-separated tags', '')
307
- .option('-p, --project <project>', 'Project path', process.cwd())
525
+ .option('-p, --project <project>', 'Project path', getDefaultProjectPath())
526
+ .option('-s, --source <source>', 'Source of this memory (e.g., "voice", "chat", "document")')
527
+ .option('-r, --reasoning <reasoning>', 'Why this memory is important')
528
+ .option('-c, --context <context>', 'What triggered this memory')
529
+ .option('-e, --examples <examples>', 'When to apply this knowledge')
530
+ .option('-x, --exceptions <exceptions>', 'When NOT to apply this')
531
+ .option('-m, --memory', 'Store as markdown file in .squish/memory/ (not in database)', false)
532
+ .option('-H, --hot', 'Store in hot tier (active, high priority) in database', false)
533
+ .option('-C, --cold', 'Store in cold tier (archived, lower priority) in database', false)
308
534
  .action(async (content, options) => {
309
535
  try {
536
+ // Markdown file storage (not in database)
537
+ if (options.memory) {
538
+ const { saveToMarkdown } = await import('./core/memory/markdown/markdown-storage.js');
539
+ const memoryFile = await saveToMarkdown({
540
+ content,
541
+ type: options.type,
542
+ tags: options.tags ? options.tags.split(',').map((t) => t.trim()) : [],
543
+ project: options.project,
544
+ source: options.source,
545
+ reasoning: options.reasoning,
546
+ memoryContext: options.context,
547
+ examples: options.examples,
548
+ exceptions: options.exceptions,
549
+ });
550
+ // Trigger hooks
551
+ const { triggerMemoryCreated } = await import('./core/memory/hooks.js');
552
+ await triggerMemoryCreated({
553
+ memoryId: memoryFile.id,
554
+ content: memoryFile.content,
555
+ type: memoryFile.type,
556
+ tags: memoryFile.tags,
557
+ project: memoryFile.project,
558
+ source: memoryFile.source,
559
+ tier: 'hot',
560
+ });
561
+ console.log(JSON.stringify({ ok: true, memory: true, ...memoryFile }, null, 2));
562
+ return;
563
+ }
564
+ // Database storage: determine tier
565
+ const tier = options.cold ? 'cold' : 'hot';
310
566
  const result = await rememberMemory({
311
567
  content,
312
568
  type: options.type,
313
569
  tags: options.tags ? options.tags.split(',').map((t) => t.trim()) : [],
314
570
  project: options.project,
571
+ source: options.source,
572
+ reasoning: options.reasoning,
573
+ memoryContext: options.context,
574
+ examples: options.examples,
575
+ exceptions: options.exceptions,
576
+ tier,
577
+ });
578
+ // Trigger hooks for DB storage
579
+ const { triggerMemoryCreated } = await import('./core/memory/hooks.js');
580
+ await triggerMemoryCreated({
581
+ memoryId: result.id,
582
+ content: result.content,
583
+ type: result.type,
584
+ tags: result.tags,
585
+ project: result.projectId || undefined,
586
+ source: options.source || 'cli',
587
+ tier,
315
588
  });
316
589
  console.log(JSON.stringify({ ok: true, ...result }, null, 2));
317
590
  }
@@ -320,218 +593,559 @@ async function runCliMode() {
320
593
  process.exit(1);
321
594
  }
322
595
  });
323
- // squish search "query" --type fact --limit 10
596
+ // squish search "query" --type fact --limit 10 --since "3 days ago" --place workshop
324
597
  program
325
598
  .command('search <query>')
326
599
  .description('Search memories')
327
600
  .option('-t, --type <type>', 'Filter by memory type')
328
601
  .option('-l, --limit <number>', 'Max results', '10')
329
- .option('-p, --project <project>', 'Project path', process.cwd())
602
+ .option('-p, --project <project>', 'Project path', getDefaultProjectPath())
603
+ .option('-s, --since <date>', 'Filter: created after this date (e.g., "3 days ago", "2026-01-01")')
604
+ .option('-u, --until <date>', 'Filter: created before this date (e.g., "yesterday", "2026-01-15")')
605
+ .option('-P, --pretty', 'Human-friendly output', false)
606
+ .option('-m, --memory', 'Search memory files instead of database', false)
607
+ .option('--place <type>', 'Filter by place type: entry_hall, library, workshop, lab, office, garden, archive')
330
608
  .action(async (query, options) => {
331
609
  try {
332
- const results = await searchMemories({
610
+ // Markdown file search
611
+ if (options.memory) {
612
+ const { getMarkdownMemories } = await import('./core/memory/markdown/markdown-storage.js');
613
+ const memoryFiles = await getMarkdownMemories({
614
+ type: options.type,
615
+ project: options.project,
616
+ });
617
+ // Simple text search (can be enhanced with QMD later)
618
+ const searchLower = query.toLowerCase();
619
+ const filtered = memoryFiles.filter(m => m.content.toLowerCase().includes(searchLower) ||
620
+ m.tags.some(t => t.toLowerCase().includes(searchLower))).slice(0, validateLimit(options.limit, 10, 1, 100));
621
+ if (options.pretty) {
622
+ console.log(`\n Memory Search: "${query}"`);
623
+ console.log(` Found ${filtered.length} results:\n`);
624
+ filtered.forEach((r, i) => {
625
+ console.log(` ${i + 1}. [${r.type || 'memory'}] ${(r.content || '').substring(0, 60)}...`);
626
+ });
627
+ console.log('');
628
+ }
629
+ else {
630
+ console.log(JSON.stringify({ ok: true, query, source: 'memory', count: filtered.length, results: filtered }, null, 2));
631
+ }
632
+ return;
633
+ }
634
+ // Database search
635
+ const results = await search({
333
636
  query,
334
637
  type: options.type,
335
- limit: parseInt(options.limit, 10),
638
+ limit: validateLimit(options.limit, 10, 1, 100) * 2,
336
639
  project: options.project,
337
640
  });
338
- console.log(JSON.stringify({ ok: true, query, count: results?.length || 0, results }, null, 2));
641
+ const filtered = filterByDateRange(results, options.since, options.until);
642
+ let limited = filtered.slice(0, validateLimit(options.limit, 10, 1, 100));
643
+ // Add place info to results
644
+ const { getMemoryPlace } = await import('./core/places/index.js');
645
+ const limitedWithPlace = await Promise.all(limited.map(async (r) => {
646
+ const placeId = await getMemoryPlace(r.id);
647
+ return { ...r, placeId };
648
+ }));
649
+ // Filter by place if specified
650
+ if (options.place) {
651
+ const placeFiltered = [];
652
+ for (const r of limitedWithPlace) {
653
+ if (r.placeId) {
654
+ const { getPlace } = await import('./core/places/index.js');
655
+ const place = await getPlace(r.placeId);
656
+ if (place && place.placeType === options.place) {
657
+ placeFiltered.push({ ...r, place: place.name || null, placeType: place.placeType || null });
658
+ }
659
+ }
660
+ }
661
+ limited = placeFiltered;
662
+ }
663
+ else if (limitedWithPlace.length > 0) {
664
+ // Add place info to results even without filter
665
+ for (const r of limitedWithPlace) {
666
+ if (r.placeId) {
667
+ const { getPlace } = await import('./core/places/index.js');
668
+ const place = await getPlace(r.placeId);
669
+ if (place) {
670
+ r.place = place.name || null;
671
+ r.placeType = place.placeType || null;
672
+ }
673
+ }
674
+ }
675
+ limited = limitedWithPlace;
676
+ }
677
+ if (options.pretty) {
678
+ console.log(`\n Search: "${query}"`);
679
+ console.log(` Found ${limited.length} results:\n`);
680
+ limited.forEach((r, i) => {
681
+ const placeTag = r.place ? ` (${r.place})` : '';
682
+ console.log(` ${i + 1}. [${r.type || 'memory'}] ${(r.content || '').substring(0, 60)}...${placeTag}`);
683
+ });
684
+ console.log('');
685
+ }
686
+ else {
687
+ console.log(JSON.stringify({ ok: true, query, count: limited.length, since: options.since, until: options.until, placeFilter: options.place || null, results: limited }, null, 2));
688
+ }
339
689
  }
340
690
  catch (error) {
341
691
  console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
342
692
  process.exit(1);
343
693
  }
344
694
  });
345
- // squish recall <memoryId>
695
+ // squish forget <memoryId> -- Delete single or bulk delete memories
346
696
  program
347
- .command('recall <memoryId>')
348
- .description('Retrieve a memory by ID')
349
- .action(async (memoryId) => {
697
+ .command('forget [memoryId]')
698
+ .description('Delete a memory by ID, or bulk delete with filters')
699
+ .option('-o, --older-than <date>', 'Bulk delete memories older than (e.g., "30 days", "6 months")')
700
+ .option('-t, --type <type>', 'Filter by memory type')
701
+ .option('-s, --search <query>', 'Search query to match specific memories')
702
+ .option('-c, --confirm', 'Actually delete (default is dry-run)', false)
703
+ .option('-l, --limit <number>', 'Max memories to delete', '100')
704
+ .option('-p, --project <project>', 'Project path', getDefaultProjectPath())
705
+ .action(async (memoryId, options) => {
350
706
  try {
351
- const memory = await getMemoryById(String(memoryId));
352
- console.log(JSON.stringify({ ok: true, found: !!memory, memory }, null, 2));
707
+ // Single memory deletion
708
+ if (memoryId) {
709
+ const db = await getDb();
710
+ const schema = await getSchema();
711
+ const sqliteDb = db;
712
+ // Get memory content before deleting for hook
713
+ const [memory] = await sqliteDb.select().from(schema.memories).where(eq(schema.memories.id, memoryId));
714
+ await sqliteDb.delete(schema.memories).where(eq(schema.memories.id, memoryId));
715
+ // Trigger memoryDeleted hook
716
+ if (memory) {
717
+ const { triggerMemoryDeleted } = await import('./core/memory/hooks.js');
718
+ await triggerMemoryDeleted({
719
+ memoryId: memory.id,
720
+ content: memory.content,
721
+ type: memory.type,
722
+ tags: typeof memory.tags === 'string' ? memory.tags.split(',') : [],
723
+ project: memory.projectId || undefined,
724
+ source: memory.source || undefined,
725
+ tier: memory.tier,
726
+ });
727
+ }
728
+ console.log(JSON.stringify({ ok: true, message: `Memory ${memoryId} deleted` }, null, 2));
729
+ return;
730
+ }
731
+ // Bulk deletion
732
+ if (!options.olderThan && !options.search) {
733
+ console.log(JSON.stringify({ ok: false, error: 'Provide memory ID or use --older-than / --search for bulk delete' }, null, 2));
734
+ process.exit(1);
735
+ }
736
+ const query = options.search || '';
737
+ const limit = validateLimit(options.limit, 100, 1, 100);
738
+ const results = await search({ query, type: options.type, limit, project: options.project });
739
+ let filtered = results;
740
+ if (options.olderThan) {
741
+ filtered = filterByDateRange(results, '', options.olderThan);
742
+ }
743
+ const db = await getDb();
744
+ const schema = await getSchema();
745
+ const sqliteDb = db;
746
+ const deleted = [];
747
+ for (const mem of filtered) {
748
+ await sqliteDb.delete(schema.memories).where(eq(schema.memories.id, mem.id));
749
+ deleted.push(mem.id);
750
+ }
751
+ console.log(JSON.stringify({ ok: true, matched: filtered.length, deleted: deleted.length, dryRun: !options.confirm }, null, 2));
353
752
  }
354
753
  catch (error) {
355
754
  console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
356
755
  process.exit(1);
357
756
  }
358
757
  });
359
- // squish core_memory view
360
- // squish core_memory edit persona --content "I am helpful"
361
- // squish core_memory append user_info --text "Prefers TypeScript"
758
+ // squish link - Unified graph operations (find related, add links, list associations)
362
759
  program
363
- .command('core_memory')
364
- .description('Manage core memory (always-visible context)')
365
- .argument('[action]', 'view, edit, append', 'view')
366
- .option('-s, --section <section>', 'Section: persona, user_info, project_context, working_notes')
367
- .option('-c, --content <content>', 'New content (for edit)')
368
- .option('-t, --text <text>', 'Text to append (for append)')
369
- .option('-p, --project <project>', 'Project path', process.cwd())
370
- .action(async (action, options) => {
760
+ .command('link')
761
+ .description('Manage memory associations: find, add, list')
762
+ .argument('<action>', 'Action: find, add, or list')
763
+ .argument('[args...]', 'Additional arguments')
764
+ .option('-p, --project <project>', 'Project path', getDefaultProjectPath())
765
+ .action(async (action, args, options) => {
371
766
  try {
372
- const projectPath = options.project;
373
- const projectRecord = await ensureProject(projectPath);
374
- if (!projectRecord) {
375
- console.log(JSON.stringify({ ok: false, error: 'Project not found and could not be created' }, null, 2));
376
- process.exit(1);
767
+ // link find <memoryId> [--depth N] [--min-weight N]
768
+ if (action === 'find') {
769
+ const memoryId = args[0];
770
+ if (!memoryId) {
771
+ console.log(JSON.stringify({ ok: false, error: 'Usage: squish link find <memoryId> [--depth N] [--min-weight N]' }, null, 2));
772
+ process.exit(1);
773
+ }
774
+ const depth = validateLimit(args[1], 2, 1, 5);
775
+ const minWeight = parseFloat(args[2]) || 0.3;
776
+ const related = await getRelatedMemories(memoryId, depth * 5);
777
+ const filtered = related.filter((r) => r.weight >= minWeight);
778
+ console.log(JSON.stringify({ ok: true, count: filtered.length, related: filtered }, null, 2));
779
+ return;
377
780
  }
378
- const projectId = projectRecord.id;
379
- switch (action) {
380
- case 'view':
381
- await initializeCoreMemory(projectId);
382
- const core = await getCoreMemory(projectId);
383
- const stats = await getCoreMemoryStats(projectId);
384
- console.log(JSON.stringify({ ok: true, action, content: core, stats }, null, 2));
385
- break;
386
- case 'edit':
387
- if (!options.section || !options.content) {
388
- console.log(JSON.stringify({ ok: false, error: '--section and --content required for edit' }, null, 2));
389
- process.exit(1);
390
- }
391
- await initializeCoreMemory(projectId);
392
- const editResult = await editCoreMemorySection(projectId, options.section, String(options.content));
393
- console.log(JSON.stringify({ ok: editResult.success, action: 'edit', section: options.section, ...editResult }, null, 2));
394
- break;
395
- case 'append':
396
- if (!options.section || !options.text) {
397
- console.log(JSON.stringify({ ok: false, error: '--section and --text required for append' }, null, 2));
398
- process.exit(1);
399
- }
400
- await initializeCoreMemory(projectId);
401
- const appendResult = await appendCoreMemorySection(projectId, options.section, String(options.text));
402
- console.log(JSON.stringify({ ok: appendResult.success, action: 'append', section: options.section, ...appendResult }, null, 2));
403
- break;
404
- default:
405
- console.log(JSON.stringify({ ok: false, error: `Unknown action: ${action}` }, null, 2));
781
+ // link add <fromId> <toId> <type>
782
+ if (action === 'add') {
783
+ const fromMemoryId = args[0];
784
+ const toMemoryId = args[1];
785
+ const type = args[2] || 'relates_to';
786
+ if (!fromMemoryId || !toMemoryId) {
787
+ console.log(JSON.stringify({ ok: false, error: 'Usage: squish link add <fromId> <toId> <type>' }, null, 2));
406
788
  process.exit(1);
789
+ }
790
+ await createAssociation(fromMemoryId, toMemoryId, type, 0.5);
791
+ console.log(JSON.stringify({ ok: true, message: `Linked ${fromMemoryId} -> ${toMemoryId} (${type})` }, null, 2));
792
+ return;
793
+ }
794
+ // link list - List all associations
795
+ if (action === 'list') {
796
+ const db = await getDb();
797
+ const schema = await getSchema();
798
+ const sqliteDb = db;
799
+ const associations = await sqliteDb.select().from(schema.memoryAssociations).limit(100);
800
+ console.log(JSON.stringify({ ok: true, count: associations.length, associations }, null, 2));
801
+ return;
407
802
  }
803
+ console.log(JSON.stringify({ ok: false, error: 'Usage: squish link <find|add|list> [args]' }, null, 2));
408
804
  }
409
805
  catch (error) {
410
806
  console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
411
807
  process.exit(1);
412
808
  }
413
809
  });
414
- // squish set-importance <memoryId> --importance 80
810
+ // squish learn <type> <content> - Record learning: success, failure, fix, or insight
415
811
  program
416
- .command('set-importance <memoryId>')
417
- .description('Manually set importance score for a memory (0-100)')
418
- .option('-i, --importance <number>', 'Importance score (0-100)', '50')
419
- .action(async (memoryId, options) => {
812
+ .command('learn <type> <content>')
813
+ .description('Record learning: success, failure, fix, or insight')
814
+ .option('-c, --context <context>', 'Additional context about what happened')
815
+ .option('-a, --action <action>', 'Action performed')
816
+ .option('-t, --target <target>', 'Target file or resource')
817
+ .option('-m, --memory-id <memoryId>', 'Optional memory ID to link this learning to')
818
+ .option('-p, --project <project>', 'Project path', getDefaultProjectPath())
819
+ .action(async (type, content, options) => {
420
820
  try {
421
- const score = parseInt(options.importance, 10);
422
- if (isNaN(score) || score < 0 || score > 100) {
423
- console.log(JSON.stringify({ ok: false, error: 'Importance must be between 0 and 100' }, null, 2));
821
+ const validTypes = ['success', 'failure', 'fix', 'insight'];
822
+ if (!validTypes.includes(type)) {
823
+ console.log(JSON.stringify({ ok: false, error: `Invalid type. Must be: ${validTypes.join(', ')}` }, null, 2));
424
824
  process.exit(1);
425
825
  }
426
- await setImportanceScore(String(memoryId), score);
427
- console.log(JSON.stringify({ ok: true, memoryId, importanceScore: score }, null, 2));
826
+ const learning = await createLearning({
827
+ type: type,
828
+ content,
829
+ context: options.context,
830
+ action: options.action,
831
+ target: options.target,
832
+ project: options.project,
833
+ memoryId: options.memoryId,
834
+ });
835
+ console.log(JSON.stringify({ ok: true, learning }, null, 2));
428
836
  }
429
837
  catch (error) {
430
838
  console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
431
839
  process.exit(1);
432
840
  }
433
841
  });
434
- // squish pin <memoryId>
435
842
  program
436
- .command('pin <memoryId>')
437
- .description('Pin a memory to prevent pruning/consolidation')
438
- .action(async (memoryId) => {
843
+ .command('update <memoryId>')
844
+ .description('Update a memory')
845
+ .option('-c, --content <content>', 'New content')
846
+ .option('-t, --type <type>', 'New type (observation, fact, decision, context, preference)')
847
+ .option('-T, --tags <tags>', 'Comma-separated tags')
848
+ .action(async (memoryId, options) => {
439
849
  try {
440
- await pinMemory(String(memoryId));
441
- console.log(JSON.stringify({ ok: true, memoryId, pinned: true }, null, 2));
850
+ const updates = {};
851
+ if (options.content)
852
+ updates.content = options.content;
853
+ if (options.type)
854
+ updates.type = options.type;
855
+ if (options.tags)
856
+ updates.tags = serializeTags(options.tags.split(','));
857
+ const db = await getDb();
858
+ const schema = await getSchema();
859
+ const sqliteDb = db;
860
+ // Get old memory for hook
861
+ const [oldMemory] = await sqliteDb.select().from(schema.memories).where(eq(schema.memories.id, memoryId));
862
+ await sqliteDb.update(schema.memories).set(updates).where(eq(schema.memories.id, memoryId));
863
+ // Trigger memoryUpdated hook
864
+ if (oldMemory) {
865
+ const { triggerMemoryUpdated } = await import('./core/memory/hooks.js');
866
+ const newContent = options.content || oldMemory.content;
867
+ await triggerMemoryUpdated({
868
+ memoryId: oldMemory.id,
869
+ content: newContent,
870
+ type: options.type || oldMemory.type,
871
+ tags: options.tags ? options.tags.split(',') : (typeof oldMemory.tags === 'string' ? oldMemory.tags.split(',') : []),
872
+ project: oldMemory.projectId || undefined,
873
+ source: oldMemory.source || undefined,
874
+ tier: oldMemory.tier,
875
+ importance: oldMemory.importanceScore || oldMemory.relevanceScore || 50,
876
+ }, oldMemory.content);
877
+ }
878
+ console.log(JSON.stringify({ ok: true, message: `Memory ${memoryId} updated` }, null, 2));
442
879
  }
443
880
  catch (error) {
444
881
  console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
445
882
  process.exit(1);
446
883
  }
447
884
  });
448
- // squish unpin <memoryId>
885
+ // squish recall <query or memoryId> - Search or get by ID
449
886
  program
450
- .command('unpin <memoryId>')
451
- .description('Unpin a memory')
452
- .action(async (memoryId) => {
887
+ .command('recall <query>')
888
+ .description('Search memories by query or get by ID (if UUID provided)')
889
+ .option('-l, --limit <number>', 'Max results', '5')
890
+ .option('-t, --type <type>', 'Filter by memory type')
891
+ .option('-p, --project <project>', 'Project path', getDefaultProjectPath())
892
+ .option('-s, --since <date>', 'Filter: created after this date (e.g., "3 days ago", "yesterday")')
893
+ .option('-u, --until <date>', 'Filter: created before this date (e.g., "today", "2026-01-15")')
894
+ .option('-P, --pretty', 'Human-friendly output', false)
895
+ .option('--place <type>', 'Filter by place type: entry_hall, library, workshop, lab, office, garden, archive')
896
+ .action(async (query, options) => {
453
897
  try {
454
- await unpinMemory(String(memoryId));
455
- console.log(JSON.stringify({ ok: true, memoryId, pinned: false }, null, 2));
898
+ const isUUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(query);
899
+ if (isUUID) {
900
+ const memory = await getMemory(query);
901
+ // Add place info to single memory retrieval
902
+ if (memory) {
903
+ const { getMemoryPlace, getPlace } = await import('./core/places/index.js');
904
+ const placeId = await getMemoryPlace(memory.id);
905
+ if (placeId) {
906
+ const place = await getPlace(placeId);
907
+ memory.place = place?.name || null;
908
+ memory.placeType = place?.placeType || null;
909
+ }
910
+ }
911
+ if (options.pretty && memory) {
912
+ const placeInfo = memory.place ? ` (${memory.place})` : '';
913
+ console.log(`\n Memory: ${memory.id}`);
914
+ console.log(` Type: ${memory.type}`);
915
+ console.log(` Content: ${memory.content}\n`);
916
+ if (placeInfo)
917
+ console.log(` Place: ${memory.place}\n`);
918
+ }
919
+ else {
920
+ console.log(JSON.stringify({ ok: true, found: !!memory, memory }, null, 2));
921
+ }
922
+ }
923
+ else {
924
+ const results = await search({
925
+ query,
926
+ type: options.type,
927
+ limit: validateLimit(options.limit, 5, 1, 100) * 2,
928
+ project: options.project,
929
+ });
930
+ const filtered = filterByDateRange(results, options.since, options.until);
931
+ let limited = filtered.slice(0, validateLimit(options.limit, 5, 1, 100));
932
+ // Add place info to results
933
+ const { getMemoryPlace, getPlace } = await import('./core/places/index.js');
934
+ const limitedWithPlace = await Promise.all(limited.map(async (r) => {
935
+ const placeId = await getMemoryPlace(r.id);
936
+ let placeInfo = {};
937
+ if (placeId) {
938
+ const place = await getPlace(placeId);
939
+ placeInfo = { place: place?.name || null, placeType: place?.placeType || null };
940
+ }
941
+ return { ...r, ...placeInfo };
942
+ }));
943
+ // Filter by place if specified
944
+ if (options.place) {
945
+ limited = limitedWithPlace.filter((r) => r.placeType === options.place);
946
+ }
947
+ else {
948
+ limited = limitedWithPlace;
949
+ }
950
+ if (options.pretty) {
951
+ console.log(`\n Recall: "${query}"`);
952
+ console.log(` Found ${limited.length} matches:\n`);
953
+ limited.forEach((r, i) => {
954
+ const placeTag = r.place ? ` (${r.place})` : '';
955
+ console.log(` ${i + 1}. [${r.type || 'memory'}] ${(r.content || '').substring(0, 60)}...${placeTag} (${(r.similarity ?? 0).toFixed(2)})`);
956
+ });
957
+ console.log('');
958
+ }
959
+ else {
960
+ const matches = limited.map((r) => ({
961
+ id: r.id,
962
+ score: r.similarity ?? 0,
963
+ type: r.type,
964
+ content: r.content.length > 200 ? r.content.slice(0, 200) + '...' : r.content,
965
+ tags: r.tags,
966
+ place: r.place,
967
+ placeType: r.placeType,
968
+ }));
969
+ console.log(JSON.stringify({ ok: true, query, count: matches.length, since: options.since, until: options.until, placeFilter: options.place || null, matches }, null, 2));
970
+ }
971
+ }
456
972
  }
457
973
  catch (error) {
458
974
  console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
459
975
  process.exit(1);
460
976
  }
461
977
  });
462
- // squish consolidate --project-id <id> --min-age 90
978
+ // squish recent --period <period> - Show recent memories
463
979
  program
464
- .command('consolidate')
465
- .description('Trigger manual memory consolidation')
466
- .option('-p, --project-id <id>', 'Project ID', process.cwd())
467
- .option('-a, --min-age <number>', 'Minimum age in days', '90')
468
- .option('-i, --max-importance <number>', 'Maximum importance to consolidate', '30')
469
- .option('-t, --threshold <number>', 'Similarity threshold (0-1)', '0.7')
470
- .option('-l, --limit <number>', 'Max memories to process', '100')
980
+ .command('recent')
981
+ .description('Show recent memories by period')
982
+ .option('-p, --period <period>', 'Period: today, yesterday, thisweek, 7days, 30days, or custom like "3 days"', 'today')
983
+ .option('-s, --since <date>', 'Start date (alternative to --period)')
984
+ .option('-u, --until <date>', 'End date (alternative to --period)')
985
+ .option('-l, --limit <number>', 'Max results', '10')
986
+ .option('-P, --project <project>', 'Project path', getDefaultProjectPath())
471
987
  .action(async (options) => {
472
988
  try {
473
- const results = await consolidateMemoriesImpl({
474
- projectId: String(options.projectId),
475
- minAge: parseInt(options.minAge, 10),
476
- maxImportance: parseInt(options.maxImportance, 10),
477
- similarityThreshold: parseFloat(options.threshold),
478
- limit: parseInt(options.limit, 10),
479
- });
480
- console.log(JSON.stringify({ ok: true, consolidated: results.length, results }, null, 2));
989
+ let since, until;
990
+ if (options.since && options.until) {
991
+ since = options.since;
992
+ until = options.until;
993
+ }
994
+ else if (options.since) {
995
+ since = options.since;
996
+ until = 'now';
997
+ }
998
+ else {
999
+ const periodMap = {
1000
+ today: ['today', 'now'],
1001
+ yesterday: ['yesterday', 'today'],
1002
+ thisweek: ['thisweek', 'now'],
1003
+ '7days': ['7 days', 'now'],
1004
+ '14days': ['14 days', 'now'],
1005
+ '30days': ['30 days', 'now'],
1006
+ '90days': ['90 days', 'now'],
1007
+ };
1008
+ const mapped = periodMap[options.period];
1009
+ if (mapped) {
1010
+ [since, until] = mapped;
1011
+ }
1012
+ else {
1013
+ since = options.period;
1014
+ until = 'now';
1015
+ }
1016
+ }
1017
+ const results = await getRecent(options.project, 100);
1018
+ const filtered = filterByDateRange(results, since, until);
1019
+ const limited = filtered.slice(0, validateLimit(options.limit, 10, 1, 100));
1020
+ console.log(JSON.stringify({ ok: true, period: options.period, since, until, count: limited.length, results: limited }, null, 2));
481
1021
  }
482
1022
  catch (error) {
483
1023
  console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
484
1024
  process.exit(1);
485
1025
  }
486
1026
  });
487
- // squish consolidation-stats --project-id <id>
1027
+ // squish confidence <memoryId> [level] - Set or view confidence level
488
1028
  program
489
- .command('consolidation-stats')
490
- .description('Get consolidation statistics for a project')
491
- .option('-p, --project-id <id>', 'Project ID', process.cwd())
492
- .action(async (options) => {
1029
+ .command('confidence <memoryId> [level]')
1030
+ .description('Set or view confidence level (certain/speculative/outdated)')
1031
+ .action(async (memoryId, level) => {
493
1032
  try {
494
- const stats = await getConsolidationStats(String(options.projectId));
495
- console.log(JSON.stringify({ ok: true, ...stats }, null, 2));
1033
+ if (!level) {
1034
+ const memory = await getMemory(String(memoryId));
1035
+ if (!memory) {
1036
+ console.log(JSON.stringify({ ok: false, error: 'Memory not found' }, null, 2));
1037
+ process.exit(1);
1038
+ }
1039
+ console.log(JSON.stringify({ ok: true, memoryId, confidenceLevel: memory.confidenceLevel ?? 'certain' }, null, 2));
1040
+ }
1041
+ else {
1042
+ const validLevels = ['certain', 'speculative', 'outdated'];
1043
+ if (!validLevels.includes(level)) {
1044
+ console.log(JSON.stringify({ ok: false, error: 'Invalid level. Use: certain, speculative, or outdated' }, null, 2));
1045
+ process.exit(1);
1046
+ }
1047
+ await setConfidence(String(memoryId), level);
1048
+ console.log(JSON.stringify({ ok: true, memoryId, confidenceLevel: level }, null, 2));
1049
+ }
496
1050
  }
497
1051
  catch (error) {
498
1052
  console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
499
1053
  process.exit(1);
500
1054
  }
501
1055
  });
502
- // squish health
1056
+ // squish pin <memoryId> [--unpin]
503
1057
  program
504
- .command('health')
505
- .description('Check service health and configuration')
506
- .option('-j, --json', 'Output as JSON', false)
507
- .action(async (options) => {
1058
+ .command('pin <memoryId>')
1059
+ .description('Pin/unpin a memory to prevent pruning/consolidation')
1060
+ .option('-u, --unpin', 'Unpin the memory instead of pinning', false)
1061
+ .action(async (memoryId, options) => {
508
1062
  try {
509
- const dbHealth = await checkDatabaseHealth();
510
- const redisHealth = await checkRedisHealth();
511
- const dataDir = process.env.SQUISH_DATA_DIR || path.join(os.homedir(), '.squish');
512
- const dirExists = fs.existsSync(dataDir);
513
- const status = {
514
- version: VERSION,
515
- mode: config.isTeamMode ? 'team' : 'local',
516
- database: dbHealth ? 'ok' : 'error',
517
- cache: redisHealth ? 'ok' : 'unavailable',
518
- dataDirectory: dataDir,
519
- dataDirectoryExists: dirExists,
520
- timestamp: new Date().toISOString()
521
- };
522
- if (options.json) {
523
- console.log(JSON.stringify({ ok: true, ...status }, null, 2));
1063
+ if (options.unpin) {
1064
+ await unpinMemory(String(memoryId));
1065
+ console.log(JSON.stringify({ ok: true, memoryId, pinned: false }, null, 2));
1066
+ }
1067
+ else {
1068
+ await pinMemory(String(memoryId));
1069
+ console.log(JSON.stringify({ ok: true, memoryId, pinned: true }, null, 2));
1070
+ }
1071
+ }
1072
+ catch (error) {
1073
+ console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
1074
+ process.exit(1);
1075
+ }
1076
+ });
1077
+ // squish tag add <tag> --search <query> --confirm
1078
+ // squish tag remove <tag> --older-than "30 days" --confirm
1079
+ program
1080
+ .command('tag')
1081
+ .description('Manage tags on memories (bulk)')
1082
+ .argument('<action>', 'add or remove')
1083
+ .argument('<tag>', 'Tag name')
1084
+ .option('-s, --search <query>', 'Search query to match memories')
1085
+ .option('-o, --older-than <date>', 'Only tag memories older than (e.g., "30 days")')
1086
+ .option('-t, --type <type>', 'Filter by memory type')
1087
+ .option('-c, --confirm', 'Actually execute the changes (default is dry-run)', false)
1088
+ .option('-l, --limit <number>', 'Max memories to process', '50')
1089
+ .option('-p, --project <project>', 'Project path', getDefaultProjectPath())
1090
+ .action(async (action, tag, options) => {
1091
+ try {
1092
+ if (!options.search && !options.olderThan) {
1093
+ console.log(JSON.stringify({ ok: false, error: 'Provide --search <query> or --older-than <date>' }, null, 2));
1094
+ process.exit(1);
1095
+ }
1096
+ const limit = validateLimit(options.limit, 50, 1, 100);
1097
+ let results;
1098
+ const searchInput = { query: options.search, limit, project: options.project };
1099
+ if (options.type)
1100
+ searchInput.type = options.type;
1101
+ if (options.search) {
1102
+ results = await search(searchInput);
1103
+ }
1104
+ else {
1105
+ results = await getRecent(options.project, limit * 2);
1106
+ }
1107
+ let filtered = results;
1108
+ if (options.olderThan) {
1109
+ filtered = filterByDateRange(results, '', options.olderThan);
1110
+ }
1111
+ const db = await getDb();
1112
+ const schema = await getSchema();
1113
+ if (action === 'add') {
1114
+ const updated = [];
1115
+ for (const mem of filtered) {
1116
+ try {
1117
+ const tags = new Set((mem.tags || []));
1118
+ if (!tags.has(tag)) {
1119
+ tags.add(tag);
1120
+ await db.update(schema.memories)
1121
+ .set({ tags: serializeTags(Array.from(tags)), updatedAt: new Date() })
1122
+ .where(eq(schema.memories.id, mem.id));
1123
+ updated.push(mem.id);
1124
+ }
1125
+ }
1126
+ catch (e) {
1127
+ console.error('DEBUG: error updating', mem.id, e.message);
1128
+ throw e;
1129
+ }
1130
+ }
1131
+ console.log(JSON.stringify({ ok: true, action: 'add', tag, matched: filtered.length, updated: updated.length, dryRun: !options.confirm }, null, 2));
1132
+ }
1133
+ else if (action === 'remove') {
1134
+ const updated = [];
1135
+ for (const mem of filtered) {
1136
+ const tags = new Set((mem.tags || []));
1137
+ if (tags.has(tag)) {
1138
+ tags.delete(tag);
1139
+ await db.update(schema.memories)
1140
+ .set({ tags: serializeTags(Array.from(tags)), updatedAt: new Date() })
1141
+ .where(eq(schema.memories.id, mem.id));
1142
+ updated.push(mem.id);
1143
+ }
1144
+ }
1145
+ console.log(JSON.stringify({ ok: true, action: 'remove', tag, matched: filtered.length, updated: updated.length, dryRun: !options.confirm }, null, 2));
524
1146
  }
525
1147
  else {
526
- console.log(`\n Squish Memory v${VERSION}`);
527
- console.log(` ====================`);
528
- console.log(` Mode: ${status.mode}`);
529
- console.log(` Database: ${status.database}`);
530
- console.log(` Cache: ${status.cache}`);
531
- console.log(` Data Dir: ${status.dataDirectory}`);
532
- console.log(` Status: ${dbHealth ? 'HEALTHY' : 'UNHEALTHY'}\n`);
533
- }
534
- if (!dbHealth) {
1148
+ console.log(JSON.stringify({ ok: false, error: 'Use: squish tag add <tag> or squish tag remove <tag>' }, null, 2));
535
1149
  process.exit(1);
536
1150
  }
537
1151
  }
@@ -540,13 +1154,62 @@ async function runCliMode() {
540
1154
  process.exit(1);
541
1155
  }
542
1156
  });
1157
+ // squish stale --days 30 - Show old, low-confidence, unaccessed memories
1158
+ program
1159
+ .command('stale')
1160
+ .description('Show stale memories (old, low-confidence, or rarely accessed)')
1161
+ .option('-d, --days <number>', 'Show memories older than N days', '30')
1162
+ .option('-c, --confidence <level>', 'Max confidence level to show (outdated, speculative)', 'speculative')
1163
+ .option('-l, --limit <number>', 'Max results', '20')
1164
+ .option('-p, --project <project>', 'Project path', getDefaultProjectPath())
1165
+ .action(async (options) => {
1166
+ try {
1167
+ const days = validateLimit(options.days, 30, 1, 365);
1168
+ const cutoffDate = new Date(Date.now() - days * 86400000);
1169
+ // Get recent memories - larger limit to find stale ones
1170
+ const results = await getRecent(options.project, 500);
1171
+ const stale = results.filter((m) => {
1172
+ const created = m.createdAt ? new Date(m.createdAt) : null;
1173
+ const isOld = created && created < cutoffDate;
1174
+ const isLowConfidence = m.confidenceLevel === 'outdated' || m.confidenceLevel === 'speculative';
1175
+ const hasLowImportance = (m.importance || 50) < 40;
1176
+ return isOld || isLowConfidence || hasLowImportance;
1177
+ });
1178
+ const limited = stale.slice(0, validateLimit(options.limit, 20, 1, 100));
1179
+ const summary = {
1180
+ totalStale: stale.length,
1181
+ old: stale.filter((m) => m.createdAt && new Date(m.createdAt) < cutoffDate).length,
1182
+ lowConfidence: stale.filter((m) => m.confidenceLevel === 'outdated' || m.confidenceLevel === 'speculative').length,
1183
+ lowImportance: stale.filter((m) => (m.importance || 50) < 40).length,
1184
+ };
1185
+ console.log(JSON.stringify({ ok: true, summary, memories: limited }, null, 2));
1186
+ }
1187
+ catch (error) {
1188
+ console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
1189
+ process.exit(1);
1190
+ }
1191
+ });
543
1192
  // squish stats
544
1193
  program
545
1194
  .command('stats')
546
1195
  .description('View statistics')
547
- .option('-p, --project <project>', 'Project path', process.cwd())
1196
+ .option('-p, --project <project>', 'Project path', getDefaultProjectPath())
1197
+ .option('-m, --memory', 'Show memory file storage stats instead of database', false)
548
1198
  .action(async (options) => {
549
1199
  try {
1200
+ // Memory file stats
1201
+ if (options.memory) {
1202
+ const { getMemoryStats, isMemoryStorageAvailable } = await import('./core/memory/markdown/markdown-storage.js');
1203
+ const available = isMemoryStorageAvailable();
1204
+ if (!available) {
1205
+ console.log(JSON.stringify({ ok: false, error: 'Memory file storage not available' }, null, 2));
1206
+ process.exit(1);
1207
+ }
1208
+ const stats = await getMemoryStats();
1209
+ console.log(JSON.stringify({ ok: true, source: 'memory', ...stats }, null, 2));
1210
+ return;
1211
+ }
1212
+ // Database stats
550
1213
  const stats = await getMemoryStats(options.project);
551
1214
  console.log(JSON.stringify({ ok: true, ...stats }, null, 2));
552
1215
  }
@@ -562,436 +1225,453 @@ async function runCliMode() {
562
1225
  .action(async () => {
563
1226
  await spawnInstallerWizard();
564
1227
  });
565
- await program.parseAsync(process.argv);
566
- }
567
- // ============================================================================
568
- // MCP MODE (for Claude Code) - DEFAULT
569
- // ============================================================================
570
- async function runMcpMode() {
571
- const TOOLS = [
572
- // Core Memory Tool
573
- {
574
- name: 'core_memory',
575
- description: 'View or edit your core memory (always-visible). Use this to see your persona, user info, project context, and working notes.',
576
- inputSchema: {
577
- type: 'object',
578
- properties: {
579
- action: { type: 'string', enum: ['view', 'edit', 'append'] },
580
- projectId: { type: 'string' },
581
- section: { type: 'string', enum: ['persona', 'user_info', 'project_context', 'working_notes'] },
582
- content: { type: 'string' },
583
- text: { type: 'string' },
584
- },
585
- required: ['action', 'projectId']
586
- }
587
- },
588
- // Context Paging
589
- {
590
- name: 'context_paging',
591
- description: 'Manage your working memory set. Load, evict, or view loaded memories.',
592
- inputSchema: {
593
- type: 'object',
594
- properties: {
595
- action: { type: 'string', enum: ['load', 'evict', 'view'] },
596
- sessionId: { type: 'string' },
597
- memoryId: { type: 'string' },
598
- },
599
- required: ['action', 'sessionId']
600
- }
601
- },
602
- {
603
- name: 'context_status',
604
- description: 'View comprehensive context window status and token usage',
605
- inputSchema: {
606
- type: 'object',
607
- properties: {
608
- sessionId: { type: 'string' },
609
- projectId: { type: 'string' },
610
- },
611
- required: ['sessionId', 'projectId']
612
- }
613
- },
614
- // Memory Tools
615
- {
616
- name: 'remember',
617
- description: 'Store information for future use. Perfect for facts, decisions, code snippets, configuration details, or user preferences.',
618
- inputSchema: {
619
- type: 'object',
620
- properties: {
621
- content: { type: 'string' },
622
- type: { type: 'string', enum: ['observation', 'fact', 'decision', 'context', 'preference'] },
623
- tags: { type: 'array', items: { type: 'string' } },
624
- project: { type: 'string' },
625
- metadata: { type: 'object' },
626
- },
627
- required: ['content']
628
- }
629
- },
630
- {
631
- name: 'recall',
632
- description: 'Retrieve a specific stored memory by ID',
633
- inputSchema: {
634
- type: 'object',
635
- properties: { id: { type: 'string' } },
636
- required: ['id']
637
- }
638
- },
639
- {
640
- name: 'search',
641
- description: 'Search your stored memories. Leave query empty to list recent memories.',
642
- inputSchema: {
643
- type: 'object',
644
- properties: {
645
- query: { type: 'string' },
646
- scope: { type: 'string', enum: ['memories', 'conversations', 'recent'], default: 'memories' },
647
- type: { type: 'string', enum: ['observation', 'fact', 'decision', 'context', 'preference'] },
648
- tags: { type: 'array', items: { type: 'string' } },
649
- limit: { type: 'number', default: 10 },
650
- project: { type: 'string' },
651
- }
652
- }
653
- },
654
- {
655
- name: 'observe',
656
- description: 'Record an observation about your work (tool usage, patterns, errors)',
657
- inputSchema: {
658
- type: 'object',
659
- properties: {
660
- type: { type: 'string', enum: ['tool_use', 'file_change', 'error', 'pattern', 'insight'] },
661
- action: { type: 'string' },
662
- target: { type: 'string' },
663
- summary: { type: 'string' },
664
- details: { type: 'object' },
665
- },
666
- required: ['type', 'action', 'summary']
667
- }
668
- },
669
- {
670
- name: 'context',
671
- description: 'Get project context',
672
- inputSchema: {
673
- type: 'object',
674
- properties: {
675
- project: { type: 'string' },
676
- include: { type: 'array', items: { type: 'string' }, default: ['memories', 'observations'] },
677
- limit: { type: 'number', default: 10 }
678
- },
679
- required: ['project']
680
- }
681
- },
682
- {
683
- name: 'init',
684
- description: 'Initialize Squish memory system for the current project',
685
- inputSchema: {
686
- type: 'object',
687
- properties: { projectPath: { type: 'string' } }
688
- }
689
- },
690
- {
691
- name: 'health',
692
- description: 'Check service status',
693
- inputSchema: { type: 'object', properties: {} }
694
- },
695
- {
696
- name: 'merge',
697
- description: 'Manage memory merges: detect, list, preview, approve, reject, reverse',
698
- inputSchema: {
699
- type: 'object',
700
- properties: {
701
- action: { type: 'string', enum: ['detect', 'list', 'preview', 'stats', 'approve', 'reject', 'reverse'] },
702
- projectId: { type: 'string' },
703
- proposalId: { type: 'string' },
704
- threshold: { type: 'number' },
705
- },
706
- required: ['action']
707
- }
708
- },
709
- {
710
- name: 'qmd_search',
711
- description: 'Search memories using QMD hybrid search (BM25 + vector + rerank)',
712
- inputSchema: {
713
- type: 'object',
714
- properties: {
715
- query: { type: 'string' },
716
- type: { type: 'string', enum: ['observation', 'fact', 'decision', 'context', 'preference'] },
717
- limit: { type: 'number', default: 10 },
718
- },
719
- required: ['query']
720
- }
721
- },
722
- // v0.8.0: Importance Scoring Tools
723
- {
724
- name: 'set_importance',
725
- description: 'Manually set importance score for a memory (0-100)',
726
- inputSchema: {
727
- type: 'object',
728
- properties: {
729
- memoryId: { type: 'string' },
730
- importance: { type: 'number', minimum: 0, maximum: 100 },
731
- },
732
- required: ['memoryId', 'importance']
733
- }
734
- },
735
- {
736
- name: 'pin_memory',
737
- description: 'Pin a memory to prevent pruning/consolidation (or unpin it)',
738
- inputSchema: {
739
- type: 'object',
740
- properties: {
741
- memoryId: { type: 'string' },
742
- pinned: { type: 'boolean', default: true },
743
- },
744
- required: ['memoryId']
745
- }
746
- },
747
- // v0.8.0: Consolidation Tool
748
- {
749
- name: 'consolidate',
750
- description: 'Trigger manual memory consolidation - summarizes old, low-importance memories',
751
- inputSchema: {
752
- type: 'object',
753
- properties: {
754
- projectId: { type: 'string' },
755
- threshold: { type: 'number', default: 0.7 },
756
- minAge: { type: 'number', default: 90 },
757
- limit: { type: 'number', default: 100 },
758
- },
759
- required: ['projectId']
760
- }
761
- },
762
- {
763
- name: 'consolidation_stats',
764
- description: 'Get consolidation statistics for a project',
765
- inputSchema: {
766
- type: 'object',
767
- properties: {
768
- projectId: { type: 'string' },
769
- },
770
- required: ['projectId']
771
- }
772
- }
773
- ];
774
- class Squish {
775
- server;
776
- projectPath;
777
- constructor() {
778
- this.projectPath = process.env.CLAUDE_WORKING_DIRECTORY || process.cwd();
779
- this.server = new Server({ name: 'squish', version: VERSION }, {
780
- capabilities: { tools: {} },
1228
+ // squish note "my thought here" - quick brain dump
1229
+ program
1230
+ .command('note <content>')
1231
+ .description('Quick brain dump - store a raw memory to process later')
1232
+ .option('-p, --project <project>', 'Project path', getDefaultProjectPath())
1233
+ .action(async (content, options) => {
1234
+ try {
1235
+ const result = await rememberMemory({
1236
+ content,
1237
+ type: 'observation',
1238
+ tags: ['note', 'quick'],
1239
+ project: options.project,
781
1240
  });
782
- this.setup();
1241
+ console.log(JSON.stringify({ ok: true, message: 'Note saved', id: result.id }, null, 2));
783
1242
  }
784
- async onSessionInitialized() {
785
- if (!shouldAutoLoad()) {
786
- logger.info('[Session] Auto-load disabled');
787
- return;
1243
+ catch (error) {
1244
+ console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
1245
+ process.exit(1);
1246
+ }
1247
+ });
1248
+ // squish context - Show project context (memories + observations + places)
1249
+ program
1250
+ .command('context')
1251
+ .description('Show project context or list available projects')
1252
+ .option('-p, --project <project>', 'Project path', getDefaultProjectPath())
1253
+ .option('-l, --limit <number>', 'Number of items to show', '10')
1254
+ .option('-i, --include <items>', 'What to include: memories, observations, entities, places', 'memories,observations,places')
1255
+ .option('--list-projects', 'List registered projects instead of loading context', false)
1256
+ .option('-j, --json', 'Output as JSON', false)
1257
+ .option('--place <type>', 'Filter by place type: entry_hall, library, workshop, lab, office, garden, archive')
1258
+ .option('--tier <level>', 'Disclosure level: quick (place names), medium (top 3), full (all)', 'medium')
1259
+ .option('--has-memories', 'Only show places with memories', true)
1260
+ .option('--sync', 'Recalculate memory counts for all places', false)
1261
+ .option('--archive', 'Move memories > 30 days to Archive place', false)
1262
+ .option('--task <description>', 'Task description for auto-place detection (e.g., "fix bug", "design API")')
1263
+ .action(async (options) => {
1264
+ try {
1265
+ // Auto-detect place from task if provided
1266
+ let placeFilter = options.place || null;
1267
+ if (options.task && !placeFilter) {
1268
+ // Simple keyword detection
1269
+ const task = options.task.toLowerCase();
1270
+ if (task.includes('fix') || task.includes('bug') || task.includes('error'))
1271
+ placeFilter = 'workshop';
1272
+ else if (task.includes('design') || task.includes('plan') || task.includes('api'))
1273
+ placeFilter = 'library';
1274
+ else if (task.includes('task') || task.includes('todo') || task.includes('manage'))
1275
+ placeFilter = 'office';
1276
+ else if (task.includes('test') || task.includes('experiment'))
1277
+ placeFilter = 'lab';
1278
+ if (placeFilter)
1279
+ console.log(`Auto-detected place: ${placeFilter}`);
788
1280
  }
789
- try {
790
- logger.info('[Session] Performing auto-load...');
791
- const result = await performAutoLoad(this.projectPath, getAutoLoadConfig());
792
- if (result.warnings.length > 0) {
793
- logger.warn('[Session] Auto-load warnings:', result.warnings);
1281
+ if (options.listProjects) {
1282
+ const projects = await getAllProjects();
1283
+ if (options.json) {
1284
+ console.log(JSON.stringify({ ok: true, count: projects.length, projects }, null, 2));
794
1285
  }
795
- logger.info(`[Session] Auto-load complete: ${result.memoriesLoaded} memories, ~${result.tokensUsed} tokens`);
1286
+ else {
1287
+ console.log(`\n Registered Projects (${projects.length})`);
1288
+ console.log(` ================================`);
1289
+ for (const project of projects) {
1290
+ console.log(`\n ${project.name}`);
1291
+ console.log(` Path: ${project.path}`);
1292
+ console.log(` ID: ${project.id}`);
1293
+ }
1294
+ console.log('');
1295
+ }
1296
+ return;
796
1297
  }
797
- catch (error) {
798
- logger.error('[Session] Auto-load failed:', error);
1298
+ // Get project context
1299
+ const projectPath = resolveProjectPath(options.project);
1300
+ await ensureProject(projectPath);
1301
+ const project = await getOrCreateProject(projectPath);
1302
+ if (!project) {
1303
+ console.log(JSON.stringify({ ok: false, error: 'Project not found' }, null, 2));
1304
+ process.exit(1);
799
1305
  }
800
- }
801
- setup() {
802
- this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
803
- tools: TOOLS
804
- }));
805
- this.server.setRequestHandler(CallToolRequestSchema, async (req) => {
806
- const { name } = req.params;
807
- const args = (req.params.arguments ?? {});
808
- try {
809
- switch (name) {
810
- case 'core_memory':
811
- return await this.handleCoreMemory(args);
812
- case 'context_paging':
813
- return await this.handleContextPaging(args);
814
- case 'context_status': {
815
- const result = await getContextStatus(String(args.sessionId), String(args.projectId));
816
- return this.jsonResponse({ ok: true, ...result });
817
- }
818
- case 'remember': {
819
- return this.jsonResponse({ ok: true, data: await rememberMemory(args) });
820
- }
821
- case 'recall': {
822
- const memory = await getMemoryById(String(args.id));
823
- return this.jsonResponse({ ok: true, found: !!memory, data: memory });
824
- }
825
- case 'search': {
826
- return this.jsonResponse({ ok: true, data: await searchMemories(args) });
827
- }
828
- case 'observe':
829
- return this.jsonResponse({ ok: true, data: await createObservation(args) });
830
- case 'context':
831
- return this.jsonResponse({ ok: true, data: await getProjectContext(args) });
832
- case 'init': {
833
- await ensureDataDirectory();
834
- const project = await ensureProject(args.projectPath || process.cwd());
835
- return this.jsonResponse({ success: true, project });
836
- }
837
- case 'health':
838
- return this.health();
839
- case 'merge':
840
- return await this.handleMerge(args);
841
- case 'qmd_search': {
842
- const available = await isQMDAvailable();
843
- if (!available) {
844
- return this.jsonResponse({ ok: true, qmdAvailable: false, data: await searchMemories(args) });
845
- }
846
- return this.jsonResponse({ ok: true, qmdAvailable: true, data: await searchWithQMD(args) });
847
- }
848
- // v0.8.0: Importance scoring tools
849
- case 'set_importance': {
850
- await setImportanceScore(String(args.memoryId), Number(args.importance));
851
- return this.jsonResponse({
852
- ok: true,
853
- message: `Importance score set to ${args.importance} for memory ${args.memoryId}`
1306
+ const limit = parseInt(options.limit);
1307
+ const include = (options.include || 'memories,observations,places').split(',');
1308
+ const tier = options.tier || 'full';
1309
+ const hasMemoriesOnly = options.hasMemories !== false; // Default true now
1310
+ const existingPlaceFilter = options.place || null;
1311
+ const result = { project: project.name, tier };
1312
+ // Get memories
1313
+ if (include.includes('memories')) {
1314
+ const memories = await getRecent(projectPath, limit);
1315
+ result.memories = memories.map((m) => ({
1316
+ id: m.id,
1317
+ type: m.type,
1318
+ content: m.content?.substring(0, 100),
1319
+ tags: m.tags,
1320
+ }));
1321
+ }
1322
+ // Get observations (learnings)
1323
+ if (include.includes('observations')) {
1324
+ const { getObservations } = await import('./core/ingestion/learnings.js');
1325
+ const observations = await getObservations(projectPath, limit);
1326
+ result.observations = observations.map((o) => ({
1327
+ id: o.id,
1328
+ type: o.type,
1329
+ content: o.content?.substring(0, 100),
1330
+ }));
1331
+ }
1332
+ // Get places (spatial memory) with filtering
1333
+ if (include.includes('places')) {
1334
+ const { initializeDefaultPlaces, getProjectPlaces, walkPlace, getPlaceByType, syncAllPlaceMemoryCounts } = await import('./core/places/index.js');
1335
+ await initializeDefaultPlaces(project.id);
1336
+ // Sync memory counts if requested
1337
+ if (options.sync) {
1338
+ await syncAllPlaceMemoryCounts(project.id);
1339
+ console.log('Synced memory counts for all places.');
1340
+ }
1341
+ // Auto-archive old memories if requested
1342
+ if (options.archive) {
1343
+ const { autoArchiveOldMemories } = await import('./core/places/index.js');
1344
+ const archiveResult = await autoArchiveOldMemories(project.id, 30);
1345
+ console.log(`Archived ${archiveResult.archived} old memories to Archive (${archiveResult.failed} failed).`);
1346
+ }
1347
+ let places = await getProjectPlaces(project.id);
1348
+ // Apply --has-memories filter
1349
+ if (hasMemoriesOnly) {
1350
+ places = places.filter((p) => p.memoryCount > 0);
1351
+ }
1352
+ // Apply --place filter (from --place or --task auto-detect)
1353
+ if (placeFilter) {
1354
+ const filtered = places.filter((p) => p.placeType === placeFilter);
1355
+ if (filtered.length > 0) {
1356
+ places = filtered;
1357
+ }
1358
+ }
1359
+ // Format places based on tier
1360
+ if (tier === 'quick') {
1361
+ // Just place names (~50 tokens)
1362
+ result.places = places.map((p) => ({
1363
+ name: p.name,
1364
+ type: p.placeType,
1365
+ }));
1366
+ }
1367
+ else if (tier === 'medium') {
1368
+ // Top 3 memories per place (~170 tokens)
1369
+ const placesWithMemories = [];
1370
+ for (const p of places) {
1371
+ if (p.memoryCount > 0) {
1372
+ const walkResult = await walkPlace(project.id, p.placeType, {
1373
+ tokenBudget: 170,
1374
+ maxMemoriesPerPlace: 3,
1375
+ compressWithToon: false,
854
1376
  });
855
- }
856
- case 'pin_memory': {
857
- const pinned = args.pinned !== undefined ? Boolean(args.pinned) : true;
858
- if (pinned) {
859
- await pinMemory(String(args.memoryId));
860
- }
861
- else {
862
- await unpinMemory(String(args.memoryId));
863
- }
864
- return this.jsonResponse({
865
- ok: true,
866
- message: `Memory ${args.memoryId} ${pinned ? 'pinned' : 'unpinned'}`
1377
+ placesWithMemories.push({
1378
+ name: p.name,
1379
+ type: p.placeType,
1380
+ purpose: p.purpose,
1381
+ memories: p.memoryCount,
1382
+ preview: walkResult?.memories.slice(0, 3).map((m) => m.content?.substring(0, 80)) || [],
867
1383
  });
868
1384
  }
869
- // v0.8.0: Consolidation tools
870
- case 'consolidate': {
871
- const results = await consolidateMemoriesImpl({
872
- projectId: String(args.projectId),
873
- minAge: args.minAge ? Number(args.minAge) : 90,
874
- maxImportance: 30,
875
- similarityThreshold: args.threshold ? Number(args.threshold) : 0.7,
876
- limit: args.limit ? Number(args.limit) : 100,
877
- });
878
- return this.jsonResponse({
879
- ok: true,
880
- consolidated: results.length,
881
- results
882
- });
1385
+ }
1386
+ result.places = placesWithMemories;
1387
+ }
1388
+ else {
1389
+ // Full - all memories (~500 tokens)
1390
+ result.places = places.map((p) => ({
1391
+ name: p.name,
1392
+ type: p.placeType,
1393
+ purpose: p.purpose,
1394
+ memories: p.memoryCount,
1395
+ }));
1396
+ }
1397
+ }
1398
+ if (options.json) {
1399
+ console.log(JSON.stringify({ ok: true, ...result }, null, 2));
1400
+ }
1401
+ else {
1402
+ // Human readable output
1403
+ console.log(`\n=== ${project.name} Context ===\n`);
1404
+ if (result.places && result.places.length > 0) {
1405
+ console.log('Spatial Memory Places:');
1406
+ result.places.forEach((p) => {
1407
+ if (tier === 'quick') {
1408
+ console.log(` ${p.name} (${p.type})`);
1409
+ }
1410
+ else if (tier === 'medium') {
1411
+ console.log(` ${p.name} (${p.memories} memories) - ${p.purpose}`);
1412
+ if (p.preview && p.preview.length > 0) {
1413
+ p.preview.forEach((m) => {
1414
+ console.log(` - ${m}...`);
1415
+ });
1416
+ }
883
1417
  }
884
- case 'consolidation_stats': {
885
- const stats = await getConsolidationStats(String(args.projectId));
886
- return this.jsonResponse({ ok: true, ...stats });
1418
+ else {
1419
+ console.log(` ${p.name} (${p.memories} memories) - ${p.purpose}`);
887
1420
  }
888
- default:
889
- throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
890
- }
1421
+ });
1422
+ console.log('');
891
1423
  }
892
- catch (error) {
893
- if (error instanceof McpError)
894
- throw error;
895
- throw new McpError(ErrorCode.InternalError, `Tool '${name}' failed`);
1424
+ if (result.memories && result.memories.length > 0) {
1425
+ console.log('Recent Memories:');
1426
+ result.memories.slice(0, 5).forEach((m) => {
1427
+ console.log(` [${m.type}] ${m.content}`);
1428
+ });
1429
+ console.log('');
896
1430
  }
1431
+ }
1432
+ }
1433
+ catch (error) {
1434
+ console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
1435
+ process.exit(1);
1436
+ }
1437
+ });
1438
+ // squish migrate - Migrate memories between databases
1439
+ program
1440
+ .command('migrate')
1441
+ .description('Migrate memories from one .squish directory to another')
1442
+ .option('-f, --from <path>', 'Source .squish directory (read from)', '')
1443
+ .option('-t, --to <path>', 'Target .squish directory (write to)', '')
1444
+ .option('--delete-source', 'Delete source after migration (use with caution)', false)
1445
+ .option('--dry-run', 'Preview migration without applying', false)
1446
+ .action(async (options) => {
1447
+ try {
1448
+ if (!options.from || !options.to) {
1449
+ console.log(JSON.stringify({
1450
+ ok: false,
1451
+ error: 'Usage: squish migrate --from /path/to/old/.squish --to /path/to/new/.squish'
1452
+ }, null, 2));
1453
+ process.exit(1);
1454
+ }
1455
+ const sourcePath = path.join(options.from, 'squish.db');
1456
+ const targetPath = path.join(options.to, 'squish.db');
1457
+ if (!existsSync(sourcePath)) {
1458
+ console.log(JSON.stringify({ ok: false, error: `Source database not found: ${sourcePath}` }, null, 2));
1459
+ process.exit(1);
1460
+ }
1461
+ if (!existsSync(targetPath)) {
1462
+ console.log(JSON.stringify({ ok: false, error: `Target database not found: ${targetPath}` }, null, 2));
1463
+ process.exit(1);
1464
+ }
1465
+ console.log(`Migrating memories from:\n ${options.from}\nto:\n ${options.to}\n`);
1466
+ // Import database modules dynamically
1467
+ const { migrateMemories } = await import('./core/memory/migrate.js');
1468
+ const result = await migrateMemories(options.from, options.to, {
1469
+ dryRun: options.dryRun,
1470
+ deleteSource: options.deleteSource
897
1471
  });
898
- this.server.onerror = (e) => logger.error('MCP Server error', e);
899
- process.on('SIGINT', () => this.shutdown());
900
- process.on('SIGTERM', () => this.shutdown());
1472
+ console.log(JSON.stringify({ ok: true, ...result }, null, 2));
901
1473
  }
902
- jsonResponse(payload) {
903
- return {
904
- content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }]
905
- };
1474
+ catch (error) {
1475
+ console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
1476
+ process.exit(1);
906
1477
  }
907
- async handleCoreMemory(args) {
908
- const action = args.action;
909
- const projectId = String(args.projectId);
910
- await initializeCoreMemory(projectId);
911
- const actions = {
912
- view: async () => {
913
- const content = await getCoreMemory(projectId);
914
- const stats = await getCoreMemoryStats(projectId);
915
- return this.jsonResponse({ ok: true, action: 'view', content, stats });
916
- },
917
- edit: async () => {
918
- const result = await editCoreMemorySection(projectId, args.section, String(args.content));
919
- return this.jsonResponse({ ok: true, action: 'edit', ...result });
920
- },
921
- append: async () => {
922
- const result = await appendCoreMemorySection(projectId, args.section, String(args.text));
923
- return this.jsonResponse({ ok: true, action: 'append', ...result });
1478
+ });
1479
+ // squish clean - Run deduplication and consolidation
1480
+ program
1481
+ .command('clean')
1482
+ .description('Run maintenance: deduplication + consolidation')
1483
+ .option('-t, --threshold <number>', 'Similarity threshold for dedup (0-1)', '0.85')
1484
+ .option('-d, --min-age <days>', 'Minimum age for consolidation', '90')
1485
+ .option('-i, --max-importance <number>', 'Max importance to consolidate (0-100)', '30')
1486
+ .option('-c, --min-cluster <number>', 'Minimum cluster size', '3')
1487
+ .option('-p, --project <project>', 'Project path', getDefaultProjectPath())
1488
+ .option('--dry-run', 'Preview changes without applying', false)
1489
+ .action(async (options) => {
1490
+ try {
1491
+ console.log('Running maintenance: deduplication + consolidation...\n');
1492
+ // Step 1: Deduplication
1493
+ console.log('Step 1: Finding duplicate memories...');
1494
+ const dedupResult = await runDeduplicationJob(options.project);
1495
+ console.log(` Found ${dedupResult.duplicatesFound} duplicates, merged ${dedupResult.mergedCount}`);
1496
+ // Step 2: Consolidation
1497
+ console.log('\nStep 2: Consolidating old memories...');
1498
+ const consolidateResult = await runFullConsolidationJob(options.project);
1499
+ console.log(` Clustered ${consolidateResult.clustered}, merged ${consolidateResult.merged}, consolidated ${consolidateResult.consolidated}`);
1500
+ console.log(JSON.stringify({
1501
+ ok: true,
1502
+ dedup: {
1503
+ duplicatesFound: dedupResult.duplicatesFound,
1504
+ mergedCount: dedupResult.mergedCount,
1505
+ tokensRecovered: dedupResult.tokensRecovered
924
1506
  },
925
- };
926
- const handler = actions[action];
927
- if (!handler)
928
- throw new McpError(ErrorCode.InvalidParams, `Unknown action: ${action}`);
929
- return handler();
930
- }
931
- async handleContextPaging(args) {
932
- const action = args.action;
933
- const sessionId = String(args.sessionId);
934
- const actions = {
935
- load: () => loadMemoryToContext(sessionId, String(args.memoryId)),
936
- evict: () => evictMemoryFromContext(sessionId, String(args.memoryId)),
937
- view: () => viewLoadedMemories(sessionId),
938
- };
939
- const handler = actions[action];
940
- if (!handler)
941
- throw new McpError(ErrorCode.InvalidParams, `Unknown action: ${action}`);
942
- return this.jsonResponse(await handler());
943
- }
944
- async handleMerge(args) {
945
- const action = args.action;
946
- const handlers = {
947
- detect: () => handleDetectDuplicates(args),
948
- list: () => handleListProposals(args),
949
- preview: () => handlePreviewMerge(args),
950
- stats: () => handleGetMergeStats(args),
951
- approve: () => handleApproveMerge(args),
952
- reject: () => handleRejectMerge(args),
953
- reverse: () => handleReverseMerge(args),
954
- };
955
- const handler = handlers[action];
956
- if (!handler)
957
- throw new McpError(ErrorCode.InvalidParams, `Unknown action: ${action}`);
958
- return this.jsonResponse(await handler());
1507
+ consolidate: {
1508
+ clustered: consolidateResult.clustered,
1509
+ merged: consolidateResult.merged,
1510
+ consolidated: consolidateResult.consolidated
1511
+ }
1512
+ }, null, 2));
959
1513
  }
960
- async shutdown() {
961
- await closeCache();
962
- process.exit(0);
1514
+ catch (error) {
1515
+ console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
963
1516
  }
964
- async health() {
965
- const dbOk = await checkDatabaseHealth();
966
- const redisOk = await checkRedisHealth();
967
- return this.jsonResponse({
968
- version: VERSION,
969
- mode: config.isTeamMode ? 'team' : 'local',
970
- status: dbOk ? 'ok' : 'error',
971
- });
1517
+ });
1518
+ // squish hooks session-start --agent claude-code --mode startup
1519
+ program
1520
+ .command('hooks')
1521
+ .description('Handle agent hooks (session-start, post-tool-use, session-end, pre-compact)')
1522
+ .argument('<event>', 'Event type: session-start, post-tool-use, session-end, pre-compact')
1523
+ .option('-a, --agent <agent>', 'Agent type: claude-code, opencode, cursor, windsurf', 'claude-code')
1524
+ .option('-m, --mode <mode>', 'Mode for session-start: startup, resume, compact', 'startup')
1525
+ .option('-p, --project <project>', 'Project path', getDefaultProjectPath())
1526
+ .option('-t, --tool <tool>', 'Tool name for post-tool-use')
1527
+ .option('--tool-input <json>', 'Tool input as JSON string')
1528
+ .option('--tool-result <json>', 'Tool result as JSON string')
1529
+ .option('--wip <work>', 'Work in progress for session-end')
1530
+ .action(async (event, options) => {
1531
+ try {
1532
+ const agentType = options.agent;
1533
+ const projectPath = options.project;
1534
+ let result;
1535
+ switch (event) {
1536
+ case 'session-start':
1537
+ result = await handleSessionStart({
1538
+ projectPath,
1539
+ mode: options.mode,
1540
+ agentType,
1541
+ });
1542
+ console.log(JSON.stringify({ ok: true, ...result }, null, 2));
1543
+ break;
1544
+ case 'post-tool-use':
1545
+ if (!options.tool) {
1546
+ console.log(JSON.stringify({ ok: false, error: '--tool required for post-tool-use' }, null, 2));
1547
+ process.exit(1);
1548
+ }
1549
+ const toolInput = options.toolInput ? JSON.parse(options.toolInput) : {};
1550
+ const toolResult = options.toolResult ? JSON.parse(options.toolResult) : {};
1551
+ result = await handlePostToolUse({
1552
+ toolName: options.tool,
1553
+ toolInput,
1554
+ toolResult,
1555
+ projectPath,
1556
+ agentType,
1557
+ });
1558
+ console.log(JSON.stringify({ ok: true, ...result }, null, 2));
1559
+ break;
1560
+ case 'session-end':
1561
+ result = await handleSessionEnd({
1562
+ projectPath,
1563
+ agentType,
1564
+ workInProgress: options.wip,
1565
+ });
1566
+ console.log(JSON.stringify({ ok: true, ...result }, null, 2));
1567
+ break;
1568
+ case 'pre-compact':
1569
+ result = await handlePreCompact({
1570
+ projectPath,
1571
+ agentType,
1572
+ });
1573
+ console.log(JSON.stringify({ ok: true, ...result }, null, 2));
1574
+ break;
1575
+ default:
1576
+ console.log(JSON.stringify({ ok: false, error: `Unknown event: ${event}` }, null, 2));
1577
+ process.exit(1);
1578
+ }
972
1579
  }
973
- async run() {
974
- // Verify plugin manifest (universal plugin self-check)
975
- const manifest = loadPluginManifest();
976
- const verification = verifyManifest(manifest);
977
- if (!verification.ok) {
978
- logger.warn('Plugin manifest verification failed:', verification.errors);
1580
+ catch (error) {
1581
+ console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
1582
+ process.exit(1);
1583
+ }
1584
+ });
1585
+ // squish walk - Walk through spatial memory places
1586
+ program
1587
+ .command('walk [place]')
1588
+ .description('Walk through spatial memory places (entry_hall, library, workshop, lab, office, garden, archive, or --all)')
1589
+ .option('-a, --all', 'Walk all places in order', false)
1590
+ .option('-t, --tokens <number>', 'Max tokens budget (default: 170)', '170')
1591
+ .option('-m, --max <number>', 'Max memories per place', '10')
1592
+ .option('-p, --project <project>', 'Project path', getDefaultProjectPath())
1593
+ .option('-q, --quick', 'Quick tour (place names only)', false)
1594
+ .option('-j, --json', 'Output as JSON', false)
1595
+ .action(async (place, options) => {
1596
+ try {
1597
+ const projectPath = resolveProjectPath(options.project);
1598
+ await ensureProject(projectPath);
1599
+ const project = await getOrCreateProject(projectPath);
1600
+ if (!project) {
1601
+ console.log(JSON.stringify({ ok: false, error: 'Project not found' }, null, 2));
1602
+ process.exit(1);
1603
+ }
1604
+ // Ensure places are initialized
1605
+ await initializeDefaultPlaces(project.id);
1606
+ if (options.quick) {
1607
+ const tour = await quickTour(project.id);
1608
+ if (options.json) {
1609
+ console.log(JSON.stringify({ ok: true, ...tour }, null, 2));
1610
+ }
1611
+ else {
1612
+ console.log(`\n=== Spatial Memory Tour ===\n`);
1613
+ console.log(`Total Memories: ${tour.totalMemories}\n`);
1614
+ for (const p of tour.places) {
1615
+ console.log(`${p.name} (${p.memoryCount} memories)`);
1616
+ console.log(` ${p.purpose}\n`);
1617
+ }
1618
+ }
1619
+ return;
1620
+ }
1621
+ const tokenBudget = parseInt(options.tokens);
1622
+ const maxMemories = parseInt(options.max);
1623
+ if (options.all) {
1624
+ const results = await walkAllPlaces(project.id, {
1625
+ tokenBudget: Math.floor(tokenBudget / 7),
1626
+ maxMemoriesPerPlace: maxMemories,
1627
+ compressWithToon: true,
1628
+ });
1629
+ if (options.json) {
1630
+ console.log(JSON.stringify({ ok: true, places: results }, null, 2));
1631
+ }
1632
+ else {
1633
+ console.log(`\n=== Walking All Places ===\n`);
1634
+ for (const r of results) {
1635
+ console.log(`## ${r.place.name} (${r.memories.length} memories, ~${r.totalTokens} tokens)`);
1636
+ r.memories.forEach((m, i) => {
1637
+ console.log(` ${i + 1}. ${m.content.substring(0, 60)}...`);
1638
+ });
1639
+ console.log('');
1640
+ }
1641
+ }
1642
+ }
1643
+ else if (place) {
1644
+ const validPlaces = ['entry_hall', 'library', 'workshop', 'lab', 'office', 'garden', 'archive'];
1645
+ const placeType = validPlaces.includes(place) ? place : 'workshop';
1646
+ const result = await walkPlace(project.id, placeType, {
1647
+ tokenBudget,
1648
+ maxMemoriesPerPlace: maxMemories,
1649
+ compressWithToon: true,
1650
+ });
1651
+ if (options.json) {
1652
+ console.log(JSON.stringify({ ok: true, ...result }, null, 2));
1653
+ }
1654
+ else if (result) {
1655
+ console.log(`\n=== ${result.place.name} ===\n`);
1656
+ console.log(`Purpose: ${result.place.purpose || 'N/A'}\n`);
1657
+ result.memories.forEach((m, i) => {
1658
+ console.log(`${i + 1}. [${m.type}] ${m.content.substring(0, 80)}`);
1659
+ });
1660
+ console.log(`\n~${result.totalTokens} tokens`);
1661
+ }
1662
+ else {
1663
+ console.log(JSON.stringify({ ok: false, error: `Place not found: ${place}` }, null, 2));
1664
+ }
979
1665
  }
980
- else {
981
- logger.info(`Squish v${VERSION} - Plugin manifest verified`);
982
- }
983
- const transport = new StdioServerTransport();
984
- await this.server.connect(transport);
985
- logger.info(`v${VERSION}`);
986
- registerJobHandler('nightly_maintenance', runNightlyJob);
987
- registerJobHandler('weekly_maintenance', runWeeklyJob);
988
- await initializeScheduler();
989
- startHeartbeatChecking();
990
- await this.onSessionInitialized();
991
- await heartbeat();
992
- startWebServer();
993
1666
  }
994
- }
995
- new Squish().run();
1667
+ catch (error) {
1668
+ console.log(JSON.stringify({ ok: false, error: error.message }, null, 2));
1669
+ process.exit(1);
1670
+ }
1671
+ });
1672
+ await program.parseAsync(process.argv);
996
1673
  }
1674
+ // MCP server: core/commands/mcp-server.ts
1675
+ // Run with: npx squish-mcp
1676
+ // ============================================================================
997
1677
  //# sourceMappingURL=index.js.map