mem-os-python 1.6.27__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. mem_os_python-1.6.27.dist-info/METADATA +1032 -0
  2. mem_os_python-1.6.27.dist-info/RECORD +253 -0
  3. mem_os_python-1.6.27.dist-info/WHEEL +4 -0
  4. mem_os_python-1.6.27.dist-info/entry_points.txt +2 -0
  5. mem_os_python-1.6.27.dist-info/licenses/LICENSE +21 -0
  6. memos/__init__.py +16 -0
  7. memos/_js/adapters/anthropic.d.ts +279 -0
  8. memos/_js/adapters/anthropic.d.ts.map +1 -0
  9. memos/_js/adapters/anthropic.js +348 -0
  10. memos/_js/adapters/anthropic.js.map +1 -0
  11. memos/_js/adapters/base.d.ts +10 -0
  12. memos/_js/adapters/base.d.ts.map +1 -0
  13. memos/_js/adapters/base.js +2 -0
  14. memos/_js/adapters/base.js.map +1 -0
  15. memos/_js/adapters/openai.d.ts +264 -0
  16. memos/_js/adapters/openai.d.ts.map +1 -0
  17. memos/_js/adapters/openai.js +342 -0
  18. memos/_js/adapters/openai.js.map +1 -0
  19. memos/_js/apps/explorer.d.ts +244 -0
  20. memos/_js/apps/explorer.d.ts.map +1 -0
  21. memos/_js/apps/explorer.js +738 -0
  22. memos/_js/apps/explorer.js.map +1 -0
  23. memos/_js/citations.d.ts +65 -0
  24. memos/_js/citations.d.ts.map +1 -0
  25. memos/_js/citations.js +143 -0
  26. memos/_js/citations.js.map +1 -0
  27. memos/_js/cli-browse.d.ts +3 -0
  28. memos/_js/cli-browse.d.ts.map +1 -0
  29. memos/_js/cli-browse.js +129 -0
  30. memos/_js/cli-browse.js.map +1 -0
  31. memos/_js/cli-completion.d.ts +31 -0
  32. memos/_js/cli-completion.d.ts.map +1 -0
  33. memos/_js/cli-completion.js +340 -0
  34. memos/_js/cli-completion.js.map +1 -0
  35. memos/_js/cli-help.d.ts +18 -0
  36. memos/_js/cli-help.d.ts.map +1 -0
  37. memos/_js/cli-help.js +357 -0
  38. memos/_js/cli-help.js.map +1 -0
  39. memos/_js/cli.d.ts +28 -0
  40. memos/_js/cli.d.ts.map +1 -0
  41. memos/_js/cli.js +2880 -0
  42. memos/_js/cli.js.map +1 -0
  43. memos/_js/compression.d.ts +25 -0
  44. memos/_js/compression.d.ts.map +1 -0
  45. memos/_js/compression.js +319 -0
  46. memos/_js/compression.js.map +1 -0
  47. memos/_js/confidence-machine.d.ts +99 -0
  48. memos/_js/confidence-machine.d.ts.map +1 -0
  49. memos/_js/confidence-machine.js +222 -0
  50. memos/_js/confidence-machine.js.map +1 -0
  51. memos/_js/context-pack.d.ts +506 -0
  52. memos/_js/context-pack.d.ts.map +1 -0
  53. memos/_js/context-pack.js +1435 -0
  54. memos/_js/context-pack.js.map +1 -0
  55. memos/_js/contradictions.d.ts +160 -0
  56. memos/_js/contradictions.d.ts.map +1 -0
  57. memos/_js/contradictions.js +255 -0
  58. memos/_js/contradictions.js.map +1 -0
  59. memos/_js/dashboard.d.ts +30 -0
  60. memos/_js/dashboard.d.ts.map +1 -0
  61. memos/_js/dashboard.js +302 -0
  62. memos/_js/dashboard.js.map +1 -0
  63. memos/_js/ebbinghaus.d.ts +29 -0
  64. memos/_js/ebbinghaus.d.ts.map +1 -0
  65. memos/_js/ebbinghaus.js +32 -0
  66. memos/_js/ebbinghaus.js.map +1 -0
  67. memos/_js/embedding-queue.d.ts +190 -0
  68. memos/_js/embedding-queue.d.ts.map +1 -0
  69. memos/_js/embedding-queue.js +316 -0
  70. memos/_js/embedding-queue.js.map +1 -0
  71. memos/_js/embeddings.d.ts +187 -0
  72. memos/_js/embeddings.d.ts.map +1 -0
  73. memos/_js/embeddings.js +633 -0
  74. memos/_js/embeddings.js.map +1 -0
  75. memos/_js/entity-extraction.d.ts +54 -0
  76. memos/_js/entity-extraction.d.ts.map +1 -0
  77. memos/_js/entity-extraction.js +325 -0
  78. memos/_js/entity-extraction.js.map +1 -0
  79. memos/_js/entity-index.d.ts +93 -0
  80. memos/_js/entity-index.d.ts.map +1 -0
  81. memos/_js/entity-index.js +165 -0
  82. memos/_js/entity-index.js.map +1 -0
  83. memos/_js/event-memory.d.ts +147 -0
  84. memos/_js/event-memory.d.ts.map +1 -0
  85. memos/_js/event-memory.js +290 -0
  86. memos/_js/event-memory.js.map +1 -0
  87. memos/_js/external-import.d.ts +98 -0
  88. memos/_js/external-import.d.ts.map +1 -0
  89. memos/_js/external-import.js +669 -0
  90. memos/_js/external-import.js.map +1 -0
  91. memos/_js/fidelity.d.ts +367 -0
  92. memos/_js/fidelity.d.ts.map +1 -0
  93. memos/_js/fidelity.js +911 -0
  94. memos/_js/fidelity.js.map +1 -0
  95. memos/_js/graph-expansion.d.ts +119 -0
  96. memos/_js/graph-expansion.d.ts.map +1 -0
  97. memos/_js/graph-expansion.js +213 -0
  98. memos/_js/graph-expansion.js.map +1 -0
  99. memos/_js/graph-mermaid.d.ts +13 -0
  100. memos/_js/graph-mermaid.d.ts.map +1 -0
  101. memos/_js/graph-mermaid.js +32 -0
  102. memos/_js/graph-mermaid.js.map +1 -0
  103. memos/_js/graph.d.ts +170 -0
  104. memos/_js/graph.d.ts.map +1 -0
  105. memos/_js/graph.js +482 -0
  106. memos/_js/graph.js.map +1 -0
  107. memos/_js/harness.d.ts +63 -0
  108. memos/_js/harness.d.ts.map +1 -0
  109. memos/_js/harness.js +93 -0
  110. memos/_js/harness.js.map +1 -0
  111. memos/_js/importance.d.ts +118 -0
  112. memos/_js/importance.d.ts.map +1 -0
  113. memos/_js/importance.js +144 -0
  114. memos/_js/importance.js.map +1 -0
  115. memos/_js/index.d.ts +51 -0
  116. memos/_js/index.d.ts.map +1 -0
  117. memos/_js/index.js +53 -0
  118. memos/_js/index.js.map +1 -0
  119. memos/_js/mcp.d.ts +58 -0
  120. memos/_js/mcp.d.ts.map +1 -0
  121. memos/_js/mcp.js +1853 -0
  122. memos/_js/mcp.js.map +1 -0
  123. memos/_js/memory.d.ts +1762 -0
  124. memos/_js/memory.d.ts.map +1 -0
  125. memos/_js/memory.js +6283 -0
  126. memos/_js/memory.js.map +1 -0
  127. memos/_js/procedural.d.ts +74 -0
  128. memos/_js/procedural.d.ts.map +1 -0
  129. memos/_js/procedural.js +125 -0
  130. memos/_js/procedural.js.map +1 -0
  131. memos/_js/project-config.d.ts +68 -0
  132. memos/_js/project-config.d.ts.map +1 -0
  133. memos/_js/project-config.js +59 -0
  134. memos/_js/project-config.js.map +1 -0
  135. memos/_js/proof-of-forgetting.d.ts +57 -0
  136. memos/_js/proof-of-forgetting.d.ts.map +1 -0
  137. memos/_js/proof-of-forgetting.js +80 -0
  138. memos/_js/proof-of-forgetting.js.map +1 -0
  139. memos/_js/provenance.d.ts +122 -0
  140. memos/_js/provenance.d.ts.map +1 -0
  141. memos/_js/provenance.js +182 -0
  142. memos/_js/provenance.js.map +1 -0
  143. memos/_js/quarantine-commands.d.ts +23 -0
  144. memos/_js/quarantine-commands.d.ts.map +1 -0
  145. memos/_js/quarantine-commands.js +42 -0
  146. memos/_js/quarantine-commands.js.map +1 -0
  147. memos/_js/quarantine.d.ts +120 -0
  148. memos/_js/quarantine.d.ts.map +1 -0
  149. memos/_js/quarantine.js +873 -0
  150. memos/_js/quarantine.js.map +1 -0
  151. memos/_js/redact.d.ts +12 -0
  152. memos/_js/redact.d.ts.map +1 -0
  153. memos/_js/redact.js +26 -0
  154. memos/_js/redact.js.map +1 -0
  155. memos/_js/repl.d.ts +57 -0
  156. memos/_js/repl.d.ts.map +1 -0
  157. memos/_js/repl.js +295 -0
  158. memos/_js/repl.js.map +1 -0
  159. memos/_js/rerank-local.d.ts +61 -0
  160. memos/_js/rerank-local.d.ts.map +1 -0
  161. memos/_js/rerank-local.js +210 -0
  162. memos/_js/rerank-local.js.map +1 -0
  163. memos/_js/retain-filter.d.ts +98 -0
  164. memos/_js/retain-filter.d.ts.map +1 -0
  165. memos/_js/retain-filter.js +232 -0
  166. memos/_js/retain-filter.js.map +1 -0
  167. memos/_js/retrieval.d.ts +265 -0
  168. memos/_js/retrieval.d.ts.map +1 -0
  169. memos/_js/retrieval.js +1018 -0
  170. memos/_js/retrieval.js.map +1 -0
  171. memos/_js/revert.d.ts +77 -0
  172. memos/_js/revert.d.ts.map +1 -0
  173. memos/_js/revert.js +252 -0
  174. memos/_js/revert.js.map +1 -0
  175. memos/_js/scope.d.ts +30 -0
  176. memos/_js/scope.d.ts.map +1 -0
  177. memos/_js/scope.js +71 -0
  178. memos/_js/scope.js.map +1 -0
  179. memos/_js/semantic-screen.d.ts +152 -0
  180. memos/_js/semantic-screen.d.ts.map +1 -0
  181. memos/_js/semantic-screen.js +290 -0
  182. memos/_js/semantic-screen.js.map +1 -0
  183. memos/_js/session-hooks.d.ts +127 -0
  184. memos/_js/session-hooks.d.ts.map +1 -0
  185. memos/_js/session-hooks.js +482 -0
  186. memos/_js/session-hooks.js.map +1 -0
  187. memos/_js/storage/sqlite.d.ts +427 -0
  188. memos/_js/storage/sqlite.d.ts.map +1 -0
  189. memos/_js/storage/sqlite.js +2428 -0
  190. memos/_js/storage/sqlite.js.map +1 -0
  191. memos/_js/sync.d.ts +213 -0
  192. memos/_js/sync.d.ts.map +1 -0
  193. memos/_js/sync.js +389 -0
  194. memos/_js/sync.js.map +1 -0
  195. memos/_js/tamper-log.d.ts +98 -0
  196. memos/_js/tamper-log.d.ts.map +1 -0
  197. memos/_js/tamper-log.js +327 -0
  198. memos/_js/tamper-log.js.map +1 -0
  199. memos/_js/temporal.d.ts +92 -0
  200. memos/_js/temporal.d.ts.map +1 -0
  201. memos/_js/temporal.js +536 -0
  202. memos/_js/temporal.js.map +1 -0
  203. memos/_js/time-travel.d.ts +38 -0
  204. memos/_js/time-travel.d.ts.map +1 -0
  205. memos/_js/time-travel.js +46 -0
  206. memos/_js/time-travel.js.map +1 -0
  207. memos/_js/timestamp-format.d.ts +18 -0
  208. memos/_js/timestamp-format.d.ts.map +1 -0
  209. memos/_js/timestamp-format.js +52 -0
  210. memos/_js/timestamp-format.js.map +1 -0
  211. memos/_js/triple-extraction.d.ts +31 -0
  212. memos/_js/triple-extraction.d.ts.map +1 -0
  213. memos/_js/triple-extraction.js +116 -0
  214. memos/_js/triple-extraction.js.map +1 -0
  215. memos/_js/types.d.ts +2474 -0
  216. memos/_js/types.d.ts.map +1 -0
  217. memos/_js/types.js +20 -0
  218. memos/_js/types.js.map +1 -0
  219. memos/_js/updater.d.ts +95 -0
  220. memos/_js/updater.d.ts.map +1 -0
  221. memos/_js/updater.js +268 -0
  222. memos/_js/updater.js.map +1 -0
  223. memos/_js/version.d.ts +17 -0
  224. memos/_js/version.d.ts.map +1 -0
  225. memos/_js/version.js +34 -0
  226. memos/_js/version.js.map +1 -0
  227. memos/_js/vision/clip-query-cache.d.ts +58 -0
  228. memos/_js/vision/clip-query-cache.d.ts.map +1 -0
  229. memos/_js/vision/clip-query-cache.js +79 -0
  230. memos/_js/vision/clip-query-cache.js.map +1 -0
  231. memos/_js/vision/clip.d.ts +113 -0
  232. memos/_js/vision/clip.d.ts.map +1 -0
  233. memos/_js/vision/clip.js +224 -0
  234. memos/_js/vision/clip.js.map +1 -0
  235. memos/_js/vision/fake-clip.d.ts +55 -0
  236. memos/_js/vision/fake-clip.d.ts.map +1 -0
  237. memos/_js/vision/fake-clip.js +122 -0
  238. memos/_js/vision/fake-clip.js.map +1 -0
  239. memos/_js/vision/index.d.ts +15 -0
  240. memos/_js/vision/index.d.ts.map +1 -0
  241. memos/_js/vision/index.js +13 -0
  242. memos/_js/vision/index.js.map +1 -0
  243. memos/_js/vision/store.d.ts +137 -0
  244. memos/_js/vision/store.d.ts.map +1 -0
  245. memos/_js/vision/store.js +383 -0
  246. memos/_js/vision/store.js.map +1 -0
  247. memos/adapters/__init__.py +0 -0
  248. memos/adapters/crewai.py +385 -0
  249. memos/adapters/langchain.py +301 -0
  250. memos/adapters/ollama.py +396 -0
  251. memos/server/__init__.py +0 -0
  252. memos/server/main.py +547 -0
  253. memos/server/routes.py +360 -0
memos/_js/cli.js ADDED
@@ -0,0 +1,2880 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MemOS CLI — command-line interface for the MemOS memory layer.
4
+ *
5
+ * Usage:
6
+ * memos store "User prefers dark mode" --type preference
7
+ * memos search "dark mode"
8
+ * memos history <id>
9
+ * memos retrieve <id>
10
+ * memos forget <id>
11
+ * memos revert --last | --id <id> | --about <topic> | "<revert request>" [--dry-run]
12
+ * memos summarize
13
+ * memos graph [--mermaid]
14
+ * memos browse
15
+ * memos link <src> <dst>
16
+ * memos count
17
+ * memos tag <id> <tag1> [tag2...]
18
+ * memos untag <id> <tag1> [tag2...]
19
+ * memos list --tag <tag>
20
+ * memos export [--format json|markdown|obsidian] [--output <dir>] [--tag <tag>]
21
+ * memos backup [--output <path>]
22
+ * memos restore <path>
23
+ * memos mcp
24
+ *
25
+ * @module @memos/cli
26
+ */
27
+ import { MemOS } from "./memory.js";
28
+ import { graphToMermaid } from "./graph-mermaid.js";
29
+ import { parseScopeArg } from "./scope.js";
30
+ import { MemorySkippedError } from "./memory.js";
31
+ import { isProvenanceTier } from "./provenance.js";
32
+ import { parseRevertCliArgs } from "./revert.js";
33
+ import { listReminders, splitRemindText } from "./event-memory.js";
34
+ import { parseTemporal } from "./temporal.js";
35
+ import { cipherKeyEnvName, loadProjectConfig } from "./project-config.js";
36
+ import { getSdkVersion } from "./version.js";
37
+ import { redactSecrets } from "./redact.js";
38
+ import { CLI_COMMANDS, suggestCommand, getCommandHelp } from "./cli-help.js";
39
+ import { getCompletionScript, COMPLETION_SHELLS } from "./cli-completion.js";
40
+ import { resolve, dirname, join } from "path";
41
+ import * as os from "os";
42
+ import { existsSync, copyFileSync, mkdirSync, statSync, writeFileSync, readFileSync, } from "fs";
43
+ import * as readline from "node:readline";
44
+ const args = process.argv.slice(2);
45
+ const command = args[0];
46
+ /**
47
+ * Filter out flags and their values from an argument list.
48
+ * Flags that take a value: --db, --type, --ttl, --limit, --format, --output,
49
+ * --id, --about, --reason, --actor, --namespace
50
+ * Boolean flags: --json
51
+ */
52
+ function nonFlagArgs(args, startIndex) {
53
+ const flagsWithValue = new Set([
54
+ "--db",
55
+ "--type",
56
+ "--ttl",
57
+ "--limit",
58
+ "--format",
59
+ "--output",
60
+ "--id",
61
+ "--about",
62
+ "--reason",
63
+ "--actor",
64
+ "--namespace",
65
+ ]);
66
+ const result = [];
67
+ let skipNext = false;
68
+ for (let i = startIndex; i < args.length; i++) {
69
+ if (skipNext) {
70
+ skipNext = false;
71
+ continue;
72
+ }
73
+ if (args[i] === "--json")
74
+ continue;
75
+ if (flagsWithValue.has(args[i])) {
76
+ skipNext = true;
77
+ continue;
78
+ }
79
+ if (args[i].startsWith("--"))
80
+ continue;
81
+ result.push(args[i]);
82
+ }
83
+ return result;
84
+ }
85
+ /**
86
+ * Build an `EmbeddingConfig` from MEMOS_EMBEDDING_* environment variables.
87
+ * Returns null when MEMOS_EMBEDDING_PROVIDER is unset so callers fall
88
+ * through to the config file / SDK default.
89
+ */
90
+ function cliEmbeddingsConfig(project) {
91
+ const provider = process.env.MEMOS_EMBEDDING_PROVIDER ?? project?.provider;
92
+ if (!provider)
93
+ return null;
94
+ const dimensions = process.env.MEMOS_EMBEDDING_DIMENSIONS;
95
+ const dimensionsRaw = process.env.MEMOS_EMBEDDING_DIMENSIONS;
96
+ return {
97
+ provider: provider,
98
+ model: process.env.MEMOS_EMBEDDING_MODEL ?? project?.model,
99
+ baseUrl: process.env.MEMOS_EMBEDDING_BASE_URL ?? project?.baseUrl,
100
+ apiKey: process.env.MEMOS_EMBEDDING_API_KEY,
101
+ dimensions: dimensionsRaw
102
+ ? parseInt(dimensionsRaw, 10)
103
+ : project?.dimensions,
104
+ queryPrefix: process.env.MEMOS_EMBEDDING_QUERY_PREFIX,
105
+ documentPrefix: process.env.MEMOS_EMBEDDING_DOCUMENT_PREFIX,
106
+ };
107
+ }
108
+ function configFilePath() {
109
+ return join(os.homedir(), ".memos", "config.json");
110
+ }
111
+ /** Read ~/.memos/config.json. Returns null when the file is missing or invalid. */
112
+ function loadFileConfig() {
113
+ try {
114
+ const raw = readFileSync(configFilePath(), "utf-8");
115
+ const parsed = JSON.parse(raw);
116
+ if (typeof parsed !== "object" || parsed === null)
117
+ return null;
118
+ return parsed;
119
+ }
120
+ catch {
121
+ return null;
122
+ }
123
+ }
124
+ /** Persist the config file, creating ~/.memos when needed. */
125
+ function saveFileConfig(config) {
126
+ mkdirSync(dirname(configFilePath()), { recursive: true });
127
+ writeFileSync(configFilePath(), JSON.stringify(config, null, 2) + "\n");
128
+ }
129
+ /** Prompt the user with a yes/no question. Resolves true only for y/yes. */
130
+ function askYesNo(question) {
131
+ return new Promise((resolve) => {
132
+ const rl = readline.createInterface({
133
+ input: process.stdin,
134
+ output: process.stdout,
135
+ });
136
+ rl.question(question, (answer) => {
137
+ rl.close();
138
+ resolve(/^y(es)?$/i.test(answer.trim()));
139
+ });
140
+ });
141
+ }
142
+ /**
143
+ * Beta auto-prompt: if the beta channel is on and we haven't checked in a
144
+ * while, look for a newer fully-successful main commit and print a notice
145
+ * to stderr. Fail-open and cached — a network failure never breaks the
146
+ * command being run, and stdout stays clean for scripts/JSON.
147
+ */
148
+ async function maybeBetaUpdateNotice() {
149
+ const { readBetaState, writeBetaState, findLatestGreenCommit, resolveInstalledRef, UPDATE_CHECK_INTERVAL_MS, } = await import("./updater.js");
150
+ const cfgPath = configFilePath();
151
+ const state = readBetaState(cfgPath);
152
+ if (!state.beta)
153
+ return;
154
+ const now = Date.now();
155
+ if (state.lastUpdateCheck &&
156
+ now - state.lastUpdateCheck < UPDATE_CHECK_INTERVAL_MS) {
157
+ return;
158
+ }
159
+ // Only the newest commit here — this is a cheap background nudge, not the
160
+ // full walk-back that `memos update` does.
161
+ const green = await findLatestGreenCommit({ maxCommits: 1 });
162
+ writeBetaState(cfgPath, { lastUpdateCheck: now });
163
+ if (!green)
164
+ return;
165
+ const installed = resolveInstalledRef();
166
+ if (installed === green.sha)
167
+ return;
168
+ const firstLine = green.message.split("\n")[0];
169
+ console.error(`\n[memos] Beta update available: ${green.sha.slice(0, 7)} — ${firstLine}\n` +
170
+ `[memos] Run \`memos update\` to install it.\n`);
171
+ }
172
+ /**
173
+ * Embedding config for the CLI: MEMOS_EMBEDDING_* wins, then the config
174
+ * file, then nothing (SDK default — embeddings on, fastembed provider).
175
+ */
176
+ function resolveCliEmbeddingsConfig() {
177
+ const fromEnv = cliEmbeddingsConfig();
178
+ if (fromEnv)
179
+ return fromEnv;
180
+ const file = loadFileConfig();
181
+ const e = file?.embeddings;
182
+ if (!e || Object.keys(e).length === 0)
183
+ return null;
184
+ return {
185
+ ...(e.enabled !== undefined ? { enabled: e.enabled } : {}),
186
+ ...(e.provider ? { provider: e.provider } : {}),
187
+ ...(e.model ? { model: e.model } : {}),
188
+ ...(e.baseUrl ? { baseUrl: e.baseUrl } : {}),
189
+ ...(e.dimensions ? { dimensions: e.dimensions } : {}),
190
+ ...(e.queryPrefix ? { queryPrefix: e.queryPrefix } : {}),
191
+ ...(e.documentPrefix ? { documentPrefix: e.documentPrefix } : {}),
192
+ };
193
+ }
194
+ /** Database path: --db wins, then the config file, then the SDK default. */
195
+ function resolveDbPath(cliDbPath) {
196
+ if (cliDbPath)
197
+ return cliDbPath;
198
+ return loadFileConfig()?.dbPath;
199
+ }
200
+ /** Prompt for a line of input; returns the default when the user hits enter. */
201
+ async function promptText(rl, label, defaultValue) {
202
+ const answer = (await rl.question(`${label} [${defaultValue}]: `)).trim();
203
+ return answer === "" ? defaultValue : answer;
204
+ }
205
+ /** Numbered-choice prompt. Returns the chosen key, or the default on empty. */
206
+ async function promptChoice(rl, label, options, defaultKey) {
207
+ console.log(label);
208
+ options.forEach((o, i) => {
209
+ const marker = o.key === defaultKey ? " (default)" : "";
210
+ console.log(` ${i + 1}) ${o.label}${o.hint ? ` — ${o.hint}` : ""}${marker}`);
211
+ });
212
+ for (;;) {
213
+ const answer = (await rl.question(`Choice [${options.findIndex((o) => o.key === defaultKey) + 1}]: `)).trim();
214
+ if (answer === "")
215
+ return defaultKey;
216
+ const n = parseInt(answer, 10);
217
+ if (!Number.isNaN(n) && n >= 1 && n <= options.length) {
218
+ return options[n - 1].key;
219
+ }
220
+ console.log(`Enter a number 1–${options.length}, or press enter for the default.`);
221
+ }
222
+ }
223
+ /**
224
+ * Prompt for a passphrase with hidden (non-echoed) input.
225
+ * Used for the sync key when no --passphrase/--passphrase-file/MEMOS_SYNC_KEY is set.
226
+ */
227
+ async function promptHidden(label) {
228
+ const readline = await import("node:readline");
229
+ const rl = readline.createInterface({
230
+ input: process.stdin,
231
+ output: process.stdout,
232
+ terminal: true,
233
+ });
234
+ // Mute echo while the user types the passphrase.
235
+ const rlAny = rl;
236
+ const originalWrite = rlAny._writeToOutput.bind(rl);
237
+ process.stdout.write(label);
238
+ rlAny._writeToOutput = () => { };
239
+ return new Promise((resolve) => {
240
+ rlAny.question("", (answer) => {
241
+ rlAny._writeToOutput = originalWrite;
242
+ rl.close();
243
+ process.stdout.write("\n");
244
+ resolve(answer);
245
+ });
246
+ });
247
+ }
248
+ /**
249
+ * Resolve the sync key, prompting interactively (hidden input) when no
250
+ * key was provided via --passphrase, --passphrase-file, or MEMOS_SYNC_KEY and stdin
251
+ * is a TTY. Non-TTY (piped/CI) keeps the hard error — scripts must
252
+ * pass the key explicitly.
253
+ */
254
+ async function resolveSyncKeyInteractive(opts) {
255
+ const { resolveSyncKey } = await import("./sync.js");
256
+ try {
257
+ return resolveSyncKey(opts);
258
+ }
259
+ catch (err) {
260
+ if (!process.stdin.isTTY)
261
+ throw err;
262
+ const passphrase = await promptHidden("Sync passphrase: ");
263
+ if (!passphrase)
264
+ throw err;
265
+ return passphrase;
266
+ }
267
+ }
268
+ /**
269
+ * `memos init` — walk the user through database path + embedding provider,
270
+ * smoke-test the setup (store → semantic search → forget a probe memory),
271
+ * and save ~/.memos/config.json. `memos init --yes` accepts all defaults
272
+ * without prompting (for scripts); a non-TTY stdin (piped/CI) is also
273
+ * treated as non-interactive automatically.
274
+ */
275
+ async function runInitWizard(cliArgs) {
276
+ // --yes/-y forces non-interactive; so does a non-TTY stdin (piped/CI),
277
+ // so scripts never hang on a readline prompt they can't answer.
278
+ const nonInteractive = cliArgs.includes("--yes") ||
279
+ cliArgs.includes("-y") ||
280
+ process.stdin.isTTY === false;
281
+ const { createInterface } = await import("node:readline/promises");
282
+ const rl = nonInteractive
283
+ ? null
284
+ : createInterface({ input: process.stdin, output: process.stdout });
285
+ try {
286
+ const existing = loadFileConfig();
287
+ if (existing && !nonInteractive) {
288
+ console.log(`Existing config found at ${configFilePath()}.`);
289
+ const again = await promptChoice(rl, "Re-run the wizard and overwrite it?", [
290
+ { key: "yes", label: "Yes, overwrite" },
291
+ { key: "no", label: "No, keep existing" },
292
+ ], "no");
293
+ if (again === "no") {
294
+ console.log("Keeping existing config. Nothing changed.");
295
+ return;
296
+ }
297
+ }
298
+ const defaultDb = join(os.homedir(), ".memos", "memos.db");
299
+ const dbPath = nonInteractive
300
+ ? (existing?.dbPath ?? defaultDb)
301
+ : await promptText(rl, "Database path", existing?.dbPath ?? defaultDb);
302
+ const providerChoice = nonInteractive
303
+ ? (existing?.embeddings?.provider ?? "fastembed")
304
+ : await promptChoice(rl, "Embedding provider (semantic search):", [
305
+ {
306
+ key: "fastembed",
307
+ label: "Local (fastembed)",
308
+ hint: "free, on-device; needs `npm i @huggingface/transformers` for real vectors",
309
+ },
310
+ {
311
+ key: "openai-compatible",
312
+ label: "OpenAI-compatible server",
313
+ hint: "e.g. scripts/embed-server.py on a GPU box",
314
+ },
315
+ {
316
+ key: "ollama",
317
+ label: "Ollama",
318
+ hint: "local models via Ollama",
319
+ },
320
+ {
321
+ key: "off",
322
+ label: "Off",
323
+ hint: "keyword-only search, no embeddings",
324
+ },
325
+ ], "fastembed");
326
+ const embeddings = {};
327
+ if (providerChoice === "off") {
328
+ embeddings.enabled = false;
329
+ }
330
+ else {
331
+ embeddings.enabled = true;
332
+ embeddings.provider = providerChoice;
333
+ if (providerChoice === "openai-compatible") {
334
+ embeddings.baseUrl = nonInteractive
335
+ ? (existing?.embeddings?.baseUrl ?? "http://localhost:8000")
336
+ : await promptText(rl, "Embedding server base URL", existing?.embeddings?.baseUrl ?? "http://localhost:8000");
337
+ embeddings.model = nonInteractive
338
+ ? (existing?.embeddings?.model ?? "BAAI/bge-small-en-v1.5")
339
+ : await promptText(rl, "Embedding model", existing?.embeddings?.model ?? "BAAI/bge-small-en-v1.5");
340
+ const dimsDefault = String(existing?.embeddings?.dimensions ?? 384);
341
+ const dimsAnswer = nonInteractive
342
+ ? dimsDefault
343
+ : await promptText(rl, "Embedding dimensions", dimsDefault);
344
+ const dims = parseInt(dimsAnswer, 10);
345
+ if (!Number.isNaN(dims) && dims > 0)
346
+ embeddings.dimensions = dims;
347
+ console.log("Note: if the server needs an API key, set MEMOS_EMBEDDING_API_KEY — keys are never written to the config file.");
348
+ }
349
+ else if (providerChoice === "ollama") {
350
+ embeddings.baseUrl = nonInteractive
351
+ ? (existing?.embeddings?.baseUrl ?? "http://localhost:11434")
352
+ : await promptText(rl, "Ollama base URL", existing?.embeddings?.baseUrl ?? "http://localhost:11434");
353
+ embeddings.model = nonInteractive
354
+ ? (existing?.embeddings?.model ?? "nomic-embed-text")
355
+ : await promptText(rl, "Ollama embedding model", existing?.embeddings?.model ?? "nomic-embed-text");
356
+ }
357
+ else {
358
+ console.log("Note: real local vectors need `pnpm add @huggingface/transformers`; " +
359
+ "without it MemOS falls back to a deterministic local hash and warns loudly.");
360
+ }
361
+ }
362
+ // Smoke test: open the store with this exact config, round-trip a probe.
363
+ console.log("\nSmoke-testing the setup…");
364
+ const memos = new MemOS({
365
+ dbPath,
366
+ ...(Object.keys(embeddings).length > 0 ? { embeddings } : {}),
367
+ });
368
+ try {
369
+ await memos.init();
370
+ const probe = `memos init smoke test ${Date.now()}`;
371
+ const { node } = await memos.store(probe);
372
+ const info = memos.getEmbeddingRuntimeInfo();
373
+ if (info) {
374
+ if (info.fallbackActive) {
375
+ console.log(` embeddings: ${info.resolvedProvider} (fallback — ${info.fallbackReason})`);
376
+ }
377
+ else {
378
+ console.log(` embeddings: ${info.resolvedProvider} (${info.resolvedModel}, ${info.observedDimensions ?? info.requestedDimensions}d)`);
379
+ }
380
+ }
381
+ else {
382
+ console.log(" embeddings: disabled (keyword search only)");
383
+ }
384
+ const hits = await memos.search({
385
+ query: "memos init smoke test",
386
+ limit: 3,
387
+ });
388
+ const found = hits.some((h) => h.node.id === node.id);
389
+ console.log(` store → search round-trip: ${found ? "ok" : "PROBE NOT FOUND"}`);
390
+ await memos.forget(node.id);
391
+ if (!found) {
392
+ console.error("Smoke test failed: probe memory was not retrievable.");
393
+ process.exit(1);
394
+ }
395
+ console.log(" probe memory cleaned up.");
396
+ }
397
+ finally {
398
+ await memos.close();
399
+ }
400
+ const config = { version: 1, dbPath };
401
+ if (Object.keys(embeddings).length > 0)
402
+ config.embeddings = embeddings;
403
+ saveFileConfig(config);
404
+ console.log(`\nSaved ${configFilePath()}.`);
405
+ console.log("Every memos command (including `memos mcp`) now uses these settings.");
406
+ console.log("Precedence: --db and MEMOS_EMBEDDING_* env vars still override the file.");
407
+ }
408
+ finally {
409
+ rl?.close();
410
+ }
411
+ }
412
+ const MCP_COMMAND = ["npx", "-y", "@mem-os/sdk", "mcp"];
413
+ function mcpServerEntry(dbPath) {
414
+ const env = {};
415
+ if (dbPath)
416
+ env.MEMOS_DB_PATH = dbPath;
417
+ if (process.env.MEMOS_EMBEDDING_PROVIDER)
418
+ env.MEMOS_EMBEDDING_PROVIDER = process.env.MEMOS_EMBEDDING_PROVIDER;
419
+ if (process.env.MEMOS_EMBEDDING_MODEL)
420
+ env.MEMOS_EMBEDDING_MODEL = process.env.MEMOS_EMBEDDING_MODEL;
421
+ if (process.env.MEMOS_EMBEDDING_BASE_URL)
422
+ env.MEMOS_EMBEDDING_BASE_URL = process.env.MEMOS_EMBEDDING_BASE_URL;
423
+ if (process.env.MEMOS_EMBEDDING_DIMENSIONS)
424
+ env.MEMOS_EMBEDDING_DIMENSIONS = process.env.MEMOS_EMBEDDING_DIMENSIONS;
425
+ if (process.env.MEMOS_EMBEDDING_QUERY_PREFIX)
426
+ env.MEMOS_EMBEDDING_QUERY_PREFIX = process.env.MEMOS_EMBEDDING_QUERY_PREFIX;
427
+ if (process.env.MEMOS_EMBEDDING_DOCUMENT_PREFIX)
428
+ env.MEMOS_EMBEDDING_DOCUMENT_PREFIX =
429
+ process.env.MEMOS_EMBEDDING_DOCUMENT_PREFIX;
430
+ return {
431
+ command: MCP_COMMAND[0],
432
+ args: MCP_COMMAND.slice(1),
433
+ ...(Object.keys(env).length > 0 ? { env } : {}),
434
+ };
435
+ }
436
+ function connectTarget(target, opts) {
437
+ const entry = mcpServerEntry(opts.dbPath);
438
+ const jsonEntry = JSON.stringify({ mcpServers: { memos: entry } }, null, 2);
439
+ switch (target) {
440
+ case "claude-code":
441
+ case "claude":
442
+ case "claudecode": {
443
+ const args = MCP_COMMAND.map((a) => `"${a}"`).join(" ");
444
+ return {
445
+ instructions: [
446
+ "Claude Code — pick ONE:",
447
+ "",
448
+ " # User scope (available in every project):",
449
+ ` claude mcp add memos -s user -- ${MCP_COMMAND.join(" ")}`,
450
+ "",
451
+ " # Project scope (checked into the repo for the whole team):",
452
+ " memos connect claude-code --write",
453
+ " # → writes ./.mcp.json:",
454
+ jsonEntry,
455
+ "",
456
+ ` Or run inside Claude Code: /plugin marketplace add Markgatcha/memos`,
457
+ ` then /plugin install memos@memos-marketplace (bundles this MCP`,
458
+ ` server + /memos, /recall commands + a memory skill).`,
459
+ "",
460
+ ` Manual one-liner equivalent: claude mcp add memos -s user -- ${args}`,
461
+ "",
462
+ " # Session hook (auto-extract facts at session end):",
463
+ " # Add to .claude/settings.json:",
464
+ " {",
465
+ ' "hooks": {',
466
+ ' "SessionEnd": [{ "hooks": [{ "type": "command", "command": "memos session-hook" }] }]',
467
+ " }",
468
+ " }",
469
+ ].join("\n"),
470
+ configPath: ".mcp.json",
471
+ fileContents: jsonEntry + "\n",
472
+ };
473
+ }
474
+ case "cursor": {
475
+ return {
476
+ instructions: [
477
+ "Cursor — MCP config at ~/.cursor/mcp.json:",
478
+ jsonEntry,
479
+ "",
480
+ "Or: memos connect cursor --write (writes the file; --force overwrites).",
481
+ "Restart Cursor, then enable the memos server in MCP settings.",
482
+ ].join("\n"),
483
+ configPath: join(homeDir(), ".cursor", "mcp.json"),
484
+ fileContents: jsonEntry + "\n",
485
+ };
486
+ }
487
+ case "windsurf": {
488
+ return {
489
+ instructions: [
490
+ "Windsurf — MCP config at ~/.codeium/windsurf/mcp_config.json:",
491
+ jsonEntry,
492
+ "",
493
+ "Or: memos connect windsurf --write",
494
+ ].join("\n"),
495
+ configPath: join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
496
+ fileContents: jsonEntry + "\n",
497
+ };
498
+ }
499
+ case "cline": {
500
+ return {
501
+ instructions: [
502
+ "Cline (VS Code) — MCP settings file:",
503
+ " Windows: %APPDATA%\\Code\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json",
504
+ " macOS: ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json",
505
+ "",
506
+ "Merge this into the mcpServers object:",
507
+ jsonEntry,
508
+ ].join("\n"),
509
+ };
510
+ }
511
+ case "opencode": {
512
+ return {
513
+ instructions: [
514
+ "OpenCode — add to opencode.json (project) or ~/.config/opencode/opencode.json:",
515
+ JSON.stringify({
516
+ mcp: {
517
+ memos: { type: "local", command: MCP_COMMAND },
518
+ },
519
+ }, null, 2),
520
+ "",
521
+ "Or: memos connect opencode --write (writes ./opencode.json).",
522
+ ].join("\n"),
523
+ configPath: "opencode.json",
524
+ fileContents: JSON.stringify({ mcp: { memos: { type: "local", command: MCP_COMMAND } } }, null, 2) + "\n",
525
+ };
526
+ }
527
+ case "codex": {
528
+ const toml = `[mcp_servers.memos]\ncommand = "${MCP_COMMAND[0]}"\nargs = [${MCP_COMMAND.slice(1)
529
+ .map((a) => `"${a}"`)
530
+ .join(", ")}]\n`;
531
+ return {
532
+ instructions: [
533
+ "Codex CLI — add to ~/.codex/config.toml:",
534
+ toml,
535
+ "Or: memos connect codex --write (appends to the file).",
536
+ ].join("\n"),
537
+ configPath: join(homeDir(), ".codex", "config.toml"),
538
+ fileContents: toml,
539
+ };
540
+ }
541
+ case "gemini": {
542
+ return {
543
+ instructions: [
544
+ "Gemini CLI — MCP config at ~/.gemini/settings.json:",
545
+ jsonEntry,
546
+ "",
547
+ "Or: memos connect gemini --write",
548
+ ].join("\n"),
549
+ configPath: join(homeDir(), ".gemini", "settings.json"),
550
+ fileContents: jsonEntry + "\n",
551
+ };
552
+ }
553
+ case "generic":
554
+ case "any":
555
+ case "mcp": {
556
+ // Tool list is injected by the caller from getMcpTools() so the
557
+ // count and names can never go stale.
558
+ const short = (opts.toolNames ?? []).map((n) => n.replace(/^memos_/, ""));
559
+ return {
560
+ instructions: [
561
+ "Any MCP-capable harness — the MemOS server is a plain stdio MCP",
562
+ `server. Launch command: ${MCP_COMMAND.join(" ")}`,
563
+ "",
564
+ "MCP config shape (mcpServers object):",
565
+ jsonEntry,
566
+ "",
567
+ "The server speaks MCP 2026-07-28 with a legacy-2025 fallback and",
568
+ `exposes ${short.length} tools: ${short.join("/")}.`,
569
+ "All data stays in local SQLite.",
570
+ ].join("\n"),
571
+ };
572
+ }
573
+ default:
574
+ console.error("Unknown target. Supported: claude-code, cursor, windsurf, cline, " +
575
+ "opencode, codex, gemini, generic.");
576
+ return null;
577
+ }
578
+ }
579
+ function homeDir() {
580
+ return os.homedir();
581
+ }
582
+ function existsSyncSafe(path) {
583
+ try {
584
+ return statSync(path).isFile();
585
+ }
586
+ catch {
587
+ return false;
588
+ }
589
+ }
590
+ function writeFileSafe(path, contents) {
591
+ mkdirSync(dirname(path), { recursive: true });
592
+ writeFileSync(path, contents, "utf8");
593
+ }
594
+ function printHelp() {
595
+ console.log(`
596
+ MemOS — Universal memory layer for AI agents.
597
+
598
+ Usage:
599
+ memos <command> [options]
600
+
601
+ Commands:
602
+ init Interactive setup wizard: database path, embedding
603
+ provider, smoke test — saves ~/.memos/config.json
604
+ (--yes accepts all defaults)
605
+ store <content> Store a new memory (--pool, --context,
606
+ --scope user:alice[,agent:coder][,run:r1])
607
+ retrieve <id> Retrieve a memory by ID
608
+ search <query> Search memories by text (--pool event|note|procedure,
609
+ --scope user:alice[,...])
610
+ forget <id> Delete a memory by ID
611
+ summarize Summarize all memories
612
+ graph Print the full memory graph (--mermaid for a
613
+ GitHub-renderable Mermaid diagram)
614
+ browse Interactive terminal browser (search, inspect,
615
+ forget memories)
616
+ image <add|list|search|show|purge>
617
+ Image memories (needs --vision, or
618
+ --vision-clip to also match pixels): add
619
+ <path|url> [--caption <text>] [--kind <kind>]
620
+ [--tags a,b]; list; search <query>;
621
+ show <ref|id> [--out <path>]; purge
622
+ link <src> <dst> Link two memories
623
+ count Show memory count
624
+ tag <id> <tag> [...] Add tags to a memory
625
+ untag <id> <tag> [...] Remove tags from a memory
626
+ list --tag <tag> List memories by tag
627
+ export Export memories to file
628
+ backup Backup the database
629
+ restore <path> Restore from a backup
630
+ import-external <file> Import a ChatGPT/Claude memory export
631
+ (--source auto|chatgpt|claude|generic, --dry-run,
632
+ --max-items <n>, --namespace <ns>)
633
+ import Import a chat export: --from chatgpt|claude|slack
634
+ (--file <path> | --dir <path>, --synthesize,
635
+ --dry-run, --max-items <n>, --namespace <ns>)
636
+ learn "<lesson>" Capture a procedural lesson — behavioral guidance,
637
+ not a fact (--context <ctx>, --tags a,b)
638
+ lesson <id> Record lesson outcome: --outcome success|failure
639
+ (reinforces or demotes the lesson score)
640
+ lessons List procedural lessons (--namespace <ns>,
641
+ --limit <n>)
642
+ cite <id> Trace a [mem:xxxx] citation token back to its
643
+ full source memory
644
+ quarantine <list|release>
645
+ Review the write-gate quarantine queue: list
646
+ flagged memories, release one back into recall
647
+ (--limit <n>, --json)
648
+ harness <list|merge> Cross-harness memory: per-harness memory counts
649
+ (list), or merge another harness's DB file into
650
+ this one (merge --from <db-path> [--dry-run])
651
+ scope list List scoped memory spaces (namespaces) with counts
652
+ (--json). Scoped reads union the shared default
653
+ scope with the requested scope(s); free-form
654
+ scopes use the project:<name> / harness:<name>
655
+ convention
656
+ history <id> Version timeline for one memory: supersedes,
657
+ superseded by, derived notes
658
+ revert Revert a memory to its previous version
659
+ (--last | --id <id> | --about <topic> |
660
+ "<natural language>"; --dry-run, --reason <r>,
661
+ --actor <a>)
662
+ reminders List scheduled event reminders, earliest first
663
+ (--due: only reminders due as of now;
664
+ --namespace <ns>, --json)
665
+ remind "<text>" at <when>
666
+ Store a reminder firing at <when>
667
+ (e.g. memos remind "call mom" at tomorrow 9am)
668
+ digest Run consolidation now and print a summary
669
+ consolidate Offline maintenance pass: merge duplicates, archive
670
+ stale memories, supersede decayed ones (kept as
671
+ history), distill cluster summary notes
672
+ (--dry-run, --no-summarize, --no-decay,
673
+ --decay-half-life <days>, --min-retention <score>,
674
+ --older-than <days>, --namespace <ns>, --watch,
675
+ --interval-min <n>)
676
+ encrypt Encrypt the database in place (--key <key> or
677
+ MEMOS_KEY; requires better-sqlite3-multiple-ciphers)
678
+ decrypt Remove encryption in place (--key <key>)
679
+ stats Token-savings telemetry for this process
680
+ (packs built, tokens injected vs naive baseline)
681
+ compact Fidelity backfill: generate missing L1/L2 levels
682
+ for stored memories (--stats shows avg tokens per
683
+ level, --namespace <ns>, --limit <n>, --dry-run)
684
+ doctor Health-check the store, embedding config and endpoints
685
+ connect <target> Register the MemOS MCP server with a coding harness
686
+ (claude-code, cursor, windsurf, cline, opencode,
687
+ codex, gemini, generic) — --write saves the config
688
+ reindex-embeddings Re-embed all memories with the configured provider
689
+ (--purge-stale deletes vectors from other models first)
690
+ mcp Start the MemOS MCP stdio server
691
+ trio [--up] Show (or launch) the full AI Trio: MemOS + LLM-Guardian + Universal-MCP-Toolkit
692
+ completion <shell> Print shell completion script (bash|zsh|fish)
693
+ help Show this help message
694
+
695
+ Options:
696
+ --db <path> Database path (default: ~/.memos/memos.db)
697
+ --type <type> Memory type (store command)
698
+ --ttl <seconds> TTL in seconds (store command)
699
+ --limit <n> Result limit (search command)
700
+ --harness <name|all> Scope search to one harness's memories
701
+ (search command; default: all harnesses)
702
+ --namespaces <a,b> Search across comma-separated namespaces
703
+ (search command; unions the shared default
704
+ namespace like --scope does)
705
+ --namespace <ns> Write into a free-form scope/namespace
706
+ (store command; convention project:<name> /
707
+ harness:<name>; wins over --scope)
708
+ --force Bypass the write-side salience triage
709
+ (store command; triage is on by default)
710
+ --provenance <tier> Provenance tier for the write: user-verified,
711
+ user, tool-output, chat, imported
712
+ (store command; tool-derived evidence should use
713
+ tool-output — it retrieves at a lower trust tier)
714
+ --format <fmt> Export format: json, markdown, obsidian
715
+ --output <path> Output path (export/backup)
716
+ --tag <tag> Tag filter (export/list)
717
+ --json Output as JSON
718
+ --vision Enable image memory (caption track)
719
+ --vision-clip Also enable CLIP pixel search (~350 MB model on
720
+ first use)
721
+
722
+ Examples:
723
+ memos store "User prefers dark mode" --type preference
724
+ memos store "Temp note" --ttl 3600
725
+ memos search "dark mode" --limit 5
726
+ memos graph --mermaid > graph.mmd
727
+ memos browse
728
+ memos tag <id> work important
729
+ memos list --tag work
730
+ memos export --format markdown --output ./my-export
731
+ memos backup --output ./backup.db
732
+ memos restore ./backup.db
733
+ memos mcp --db ~/.memos/memos.db
734
+ `);
735
+ }
736
+ async function main() {
737
+ if (!command ||
738
+ command === "help" ||
739
+ command === "--help" ||
740
+ command === "-h") {
741
+ printHelp();
742
+ process.exit(0);
743
+ }
744
+ // Per-command help: `memos <command> --help` prints focused usage instead of
745
+ // being misinterpreted as an argument (e.g. `memos search --help` used to
746
+ // literally search for the string "--help").
747
+ if (CLI_COMMANDS.includes(command) &&
748
+ args.slice(1).some((a) => a === "--help" || a === "-h")) {
749
+ const detail = getCommandHelp(command);
750
+ if (detail) {
751
+ console.log(detail);
752
+ process.exit(0);
753
+ }
754
+ }
755
+ const dbFlagIdx = args.indexOf("--db");
756
+ const cliDbPath = dbFlagIdx !== -1 ? args[dbFlagIdx + 1] : undefined;
757
+ // --db wins, then ~/.memos/config.json (written by `memos init`), then the
758
+ // SDK default (~/.memos/memos.db).
759
+ const dbPath = resolveDbPath(cliDbPath);
760
+ const jsonFlag = args.includes("--json");
761
+ // Memory-as-code: nearest .memos.json (walking up) supplies policy
762
+ // defaults — scope, consolidation knobs, embedding routing, and the
763
+ // NAME of the env var holding the encryption key.
764
+ const projectConfig = loadProjectConfig(process.cwd());
765
+ const projectScope = projectConfig?.scope &&
766
+ (projectConfig.scope.user || projectConfig.scope.agent)
767
+ ? {
768
+ ...(projectConfig.scope.user
769
+ ? { user: projectConfig.scope.user }
770
+ : {}),
771
+ ...(projectConfig.scope.agent
772
+ ? { agent: projectConfig.scope.agent }
773
+ : {}),
774
+ }
775
+ : undefined;
776
+ // Encryption key for at-rest-encrypted databases: --key, the project's
777
+ // cipherKeyEnv, or MEMOS_KEY.
778
+ // encrypt/decrypt are excluded — they perform the conversion itself and
779
+ // must open the source at its CURRENT (pre/post conversion) state.
780
+ const keyFlagIdx = args.indexOf("--key");
781
+ const cipherKey = command === "encrypt" || command === "decrypt"
782
+ ? undefined
783
+ : keyFlagIdx !== -1
784
+ ? args[keyFlagIdx + 1]
785
+ : process.env[cipherKeyEnvName(projectConfig)];
786
+ if (command === "completion") {
787
+ const shell = args[1];
788
+ const script = shell ? getCompletionScript(shell) : null;
789
+ if (!script) {
790
+ console.error(`Usage: memos completion <${COMPLETION_SHELLS.join("|")}>\n` +
791
+ `Unknown shell: ${shell ?? "(none given)"}`);
792
+ process.exit(1);
793
+ }
794
+ console.log(script);
795
+ return;
796
+ }
797
+ if (command === "mcp") {
798
+ const { runMcpServer } = await import("./mcp.js");
799
+ const embeddings = cliEmbeddingsConfig(projectConfig?.embeddings);
800
+ await runMcpServer({
801
+ dbPath,
802
+ ...(embeddings ? { embeddings: { ...embeddings, enabled: true } } : {}),
803
+ });
804
+ return;
805
+ }
806
+ // Beta channel opt-in and beta updates. Neither needs the database open.
807
+ if (command === "beta") {
808
+ const { readBetaState, writeBetaState } = await import("./updater.js");
809
+ const sub = args[1];
810
+ const cfgPath = configFilePath();
811
+ if (!sub || sub === "status") {
812
+ const state = readBetaState(cfgPath);
813
+ console.log(`Beta channel: ${state.beta ? "enabled" : "disabled"}`);
814
+ if (!state.beta) {
815
+ console.log("Run `memos beta enable` to be prompted on every fully-successful main commit.");
816
+ }
817
+ return;
818
+ }
819
+ if (sub === "enable" || sub === "on") {
820
+ writeBetaState(cfgPath, { beta: true });
821
+ console.log("Beta channel enabled.");
822
+ console.log("You'll be prompted to update whenever a new fully-successful main commit lands (CI, Prebuilds, and CodeQL all green).");
823
+ console.log("Run `memos update --check` to check right now.");
824
+ return;
825
+ }
826
+ if (sub === "disable" || sub === "off") {
827
+ writeBetaState(cfgPath, { beta: false });
828
+ console.log("Beta channel disabled.");
829
+ return;
830
+ }
831
+ console.error("Usage: memos beta [enable|disable|status]");
832
+ process.exit(1);
833
+ }
834
+ if (command === "update") {
835
+ const { findLatestGreenCommit, resolveInstalledRef, installFromCommit, memosHomeDir, } = await import("./updater.js");
836
+ const checkOnly = args.includes("--check");
837
+ const yes = args.includes("--yes");
838
+ let green;
839
+ try {
840
+ green = await findLatestGreenCommit({ maxCommits: 10 });
841
+ }
842
+ catch (err) {
843
+ console.error(`Could not reach GitHub: ${err instanceof Error ? err.message : String(err)}`);
844
+ process.exit(1);
845
+ }
846
+ if (!green) {
847
+ console.log("No fully-successful main commit found in the last 10 commits (CI may still be running). Try again later.");
848
+ return;
849
+ }
850
+ const short = green.sha.slice(0, 7);
851
+ const installed = resolveInstalledRef();
852
+ const firstLine = green.message.split("\n")[0];
853
+ if (jsonFlag) {
854
+ console.log(JSON.stringify({
855
+ latestGreen: green.sha,
856
+ installed,
857
+ upToDate: installed === green.sha,
858
+ }, null, 2));
859
+ return;
860
+ }
861
+ console.log(`Newest fully-successful main commit: ${short} — ${firstLine}`);
862
+ console.log(`Installed: ${installed ?? "unknown"}`);
863
+ if (installed === green.sha) {
864
+ console.log("Already up to date.");
865
+ return;
866
+ }
867
+ if (checkOnly)
868
+ return;
869
+ let proceed = yes;
870
+ if (!proceed) {
871
+ if (!process.stdin.isTTY) {
872
+ console.log("Run `memos update --yes` to install it non-interactively.");
873
+ return;
874
+ }
875
+ proceed = await askYesNo("Install this beta build from source now? [y/N] ");
876
+ }
877
+ if (!proceed) {
878
+ console.log("Not installing.");
879
+ return;
880
+ }
881
+ console.log(`Installing ${short} from source (download + pnpm install + build)...`);
882
+ const { execSync } = await import("node:child_process");
883
+ const { installDir } = await installFromCommit(green.sha, {
884
+ memosHome: memosHomeDir(),
885
+ nodeBin: process.execPath,
886
+ runCmd: (cmd, a, cwd) => {
887
+ execSync([cmd, ...a]
888
+ .map((x) => (/\s/.test(x) ? JSON.stringify(x) : x))
889
+ .join(" "), { cwd, stdio: "inherit" });
890
+ },
891
+ download: async (url, dest) => {
892
+ const { pipeline } = await import("node:stream/promises");
893
+ const { createWriteStream } = await import("node:fs");
894
+ const res = await fetch(url);
895
+ if (!res.ok || !res.body)
896
+ throw new Error(`download failed: ${res.status}`);
897
+ await pipeline(res.body, createWriteStream(dest));
898
+ },
899
+ });
900
+ console.log(`Updated to ${short}. Installed at ${installDir}`);
901
+ console.log("Restart your shell (or rehash) so the new memos is picked up.");
902
+ return;
903
+ }
904
+ // Beta auto-prompt: at most every 6h, cached, stderr only, never fatal.
905
+ // Skipped for the commands that manage updates themselves.
906
+ if (!["beta", "update", "completion", "mcp"].includes(command)) {
907
+ await maybeBetaUpdateNotice().catch(() => { });
908
+ }
909
+ // Encrypt/decrypt operate on the raw database file and must run BEFORE
910
+ // the store is opened (Windows refuses to rename an open file).
911
+ if (command === "encrypt" || command === "decrypt") {
912
+ const keyIdx = args.indexOf("--key");
913
+ const key = keyIdx !== -1 ? args[keyIdx + 1] : process.env.MEMOS_KEY;
914
+ if (!key) {
915
+ console.error("Error: --key <key> (or the MEMOS_KEY environment variable) is required.");
916
+ process.exit(1);
917
+ }
918
+ if (!dbPath) {
919
+ console.error("Error: --db <path> is required.");
920
+ process.exit(1);
921
+ }
922
+ const { encryptDatabase, decryptDatabase } = await import("./storage/sqlite.js");
923
+ const backup = command === "encrypt"
924
+ ? await encryptDatabase(dbPath, key)
925
+ : await decryptDatabase(dbPath, key);
926
+ if (jsonFlag) {
927
+ console.log(JSON.stringify({ command, dbPath, backup }, null, 2));
928
+ }
929
+ else {
930
+ console.log(`${command === "encrypt" ? "Encrypted" : "Decrypted"}: ${dbPath}`);
931
+ console.log(` previous file kept as ${backup}`);
932
+ console.log(command === "encrypt"
933
+ ? " From now on provide the key: --key, MEMOS_KEY, or cipherKey config."
934
+ : " The database is now plaintext.");
935
+ }
936
+ return;
937
+ }
938
+ // Embedding config via environment (same variables the Python server
939
+ // and benchmark scripts accept). Without MEMOS_EMBEDDING_PROVIDER the
940
+ // CLI runs the default local-hash provider.
941
+ const embeddingsConfig = cliEmbeddingsConfig(projectConfig?.embeddings);
942
+ // Vision (image memory) config: `--vision` enables the caption track,
943
+ // `--vision-clip` additionally enables the CLIP pixel track. Env
944
+ // fallbacks (MEMOS_VISION=1 / MEMOS_VISION_CLIP=1) let scripts turn it
945
+ // on without flags. Off by default — a plain `memos` install has no
946
+ // image surface at all.
947
+ const visionEnabled = args.includes("--vision") ||
948
+ process.env.MEMOS_VISION === "1" ||
949
+ (projectConfig?.vision?.enabled ?? false);
950
+ const visionClipEnabled = args.includes("--vision-clip") ||
951
+ process.env.MEMOS_VISION_CLIP === "1" ||
952
+ (projectConfig?.vision?.clip?.enabled ?? false);
953
+ const memos = new MemOS({
954
+ dbPath,
955
+ ...(cipherKey ? { cipherKey } : {}),
956
+ ...(embeddingsConfig
957
+ ? { embeddings: { ...embeddingsConfig, enabled: true } }
958
+ : {}),
959
+ ...(visionEnabled || visionClipEnabled
960
+ ? {
961
+ vision: {
962
+ enabled: true,
963
+ ...(visionClipEnabled ? { clip: { enabled: true } } : {}),
964
+ },
965
+ }
966
+ : {}),
967
+ });
968
+ await memos.init();
969
+ try {
970
+ switch (command) {
971
+ case "store": {
972
+ const content = args[1];
973
+ if (!content) {
974
+ console.error("Error: content is required.\n Usage: memos store <content>");
975
+ process.exit(1);
976
+ }
977
+ const typeIdx = args.indexOf("--type");
978
+ const type = typeIdx !== -1 ? args[typeIdx + 1] : undefined;
979
+ const ttlIdx = args.indexOf("--ttl");
980
+ const ttl = ttlIdx !== -1 ? parseInt(args[ttlIdx + 1], 10) : undefined;
981
+ const tagIdx = args.indexOf("--tag");
982
+ let tags;
983
+ if (tagIdx !== -1) {
984
+ tags = [];
985
+ for (let i = tagIdx + 1; i < args.length; i++) {
986
+ if (args[i].startsWith("--"))
987
+ break;
988
+ tags.push(args[i]);
989
+ }
990
+ if (tags.length === 0)
991
+ tags = undefined;
992
+ }
993
+ const opts = {};
994
+ if (type)
995
+ opts.type = type;
996
+ if (ttl)
997
+ opts.ttl = ttl;
998
+ if (tags && tags.length > 0)
999
+ opts.tags = tags;
1000
+ const poolIdx = args.indexOf("--pool");
1001
+ const pool = poolIdx !== -1 ? args[poolIdx + 1] : undefined;
1002
+ if (pool === "event" || pool === "note" || pool === "procedure") {
1003
+ opts.pool = pool;
1004
+ }
1005
+ const contextIdx = args.indexOf("--context");
1006
+ const context = contextIdx !== -1 ? args[contextIdx + 1] : undefined;
1007
+ if (context)
1008
+ opts.context = context;
1009
+ const scopeIdx = args.indexOf("--scope");
1010
+ if (scopeIdx !== -1 && args[scopeIdx + 1]) {
1011
+ opts.scope = parseScopeArg(args[scopeIdx + 1]);
1012
+ }
1013
+ else if (projectScope) {
1014
+ opts.scope = projectScope;
1015
+ }
1016
+ // Free-form scope/namespace (convention: project:<name>,
1017
+ // harness:<name>). Explicit --namespace wins over --scope.
1018
+ const namespaceIdx = args.indexOf("--namespace");
1019
+ if (namespaceIdx !== -1 && args[namespaceIdx + 1]) {
1020
+ opts.namespace = args[namespaceIdx + 1];
1021
+ delete opts.scope;
1022
+ }
1023
+ const provenanceIdx = args.indexOf("--provenance");
1024
+ const provenanceArg = provenanceIdx !== -1 ? args[provenanceIdx + 1] : undefined;
1025
+ if (provenanceArg !== undefined) {
1026
+ if (!isProvenanceTier(provenanceArg)) {
1027
+ console.error(`Error: invalid --provenance "${provenanceArg}". ` +
1028
+ `Valid tiers: user-verified, user, tool-output, chat, imported.`);
1029
+ process.exit(1);
1030
+ }
1031
+ opts.provenance = provenanceArg;
1032
+ }
1033
+ // Write-side salience triage: the no-LLM retain filter drops
1034
+ // trivial/noise writes (acknowledgements, near-duplicates) before
1035
+ // they reach the store. `--force` bypasses it.
1036
+ const force = args.includes("--force");
1037
+ if (!force)
1038
+ opts.filterRetain = true;
1039
+ let result;
1040
+ try {
1041
+ result = await memos.store(content, opts);
1042
+ }
1043
+ catch (err) {
1044
+ if (err instanceof MemorySkippedError) {
1045
+ // Not a failure: the triage judged this write not worth
1046
+ // keeping. Debug detail behind MEMOS_DEBUG; one line on
1047
+ // stderr so the skip is never silent.
1048
+ if (process.env.MEMOS_DEBUG) {
1049
+ console.debug(`[retain-filter] skipped: ${err.message}`);
1050
+ }
1051
+ console.error(`Skipped: ${err.message} (use --force to store anyway)`);
1052
+ process.exit(0);
1053
+ }
1054
+ throw err;
1055
+ }
1056
+ if (jsonFlag) {
1057
+ console.log(JSON.stringify(result, null, 2));
1058
+ }
1059
+ else {
1060
+ console.log(`Stored memory: ${result.node.id}`);
1061
+ console.log(` Summary: ${result.node.summary}`);
1062
+ if (result.node.expiresAt) {
1063
+ const expDate = new Date(result.node.expiresAt * 1000).toISOString();
1064
+ console.log(` Expires: ${expDate}`);
1065
+ }
1066
+ if (result.node.tags.length > 0) {
1067
+ console.log(` Tags: ${result.node.tags.join(", ")}`);
1068
+ }
1069
+ if (result.links.length > 0) {
1070
+ console.log(` Auto-linked to ${result.links.length} existing memories`);
1071
+ }
1072
+ }
1073
+ break;
1074
+ }
1075
+ case "retrieve": {
1076
+ const id = args[1];
1077
+ if (!id) {
1078
+ console.error("Error: ID is required.\n Usage: memos retrieve <id>");
1079
+ process.exit(1);
1080
+ }
1081
+ const node = await memos.retrieve(id);
1082
+ if (!node) {
1083
+ console.error(`Memory not found: ${id}`);
1084
+ process.exit(1);
1085
+ }
1086
+ if (jsonFlag) {
1087
+ console.log(JSON.stringify(node, null, 2));
1088
+ }
1089
+ else {
1090
+ console.log(`[${node.type}] ${node.content}`);
1091
+ console.log(` ID: ${node.id}`);
1092
+ console.log(` Importance: ${node.importance}`);
1093
+ console.log(` Access count: ${node.accessCount}`);
1094
+ console.log(` Created: ${new Date(node.createdAt).toISOString()}`);
1095
+ if (node.tags.length > 0) {
1096
+ console.log(` Tags: ${node.tags.join(", ")}`);
1097
+ }
1098
+ if (node.expiresAt) {
1099
+ console.log(` Expires: ${new Date(node.expiresAt * 1000).toISOString()}`);
1100
+ }
1101
+ }
1102
+ break;
1103
+ }
1104
+ case "search": {
1105
+ const query = args[1];
1106
+ if (!query) {
1107
+ console.error("Error: query is required.\n Usage: memos search <query>");
1108
+ process.exit(1);
1109
+ }
1110
+ const limitIdx = args.indexOf("--limit");
1111
+ const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : 10;
1112
+ const poolIdx = args.indexOf("--pool");
1113
+ const poolArg = poolIdx !== -1 ? args[poolIdx + 1] : undefined;
1114
+ const pool = poolArg
1115
+ ? poolArg.split(",").map((p) => p.trim())
1116
+ : undefined;
1117
+ const scopeIdx = args.indexOf("--scope");
1118
+ const scopeArg = scopeIdx !== -1 ? args[scopeIdx + 1] : undefined;
1119
+ const scope = scopeArg ? parseScopeArg(scopeArg) : undefined;
1120
+ // Multi-scope read: comma-separated raw namespaces, e.g.
1121
+ // --namespaces project:alpha,project:beta (always unions the
1122
+ // shared `default` namespace, like --scope does).
1123
+ const namespacesIdx = args.indexOf("--namespaces");
1124
+ const namespacesArg = namespacesIdx !== -1 ? args[namespacesIdx + 1] : undefined;
1125
+ const namespaces = namespacesArg
1126
+ ? namespacesArg
1127
+ .split(",")
1128
+ .map((n) => n.trim())
1129
+ .filter((n) => n.length > 0)
1130
+ : undefined;
1131
+ const harnessIdx = args.indexOf("--harness");
1132
+ const harnessArg = harnessIdx !== -1 ? args[harnessIdx + 1] : undefined;
1133
+ const tagIdx = args.indexOf("--tag");
1134
+ let searchTags;
1135
+ if (tagIdx !== -1) {
1136
+ searchTags = [];
1137
+ for (let i = tagIdx + 1; i < args.length; i++) {
1138
+ if (args[i].startsWith("--"))
1139
+ break;
1140
+ searchTags.push(args[i]);
1141
+ }
1142
+ if (searchTags.length === 0)
1143
+ searchTags = undefined;
1144
+ }
1145
+ const results = await memos.search({
1146
+ query,
1147
+ limit,
1148
+ tags: searchTags,
1149
+ ...(pool ? { pool } : {}),
1150
+ ...(scope && Object.keys(scope).length > 0 ? { scope } : {}),
1151
+ ...(namespaces && namespaces.length > 0 ? { namespaces } : {}),
1152
+ ...(harnessArg ? { harness: harnessArg } : {}),
1153
+ });
1154
+ if (jsonFlag) {
1155
+ console.log(JSON.stringify(results, null, 2));
1156
+ }
1157
+ else {
1158
+ if (results.length === 0) {
1159
+ console.log("No memories found.");
1160
+ }
1161
+ else {
1162
+ console.log(`Found ${results.length} memories:\n`);
1163
+ for (const r of results) {
1164
+ console.log(` [${r.node.type}] ${r.node.content}`);
1165
+ console.log(` ID: ${r.node.id} Score: ${r.score.toFixed(3)}`);
1166
+ if (r.node.tags.length > 0) {
1167
+ console.log(` Tags: ${r.node.tags.join(", ")}`);
1168
+ }
1169
+ }
1170
+ }
1171
+ }
1172
+ break;
1173
+ }
1174
+ case "forget": {
1175
+ const id = args[1];
1176
+ if (!id) {
1177
+ console.error("Error: ID is required.\n Usage: memos forget <id>");
1178
+ process.exit(1);
1179
+ }
1180
+ const deleted = await memos.forget(id);
1181
+ if (jsonFlag) {
1182
+ console.log(JSON.stringify({ deleted, id }));
1183
+ }
1184
+ else {
1185
+ console.log(deleted ? `Forgot memory: ${id}` : `Memory not found: ${id}`);
1186
+ }
1187
+ break;
1188
+ }
1189
+ case "summarize": {
1190
+ const summary = await memos.summarize();
1191
+ if (jsonFlag) {
1192
+ console.log(JSON.stringify({ summary }));
1193
+ }
1194
+ else {
1195
+ console.log(summary);
1196
+ }
1197
+ break;
1198
+ }
1199
+ case "graph": {
1200
+ const graph = await memos.getGraph();
1201
+ if (args.includes("--mermaid")) {
1202
+ console.log(graphToMermaid(graph));
1203
+ }
1204
+ else if (jsonFlag) {
1205
+ console.log(JSON.stringify(graph, null, 2));
1206
+ }
1207
+ else {
1208
+ console.log(`Memory Graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges\n`);
1209
+ for (const node of graph.nodes) {
1210
+ console.log(` [${node.type}] ${node.id.slice(0, 8)} — ${node.content.slice(0, 60)}`);
1211
+ }
1212
+ if (graph.edges.length > 0) {
1213
+ console.log("\nEdges:");
1214
+ for (const edge of graph.edges) {
1215
+ console.log(` ${edge.sourceId.slice(0, 8)} --[${edge.relation}]--> ${edge.targetId.slice(0, 8)}`);
1216
+ }
1217
+ }
1218
+ }
1219
+ break;
1220
+ }
1221
+ case "browse": {
1222
+ const { runBrowse } = await import("./cli-browse.js");
1223
+ await runBrowse(memos);
1224
+ break;
1225
+ }
1226
+ case "image": {
1227
+ // Vision (image memory). The subcommand reports plainly how to
1228
+ // turn it on rather than throwing a stack trace at an operator
1229
+ // who has not configured it yet.
1230
+ const sub = args[1];
1231
+ const target = args[2];
1232
+ if (!sub || !["add", "list", "search", "show", "purge"].includes(sub)) {
1233
+ console.error("Error: image subcommand is required.\n" +
1234
+ " Usage: memos image add <path|url> [--caption <text>] [--kind <kind>] [--tags a,b]\n" +
1235
+ " memos image list [--limit <n>]\n" +
1236
+ " memos image search <query> [--limit <n>]\n" +
1237
+ " memos image show <ref|id>\n" +
1238
+ " memos image purge");
1239
+ process.exit(1);
1240
+ }
1241
+ // Fail with instructions, not a stack trace, when vision is off.
1242
+ // Every `image` subcommand needs it (even `list` — it reads
1243
+ // image-tagged nodes, which is harmless but meaningless without
1244
+ // the feature, and the message is more useful than an empty list).
1245
+ try {
1246
+ // A cheap probe: listImages throws only when vision is off.
1247
+ if (sub !== "add")
1248
+ await memos.listImages({ limit: 1 });
1249
+ else
1250
+ await memos.addImageMemory(Buffer.alloc(0), { caption: "probe" });
1251
+ }
1252
+ catch (err) {
1253
+ const message = err instanceof Error ? err.message : String(err);
1254
+ if (/disabled/i.test(message)) {
1255
+ console.error("Error: image memory is disabled.\n" +
1256
+ " Enable it with --vision (caption search) or --vision-clip\n" +
1257
+ " (also matches image pixels with CLIP), or set it in\n" +
1258
+ ' .memos.json: { "vision": { "enabled": true } }');
1259
+ process.exit(1);
1260
+ }
1261
+ // Any other error from the probe is not a config problem —
1262
+ // fall through and let the real subcommand run.
1263
+ }
1264
+ if (sub === "add") {
1265
+ if (!target) {
1266
+ console.error("Error: image path or URL is required.\n Usage: memos image add <path|url> [--caption <text>]");
1267
+ process.exit(1);
1268
+ }
1269
+ const captionIdx = args.indexOf("--caption");
1270
+ const kindIdx = args.indexOf("--kind");
1271
+ const imageTagsIdx = args.indexOf("--tags");
1272
+ const nsIdx = args.indexOf("--namespace");
1273
+ const result = await memos.addImageMemory(target, {
1274
+ ...(captionIdx !== -1 && args[captionIdx + 1]
1275
+ ? { caption: args[captionIdx + 1] }
1276
+ : {}),
1277
+ ...(kindIdx !== -1 && args[kindIdx + 1]
1278
+ ? { kind: args[kindIdx + 1] }
1279
+ : {}),
1280
+ ...(imageTagsIdx !== -1 && args[imageTagsIdx + 1]
1281
+ ? { tags: args[imageTagsIdx + 1].split(",").map((t) => t.trim()) }
1282
+ : {}),
1283
+ ...(nsIdx !== -1 && args[nsIdx + 1]
1284
+ ? { namespace: args[nsIdx + 1] }
1285
+ : {}),
1286
+ });
1287
+ if (jsonFlag) {
1288
+ console.log(JSON.stringify(result, null, 2));
1289
+ }
1290
+ else {
1291
+ const verb = result.deduped ? "already stored" : "stored";
1292
+ console.log(`Image ${verb}: ${result.image.ref}`);
1293
+ console.log(` Node: ${result.node.id}`);
1294
+ console.log(` Kind: ${result.image.kind} ${result.image.width}x${result.image.height}`);
1295
+ console.log(` Bytes: ${result.image.bytes} (${result.image.mime})`);
1296
+ if (result.clipStatus !== "skipped") {
1297
+ console.log(` CLIP: ${result.clipStatus}`);
1298
+ }
1299
+ }
1300
+ break;
1301
+ }
1302
+ if (sub === "list") {
1303
+ const listLimitIdx = args.indexOf("--limit");
1304
+ const listNsIdx = args.indexOf("--namespace");
1305
+ const images = await memos.listImages({
1306
+ limit: listLimitIdx !== -1 ? parseInt(args[listLimitIdx + 1], 10) : 50,
1307
+ ...(listNsIdx !== -1 && args[listNsIdx + 1]
1308
+ ? { namespace: args[listNsIdx + 1] }
1309
+ : {}),
1310
+ });
1311
+ if (jsonFlag) {
1312
+ console.log(JSON.stringify(images, null, 2));
1313
+ }
1314
+ else if (images.length === 0) {
1315
+ console.log("No images stored.");
1316
+ }
1317
+ else {
1318
+ console.log(`${images.length} image(s):\n`);
1319
+ for (const hit of images) {
1320
+ console.log(` ${hit.image.ref} [${hit.image.kind}] ${hit.image.width}x${hit.image.height}`);
1321
+ console.log(` ${hit.image.caption}`);
1322
+ console.log(` Node: ${hit.node.id}`);
1323
+ }
1324
+ }
1325
+ break;
1326
+ }
1327
+ if (sub === "search") {
1328
+ if (!target) {
1329
+ console.error("Error: query is required.\n Usage: memos image search <query> [--limit <n>]");
1330
+ process.exit(1);
1331
+ }
1332
+ const searchLimitIdx = args.indexOf("--limit");
1333
+ const searchNsIdx = args.indexOf("--namespace");
1334
+ const hits = await memos.searchImages(target, {
1335
+ limit: searchLimitIdx !== -1
1336
+ ? parseInt(args[searchLimitIdx + 1], 10)
1337
+ : 10,
1338
+ ...(searchNsIdx !== -1 && args[searchNsIdx + 1]
1339
+ ? { namespace: args[searchNsIdx + 1] }
1340
+ : {}),
1341
+ });
1342
+ if (jsonFlag) {
1343
+ console.log(JSON.stringify(hits, null, 2));
1344
+ }
1345
+ else if (hits.length === 0) {
1346
+ console.log("No matching images found.");
1347
+ }
1348
+ else {
1349
+ console.log(`Found ${hits.length} image(s):\n`);
1350
+ for (const hit of hits) {
1351
+ console.log(` ${hit.image.ref} [${hit.image.kind}] score ${hit.score.toFixed(3)}`);
1352
+ console.log(` ${hit.image.caption}`);
1353
+ console.log(` Node: ${hit.node.id}`);
1354
+ }
1355
+ }
1356
+ break;
1357
+ }
1358
+ if (sub === "show") {
1359
+ if (!target) {
1360
+ console.error("Error: image ref or node id is required.\n Usage: memos image show <ref|id>");
1361
+ process.exit(1);
1362
+ }
1363
+ const resolved = await memos.imageMemory(target);
1364
+ if (!resolved) {
1365
+ console.error(`Image not found: ${target}`);
1366
+ process.exit(1);
1367
+ }
1368
+ // Write the bytes somewhere the user can open them: stdout
1369
+ // would corrupt the terminal for anything non-textual.
1370
+ const outIdx = args.indexOf("--out");
1371
+ const outPath = outIdx !== -1 ? args[outIdx + 1] : undefined;
1372
+ if (outPath) {
1373
+ const { writeFile } = await import("node:fs/promises");
1374
+ await writeFile(outPath, resolved.buffer);
1375
+ console.log(`Wrote ${resolved.buffer.length} bytes to ${outPath}`);
1376
+ }
1377
+ else if (jsonFlag) {
1378
+ console.log(JSON.stringify({
1379
+ ref: resolved.ref,
1380
+ mime: resolved.mime,
1381
+ bytes: resolved.buffer.length,
1382
+ }, null, 2));
1383
+ }
1384
+ else {
1385
+ console.log(`${resolved.ref} (${resolved.mime}, ${resolved.buffer.length} bytes)`);
1386
+ console.log(" Use --out <path> to write the bytes to a file.");
1387
+ }
1388
+ break;
1389
+ }
1390
+ // sub === "purge"
1391
+ const purged = await memos.purgeOrphanImages();
1392
+ if (jsonFlag) {
1393
+ console.log(JSON.stringify({ purged }, null, 2));
1394
+ }
1395
+ else {
1396
+ console.log(purged === 0
1397
+ ? "No orphaned image blobs."
1398
+ : `Purged ${purged} orphaned image blob(s).`);
1399
+ }
1400
+ break;
1401
+ }
1402
+ case "import-external": {
1403
+ const file = args[1];
1404
+ if (!file) {
1405
+ console.error("Error: export file is required.\n Usage: memos import-external <file> [--source auto|chatgpt|claude|generic] [--dry-run]");
1406
+ process.exit(1);
1407
+ }
1408
+ const sourceIdx = args.indexOf("--source");
1409
+ const importSource = sourceIdx !== -1
1410
+ ? args[sourceIdx + 1]
1411
+ : "auto";
1412
+ const maxItemsIdx = args.indexOf("--max-items");
1413
+ const maxItems = maxItemsIdx !== -1 ? parseInt(args[maxItemsIdx + 1], 10) : undefined;
1414
+ const nsIdx = args.indexOf("--namespace");
1415
+ const namespace = nsIdx !== -1 ? args[nsIdx + 1] : undefined;
1416
+ const result = await memos.importExternal({
1417
+ file,
1418
+ ...(importSource ? { source: importSource } : {}),
1419
+ ...(maxItems !== undefined && !Number.isNaN(maxItems)
1420
+ ? { maxItems }
1421
+ : {}),
1422
+ ...(namespace ? { namespace } : {}),
1423
+ dryRun: args.includes("--dry-run"),
1424
+ });
1425
+ if (jsonFlag) {
1426
+ console.log(JSON.stringify(result, null, 2));
1427
+ }
1428
+ else {
1429
+ console.log(`Import from ${result.detected} export ${result.dryRun ? "(dry run)" : "complete"}: ${result.imported} imported, ${result.skipped} skipped of ${result.total} item(s)`);
1430
+ }
1431
+ break;
1432
+ }
1433
+ case "import": {
1434
+ const fromIdx = args.indexOf("--from");
1435
+ const from = fromIdx !== -1 ? args[fromIdx + 1] : undefined;
1436
+ if (from !== "chatgpt" && from !== "claude" && from !== "slack") {
1437
+ console.error("Error: --from is required.\\n Usage: memos import --from chatgpt|claude|slack (--file <path> | --dir <path>) [--synthesize] [--dry-run] [--max-items <n>] [--namespace <ns>] [--tags a,b]");
1438
+ process.exit(1);
1439
+ }
1440
+ const fileIdx = args.indexOf("--file");
1441
+ const file = fileIdx !== -1 ? args[fileIdx + 1] : undefined;
1442
+ const dirIdx = args.indexOf("--dir");
1443
+ const dir = dirIdx !== -1 ? args[dirIdx + 1] : undefined;
1444
+ if (!file && !dir) {
1445
+ console.error("Error: --file or --dir is required.\\n Usage: memos import --from chatgpt|claude|slack (--file <path> | --dir <path>)");
1446
+ process.exit(1);
1447
+ }
1448
+ if (from === "slack" && !dir) {
1449
+ console.error("Error: Slack imports require --dir (the export folder with channels.json).");
1450
+ process.exit(1);
1451
+ }
1452
+ const maxItemsIdx = args.indexOf("--max-items");
1453
+ const maxItems = maxItemsIdx !== -1 ? parseInt(args[maxItemsIdx + 1], 10) : undefined;
1454
+ const nsIdx = args.indexOf("--namespace");
1455
+ const namespace = nsIdx !== -1 ? args[nsIdx + 1] : undefined;
1456
+ const tagsIdx = args.indexOf("--tags");
1457
+ const importTags = tagsIdx !== -1
1458
+ ? args[tagsIdx + 1]
1459
+ .split(",")
1460
+ .map((t) => t.trim())
1461
+ .filter((t) => t.length > 0)
1462
+ : undefined;
1463
+ const result = await memos.importExternal({
1464
+ source: from,
1465
+ ...(file ? { file } : {}),
1466
+ ...(dir ? { dir } : {}),
1467
+ ...(maxItems !== undefined && !Number.isNaN(maxItems)
1468
+ ? { maxItems }
1469
+ : {}),
1470
+ ...(namespace ? { namespace } : {}),
1471
+ ...(importTags ? { tags: importTags } : {}),
1472
+ dryRun: args.includes("--dry-run"),
1473
+ synthesize: args.includes("--synthesize"),
1474
+ });
1475
+ if (jsonFlag) {
1476
+ console.log(JSON.stringify(result, null, 2));
1477
+ }
1478
+ else {
1479
+ console.log(`Import from ${result.detected} export${result.dryRun ? " (dry run)" : ""}: ${result.imported} imported, ${result.skipped} skipped of ${result.total} item(s)`);
1480
+ const s = result.synthesized;
1481
+ if (s.decisions + s.preferences + s.milestones > 0) {
1482
+ console.log(`Synthesized: ${s.decisions} decision(s), ${s.preferences} preference(s), ${s.milestones} milestone(s)`);
1483
+ }
1484
+ }
1485
+ break;
1486
+ }
1487
+ case "learn": {
1488
+ const lesson = args[1];
1489
+ if (!lesson) {
1490
+ console.error('Error: lesson text is required.\\n Usage: memos learn "<lesson>" [--context <ctx>] [--tags a,b] [--namespace <ns>]');
1491
+ process.exit(1);
1492
+ }
1493
+ const contextIdx = args.indexOf("--context");
1494
+ const context = contextIdx !== -1 ? args[contextIdx + 1] : undefined;
1495
+ const tagsIdx = args.indexOf("--tags");
1496
+ const tags = tagsIdx !== -1
1497
+ ? args[tagsIdx + 1]
1498
+ .split(",")
1499
+ .map((t) => t.trim())
1500
+ .filter((t) => t.length > 0)
1501
+ : undefined;
1502
+ const nsIdx = args.indexOf("--namespace");
1503
+ const namespace = nsIdx !== -1 ? args[nsIdx + 1] : undefined;
1504
+ const saved = await memos.memorizeProcedural(lesson, {
1505
+ ...(context ? { context } : {}),
1506
+ ...(tags ? { tags } : {}),
1507
+ ...(namespace ? { namespace } : {}),
1508
+ });
1509
+ if (jsonFlag) {
1510
+ console.log(JSON.stringify(saved, null, 2));
1511
+ }
1512
+ else {
1513
+ console.log(`Learned [${saved.id.slice(0, 8)}] (score ${saved.score.toFixed(2)}): ${saved.lesson}`);
1514
+ }
1515
+ break;
1516
+ }
1517
+ case "lesson": {
1518
+ const id = args[1];
1519
+ const outcomeIdx = args.indexOf("--outcome");
1520
+ const outcome = outcomeIdx !== -1 ? args[outcomeIdx + 1] : undefined;
1521
+ if (!id || (outcome !== "success" && outcome !== "failure")) {
1522
+ console.error("Error: lesson ID and --outcome are required.\\n Usage: memos lesson <id> --outcome success|failure");
1523
+ process.exit(1);
1524
+ }
1525
+ try {
1526
+ const updated = await memos.recordLessonOutcome(id, outcome);
1527
+ if (jsonFlag) {
1528
+ console.log(JSON.stringify(updated, null, 2));
1529
+ }
1530
+ else {
1531
+ console.log(`Lesson [${updated.id.slice(0, 8)}] ${outcome}: score ${updated.score.toFixed(2)} (${updated.successCount} success, ${updated.failureCount} failure, ${updated.useCount} uses)`);
1532
+ }
1533
+ }
1534
+ catch (err) {
1535
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
1536
+ process.exit(1);
1537
+ }
1538
+ break;
1539
+ }
1540
+ case "lessons": {
1541
+ const nsIdx = args.indexOf("--namespace");
1542
+ const namespace = nsIdx !== -1 ? args[nsIdx + 1] : undefined;
1543
+ const limitIdx = args.indexOf("--limit");
1544
+ const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : 20;
1545
+ const lessons = await memos.listProceduralLessons({
1546
+ ...(namespace ? { namespace } : {}),
1547
+ });
1548
+ const shown = Number.isNaN(limit) ? lessons : lessons.slice(0, limit);
1549
+ if (jsonFlag) {
1550
+ console.log(JSON.stringify(shown, null, 2));
1551
+ }
1552
+ else if (shown.length === 0) {
1553
+ console.log('No procedural lessons yet. Teach one: memos learn "..."');
1554
+ }
1555
+ else {
1556
+ console.log(`Procedural lessons (${shown.length}):\\n`);
1557
+ for (const l of shown) {
1558
+ console.log(` [${l.id.slice(0, 8)}] score ${l.score.toFixed(2)} · ${l.useCount} uses (${l.successCount}✓/${l.failureCount}✗)`);
1559
+ console.log(` ${l.lesson}`);
1560
+ if (l.context)
1561
+ console.log(` when: ${l.context}`);
1562
+ }
1563
+ }
1564
+ break;
1565
+ }
1566
+ case "cite": {
1567
+ const id = args[1];
1568
+ if (!id) {
1569
+ console.error("Error: citation ID is required.\\n Usage: memos cite <id> (accepts [mem:a3f9], a3f9, or a full ID)");
1570
+ process.exit(1);
1571
+ }
1572
+ const resolution = await memos.resolveCitation(id);
1573
+ console.log(memos.formatCitation(resolution, jsonFlag));
1574
+ if (resolution.status === "not_found")
1575
+ process.exit(1);
1576
+ break;
1577
+ }
1578
+ case "quarantine": {
1579
+ const { formatQuarantineList, releaseQuarantined } = await import("./quarantine-commands.js");
1580
+ const sub = args[1];
1581
+ if (sub === "list") {
1582
+ const limitIdx = args.indexOf("--limit");
1583
+ const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : undefined;
1584
+ const nodes = await memos.listQuarantined(limit !== undefined && !Number.isNaN(limit) ? { limit } : {});
1585
+ console.log(formatQuarantineList(nodes, jsonFlag));
1586
+ break;
1587
+ }
1588
+ if (sub === "release") {
1589
+ const id = args[2];
1590
+ if (!id) {
1591
+ console.error("Error: memory ID is required.\n Usage: memos quarantine release <id>");
1592
+ process.exit(1);
1593
+ }
1594
+ try {
1595
+ console.log(await releaseQuarantined(memos, id, jsonFlag));
1596
+ }
1597
+ catch (err) {
1598
+ console.error(`Error: ${err.message}`);
1599
+ process.exit(1);
1600
+ }
1601
+ break;
1602
+ }
1603
+ console.error("Error: subcommand is required.\n Usage: memos quarantine <list|release> (list [--limit <n>] [--json], release <id> [--json])");
1604
+ process.exit(1);
1605
+ }
1606
+ case "harness": {
1607
+ const sub = args[1];
1608
+ if (sub === "list") {
1609
+ const counts = await memos.getHarnessCounts();
1610
+ if (jsonFlag) {
1611
+ console.log(JSON.stringify(counts, null, 2));
1612
+ }
1613
+ else if (counts.length === 0) {
1614
+ console.log("No memories stored.");
1615
+ }
1616
+ else {
1617
+ const total = counts.reduce((n, c) => n + c.count, 0);
1618
+ console.log(`Memories by harness (${total} total):\n`);
1619
+ for (const c of counts) {
1620
+ console.log(` ${c.harness.padEnd(16)} ${c.count}`);
1621
+ }
1622
+ }
1623
+ break;
1624
+ }
1625
+ if (sub === "merge") {
1626
+ const fromIdx = args.indexOf("--from");
1627
+ const from = fromIdx !== -1 ? args[fromIdx + 1] : undefined;
1628
+ if (!from) {
1629
+ console.error("Error: --from <db-path> is required.\n Usage: memos harness merge --from <db-path> [--dry-run] [--json]");
1630
+ process.exit(1);
1631
+ }
1632
+ const dryRun = args.includes("--dry-run");
1633
+ const result = await memos.mergeHarnessDb(from, { dryRun });
1634
+ if (jsonFlag) {
1635
+ console.log(JSON.stringify(result, null, 2));
1636
+ }
1637
+ else {
1638
+ console.log(`Merge ${dryRun ? "(dry run) " : ""}from ${result.from}:`);
1639
+ console.log(` source: ${result.sourceNodes} nodes, ${result.sourceEdges} edges`);
1640
+ console.log(` nodes: ${result.nodesImported} imported (${result.nodesRemapped} id remapped)`);
1641
+ console.log(` edges: ${result.edgesImported} imported (${result.edgesSkipped} skipped)`);
1642
+ for (const n of result.nodes) {
1643
+ if (n.remapped) {
1644
+ console.log(` remapped ${n.sourceId} → ${n.targetId}`);
1645
+ }
1646
+ }
1647
+ if (dryRun) {
1648
+ console.log(" (dry run — nothing was written; run without --dry-run to merge)");
1649
+ }
1650
+ }
1651
+ break;
1652
+ }
1653
+ console.error("Error: subcommand is required.\n Usage: memos harness <list|merge> (list [--json], merge --from <db-path> [--dry-run] [--json])");
1654
+ process.exit(1);
1655
+ }
1656
+ case "scope": {
1657
+ const sub = args[1];
1658
+ if (sub === "list") {
1659
+ const spaces = await memos.listNamespaceCounts();
1660
+ if (jsonFlag) {
1661
+ console.log(JSON.stringify(spaces, null, 2));
1662
+ }
1663
+ else if (spaces.length === 0) {
1664
+ console.log("No memories stored.");
1665
+ }
1666
+ else {
1667
+ const total = spaces.reduce((n, s) => n + s.count, 0);
1668
+ console.log(`Memory scopes (${total} total):\n`);
1669
+ for (const s of spaces) {
1670
+ console.log(` ${s.namespace.padEnd(28)} ${s.count}`);
1671
+ }
1672
+ console.log("\nScoped reads union the shared `default` scope with the requested scope(s).");
1673
+ }
1674
+ break;
1675
+ }
1676
+ console.error("Error: subcommand is required.\n Usage: memos scope list [--json]\n" +
1677
+ " Free-form scope names follow the convention project:<name> / harness:<name>.");
1678
+ process.exit(1);
1679
+ }
1680
+ case "stats": {
1681
+ const usage = memos.usageStats();
1682
+ if (jsonFlag) {
1683
+ console.log(JSON.stringify(usage, null, 2));
1684
+ }
1685
+ else {
1686
+ console.log("Context-pack token telemetry (this process):");
1687
+ console.log(` packs built: ${usage.packsBuilt}`);
1688
+ console.log(` tokens injected: ${usage.packTokens}`);
1689
+ console.log(` naive baseline: ${usage.naiveBaselineTokens} (raw node JSON, same candidates)`);
1690
+ console.log(` saved: ${usage.savedTokens} tok (${usage.savedPct}%)`);
1691
+ }
1692
+ break;
1693
+ }
1694
+ case "compact": {
1695
+ const nsIdx = args.indexOf("--namespace");
1696
+ const namespace = nsIdx !== -1 ? args[nsIdx + 1] : undefined;
1697
+ const limitIdx = args.indexOf("--limit");
1698
+ const limit = limitIdx !== -1 ? parseInt(args[limitIdx + 1], 10) : undefined;
1699
+ const dryRun = args.includes("--dry-run");
1700
+ const showStats = args.includes("--stats") || dryRun;
1701
+ const result = await memos.compact({
1702
+ ...(namespace ? { namespace } : {}),
1703
+ ...(limit !== undefined && !Number.isNaN(limit) ? { limit } : {}),
1704
+ stats: showStats,
1705
+ dryRun,
1706
+ });
1707
+ if (jsonFlag) {
1708
+ console.log(JSON.stringify(result, null, 2));
1709
+ }
1710
+ else {
1711
+ if (showStats) {
1712
+ console.log(`Fidelity levels (${result.stats?.[0]?.count ?? 0} memories${namespace ? `, namespace ${namespace}` : ""}):`);
1713
+ for (const s of result.stats ?? []) {
1714
+ const saved = (s.savingsVsVerbatim * 100).toFixed(1);
1715
+ console.log(` ${s.level} ${s.label.padEnd(18)} avg ${s.avgTokens.toFixed(1).padStart(7)} tok -${saved}% vs verbatim`);
1716
+ }
1717
+ }
1718
+ if (!args.includes("--stats")) {
1719
+ console.log(`compact: scanned ${result.scanned}, backfilled ${result.backfilled}, skipped ${result.skipped}${dryRun ? " (dry run)" : ""}`);
1720
+ }
1721
+ }
1722
+ break;
1723
+ }
1724
+ case "consolidate": {
1725
+ const dryRun = args.includes("--dry-run");
1726
+ const summarize = !args.includes("--no-summarize");
1727
+ const decay = !args.includes("--no-decay");
1728
+ const nsIdx = args.indexOf("--namespace");
1729
+ const namespace = nsIdx !== -1 ? args[nsIdx + 1] : undefined;
1730
+ const halfLifeIdx = args.indexOf("--decay-half-life");
1731
+ const decayHalfLifeDays = halfLifeIdx !== -1 ? Number(args[halfLifeIdx + 1]) : undefined;
1732
+ const minRetentionIdx = args.indexOf("--min-retention");
1733
+ const minRetentionScore = minRetentionIdx !== -1
1734
+ ? Number(args[minRetentionIdx + 1])
1735
+ : undefined;
1736
+ const olderThanIdx = args.indexOf("--older-than");
1737
+ const olderThanDays = olderThanIdx !== -1 ? Number(args[olderThanIdx + 1]) : undefined;
1738
+ // Watch mode: self-maintaining memory. Runs the full consolidation
1739
+ // immediately, then every interval until interrupted. Each cycle
1740
+ // prints a one-line digest.
1741
+ if (args.includes("--watch")) {
1742
+ const intervalIdx = args.indexOf("--interval-min");
1743
+ const intervalMin = Math.max(1, intervalIdx !== -1 ? parseInt(args[intervalIdx + 1], 10) || 30 : 30);
1744
+ const consolidateOpts = {
1745
+ ...(namespace ? { namespace } : {}),
1746
+ dryRun,
1747
+ summarize,
1748
+ decay,
1749
+ ...(decayHalfLifeDays !== undefined &&
1750
+ !Number.isNaN(decayHalfLifeDays)
1751
+ ? { decayHalfLifeDays }
1752
+ : {}),
1753
+ ...(minRetentionScore !== undefined &&
1754
+ !Number.isNaN(minRetentionScore)
1755
+ ? { minRetentionScore }
1756
+ : {}),
1757
+ ...(olderThanDays !== undefined && !Number.isNaN(olderThanDays)
1758
+ ? { olderThanDays }
1759
+ : {}),
1760
+ };
1761
+ console.log(`consolidation watch: every ${intervalMin} min — ctrl+c to stop`);
1762
+ for (;;) {
1763
+ const started = Date.now();
1764
+ try {
1765
+ const result = await memos.consolidate(consolidateOpts);
1766
+ const usage = memos.usageStats();
1767
+ console.log(`[${new Date().toISOString()}] merged ${result.merges.length} · archived ${result.moves.length} · decayed ${result.decayed.length} · notes ${result.clusters.length} · ${usage.packTokens} tok injected (${usage.savedPct}% saved) — ${Date.now() - started} ms`);
1768
+ }
1769
+ catch (error) {
1770
+ console.error("consolidation cycle failed:", error instanceof Error ? error.message : String(error));
1771
+ }
1772
+ await new Promise((resolve) => setTimeout(resolve, intervalMin * 60_000));
1773
+ }
1774
+ }
1775
+ // Consolidation defaults come from .memos.json; an explicit CLI
1776
+ // flag still wins (each spread below is guarded by `=== undefined`).
1777
+ const projectConsolidation = projectConfig?.consolidation;
1778
+ const result = await memos.consolidate({
1779
+ ...(namespace ? { namespace } : {}),
1780
+ dryRun,
1781
+ summarize,
1782
+ decay,
1783
+ ...(projectConsolidation?.minRetentionScore !== undefined &&
1784
+ minRetentionScore === undefined
1785
+ ? { minRetentionScore: projectConsolidation.minRetentionScore }
1786
+ : {}),
1787
+ ...(projectConsolidation?.decayHalfLifeDays !== undefined &&
1788
+ decayHalfLifeDays === undefined
1789
+ ? { decayHalfLifeDays: projectConsolidation.decayHalfLifeDays }
1790
+ : {}),
1791
+ ...(projectConsolidation?.olderThanDays !== undefined &&
1792
+ olderThanDays === undefined
1793
+ ? { olderThanDays: projectConsolidation.olderThanDays }
1794
+ : {}),
1795
+ ...(decayHalfLifeDays !== undefined &&
1796
+ !Number.isNaN(decayHalfLifeDays)
1797
+ ? { decayHalfLifeDays }
1798
+ : {}),
1799
+ ...(minRetentionScore !== undefined &&
1800
+ !Number.isNaN(minRetentionScore)
1801
+ ? { minRetentionScore }
1802
+ : {}),
1803
+ ...(olderThanDays !== undefined && !Number.isNaN(olderThanDays)
1804
+ ? { olderThanDays }
1805
+ : {}),
1806
+ });
1807
+ if (jsonFlag) {
1808
+ console.log(JSON.stringify(result, null, 2));
1809
+ }
1810
+ else {
1811
+ console.log(`Consolidation ${dryRun ? "(dry run — nothing changed)" : "complete"} in ${result.durationMs} ms`);
1812
+ console.log(` merged: ${result.merges.length} duplicate cluster(s)`);
1813
+ console.log(` archived: ${result.moves.length} memory(ies)`);
1814
+ console.log(` decayed: ${result.decayed.length} superseded (kept as history — queryable via searchTemporal)`);
1815
+ console.log(` notes: ${result.clusters.length} cluster summary(ies)`);
1816
+ for (const cluster of result.clusters) {
1817
+ if (cluster.summaryId) {
1818
+ console.log(` - ${cluster.summaryId.slice(0, 8)}: ${cluster.summary.slice(0, 72)}`);
1819
+ }
1820
+ }
1821
+ }
1822
+ break;
1823
+ }
1824
+ case "digest": {
1825
+ const result = await memos.consolidate({
1826
+ summarize: !args.includes("--no-summarize"),
1827
+ });
1828
+ const usage = memos.usageStats();
1829
+ if (jsonFlag) {
1830
+ console.log(JSON.stringify({ result, usage }, null, 2));
1831
+ }
1832
+ else {
1833
+ console.log("Memory digest:");
1834
+ console.log(` learned/merged: ${result.merges.length} duplicate cluster(s) merged`);
1835
+ console.log(` superseded: ${result.decayed.length} decayed`);
1836
+ console.log(` archived: ${result.moves.length} moved aside`);
1837
+ console.log(` new notes: ${result.clusters.length} cluster summary(ies)`);
1838
+ console.log(` context packs: ${usage.packsBuilt} built · ${usage.savedPct}% tokens saved vs raw JSON`);
1839
+ }
1840
+ break;
1841
+ }
1842
+ case "history": {
1843
+ const id = args[1];
1844
+ if (!id) {
1845
+ console.error("Error: memory ID is required.\n Usage: memos history <id>");
1846
+ process.exit(1);
1847
+ }
1848
+ const timeline = await memos.history(id);
1849
+ if (jsonFlag) {
1850
+ console.log(JSON.stringify(timeline, null, 2));
1851
+ break;
1852
+ }
1853
+ const node = timeline.node;
1854
+ const validity = node.validFrom || node.validTo
1855
+ ? `${node.validFrom ? new Date(node.validFrom).toISOString().slice(0, 10) : "…"} → ${node.validTo ? new Date(node.validTo).toISOString().slice(0, 10) : "present"}`
1856
+ : "always";
1857
+ console.log(`Memory ${node.id}`);
1858
+ console.log(` type: ${node.type} · trust: ${node.trustScore.toFixed(2)} · valid: ${validity}`);
1859
+ console.log(` ${node.content}`);
1860
+ if (timeline.supersedes.length > 0) {
1861
+ console.log(`\nSupersedes (older versions):`);
1862
+ for (const version of timeline.supersedes) {
1863
+ console.log(` ← [${version.node.createdAt ? new Date(version.node.createdAt).toISOString().slice(0, 10) : "?"}] ${version.node.id.slice(0, 8)} — ${version.node.content.slice(0, 60)}`);
1864
+ }
1865
+ }
1866
+ if (timeline.supersededBy.length > 0) {
1867
+ console.log(`\nSuperseded by (newer versions):`);
1868
+ for (const version of timeline.supersededBy) {
1869
+ console.log(` → [${new Date(version.node.createdAt).toISOString().slice(0, 10)}] ${version.node.id.slice(0, 8)} — ${version.node.content.slice(0, 60)}`);
1870
+ }
1871
+ }
1872
+ if (timeline.derivedNotes.length > 0) {
1873
+ console.log(`\nDerived notes: ${timeline.derivedNotes.length}`);
1874
+ for (const note of timeline.derivedNotes) {
1875
+ console.log(` ¶ ${note.id.slice(0, 8)} — ${note.content.slice(0, 60)}`);
1876
+ }
1877
+ }
1878
+ if (timeline.supersedes.length === 0 &&
1879
+ timeline.supersededBy.length === 0) {
1880
+ console.log("\nNo related versions found (this is the only version).");
1881
+ }
1882
+ break;
1883
+ }
1884
+ case "revert": {
1885
+ const parsed = parseRevertCliArgs(args.slice(1));
1886
+ if (parsed.error) {
1887
+ console.error(`Error: ${parsed.error}`);
1888
+ process.exit(1);
1889
+ }
1890
+ try {
1891
+ const result = await memos.revert(parsed.scope, {
1892
+ dryRun: parsed.dryRun,
1893
+ ...(parsed.reason ? { reason: parsed.reason } : {}),
1894
+ ...(parsed.actor ? { actor: parsed.actor } : {}),
1895
+ ...(parsed.namespace ? { namespace: parsed.namespace } : {}),
1896
+ });
1897
+ if (jsonFlag) {
1898
+ console.log(JSON.stringify(result, null, 2));
1899
+ }
1900
+ else if (result.dryRun) {
1901
+ console.log("Dry run — nothing was changed.");
1902
+ console.log(` Would revert: ${result.target.id.slice(0, 8)} — ${result.target.content.slice(0, 80)}`);
1903
+ console.log(` Would restore: ${result.predecessor.id.slice(0, 8)} — ${result.predecessor.content.slice(0, 80)}`);
1904
+ if (result.alternatives.length > 0) {
1905
+ console.log(" Other candidates (not chosen):");
1906
+ for (const alt of result.alternatives) {
1907
+ console.log(` ${alt.node.id.slice(0, 8)} — ${alt.node.content.slice(0, 60)}`);
1908
+ }
1909
+ }
1910
+ }
1911
+ else {
1912
+ console.log(`Reverted ${result.target.id.slice(0, 8)} — restored predecessor ${result.predecessor.id.slice(0, 8)}.`);
1913
+ console.log(` Now believed: ${result.predecessor.content.slice(0, 100)}`);
1914
+ console.log(` Audit event: ${result.auditId}`);
1915
+ }
1916
+ }
1917
+ catch (err) {
1918
+ console.error(`Error: ${err.message}`);
1919
+ process.exit(1);
1920
+ }
1921
+ break;
1922
+ }
1923
+ case "reminders": {
1924
+ const dueOnly = args.includes("--due");
1925
+ const nsIdx = args.indexOf("--namespace");
1926
+ const namespace = nsIdx !== -1 && args[nsIdx + 1] && !args[nsIdx + 1].startsWith("--")
1927
+ ? args[nsIdx + 1]
1928
+ : undefined;
1929
+ const reminders = await listReminders(memos, {
1930
+ now: new Date(),
1931
+ dueOnly,
1932
+ ...(namespace !== undefined ? { namespace } : {}),
1933
+ });
1934
+ if (jsonFlag) {
1935
+ console.log(JSON.stringify(reminders, null, 2));
1936
+ }
1937
+ else if (reminders.length === 0) {
1938
+ console.log(dueOnly ? "No reminders due." : "No scheduled reminders.");
1939
+ }
1940
+ else {
1941
+ for (const r of reminders) {
1942
+ const when = new Date(r.reminderAt).toLocaleString();
1943
+ console.log(`• ${when} — ${r.label}${r.due && !dueOnly ? " (due)" : ""}`);
1944
+ console.log(` ${r.content.slice(0, 100)}`);
1945
+ }
1946
+ }
1947
+ break;
1948
+ }
1949
+ case "remind": {
1950
+ // Strip global flags before parsing the "<text> at <when>" shape.
1951
+ const remindArgs = [];
1952
+ const remindRaw = args.slice(1);
1953
+ let remindNamespace;
1954
+ for (let i = 0; i < remindRaw.length; i++) {
1955
+ const a = remindRaw[i];
1956
+ if (a === "--json")
1957
+ continue;
1958
+ if (a === "--db" || a === "--key") {
1959
+ i++;
1960
+ continue;
1961
+ }
1962
+ if (a === "--namespace") {
1963
+ remindNamespace = remindRaw[i + 1];
1964
+ i++;
1965
+ continue;
1966
+ }
1967
+ remindArgs.push(a);
1968
+ }
1969
+ const input = remindArgs.join(" ");
1970
+ const split = splitRemindText(input);
1971
+ if (!split) {
1972
+ console.error('Error: could not find a temporal expression.\n Usage: memos remind "<text>" at <when>\n Example: memos remind "call mom" at tomorrow 9am');
1973
+ process.exit(1);
1974
+ }
1975
+ const parsed = parseTemporal(split.temporal, new Date());
1976
+ if (!parsed) {
1977
+ console.error(`Error: could not parse a time from "${split.temporal}".`);
1978
+ process.exit(1);
1979
+ }
1980
+ const at = parsed.at.toISOString();
1981
+ const result = await memos.store(split.text, {
1982
+ ...(remindNamespace !== undefined
1983
+ ? { namespace: remindNamespace }
1984
+ : {}),
1985
+ metadata: {
1986
+ event: {
1987
+ kind: "reminder",
1988
+ label: split.text,
1989
+ at,
1990
+ grain: parsed.grain,
1991
+ status: "scheduled",
1992
+ reminder_at: at,
1993
+ },
1994
+ },
1995
+ });
1996
+ if (jsonFlag) {
1997
+ console.log(JSON.stringify(result.node, null, 2));
1998
+ }
1999
+ else {
2000
+ console.log(`Reminder set: "${split.text}" at ${parsed.at.toLocaleString()}`);
2001
+ console.log(` Memory: ${result.node.id}`);
2002
+ }
2003
+ break;
2004
+ }
2005
+ case "link": {
2006
+ const sourceId = args[1];
2007
+ const targetId = args[2];
2008
+ if (!sourceId || !targetId) {
2009
+ console.error("Error: source and target IDs are required.\n Usage: memos link <source-id> <target-id>");
2010
+ process.exit(1);
2011
+ }
2012
+ const edge = await memos.link(sourceId, targetId);
2013
+ if (jsonFlag) {
2014
+ console.log(JSON.stringify(edge, null, 2));
2015
+ }
2016
+ else {
2017
+ console.log(`Linked: ${sourceId.slice(0, 8)} --[${edge.relation}]--> ${targetId.slice(0, 8)}`);
2018
+ }
2019
+ break;
2020
+ }
2021
+ case "count": {
2022
+ if (jsonFlag) {
2023
+ console.log(JSON.stringify({ count: memos.count }));
2024
+ }
2025
+ else {
2026
+ console.log(`${memos.count} memories stored.`);
2027
+ }
2028
+ break;
2029
+ }
2030
+ case "tag": {
2031
+ const id = args[1];
2032
+ const tags = nonFlagArgs(args, 2);
2033
+ if (!id || tags.length === 0) {
2034
+ console.error("Error: ID and at least one tag are required.\n Usage: memos tag <id> <tag1> [tag2...]");
2035
+ process.exit(1);
2036
+ }
2037
+ await memos.tag(id, tags);
2038
+ if (jsonFlag) {
2039
+ console.log(JSON.stringify({ id, tags, tagged: true }));
2040
+ }
2041
+ else {
2042
+ console.log(`Tagged ${id.slice(0, 8)} with: ${tags.join(", ")}`);
2043
+ }
2044
+ break;
2045
+ }
2046
+ case "untag": {
2047
+ const id = args[1];
2048
+ const tags = nonFlagArgs(args, 2);
2049
+ if (!id || tags.length === 0) {
2050
+ console.error("Error: ID and at least one tag are required.\n Usage: memos untag <id> <tag1> [tag2...]");
2051
+ process.exit(1);
2052
+ }
2053
+ await memos.untag(id, tags);
2054
+ if (jsonFlag) {
2055
+ console.log(JSON.stringify({ id, tags, untagged: true }));
2056
+ }
2057
+ else {
2058
+ console.log(`Removed tags from ${id.slice(0, 8)}: ${tags.join(", ")}`);
2059
+ }
2060
+ break;
2061
+ }
2062
+ case "list": {
2063
+ const tagIdx = args.indexOf("--tag");
2064
+ if (tagIdx === -1 || !args[tagIdx + 1]) {
2065
+ console.error("Error: --tag is required.\n Usage: memos list --tag <tag>");
2066
+ process.exit(1);
2067
+ }
2068
+ const tag = args[tagIdx + 1];
2069
+ const nodes = await memos.listByTag(tag);
2070
+ if (jsonFlag) {
2071
+ console.log(JSON.stringify(nodes, null, 2));
2072
+ }
2073
+ else {
2074
+ if (nodes.length === 0) {
2075
+ console.log(`No memories with tag: ${tag}`);
2076
+ }
2077
+ else {
2078
+ console.log(`Found ${nodes.length} memories with tag "${tag}":\n`);
2079
+ for (const node of nodes) {
2080
+ console.log(` [${node.type}] ${node.id.slice(0, 8)} — ${node.content.slice(0, 60)}`);
2081
+ }
2082
+ }
2083
+ }
2084
+ break;
2085
+ }
2086
+ case "export": {
2087
+ const formatIdx = args.indexOf("--format");
2088
+ const format = formatIdx !== -1 ? args[formatIdx + 1] : "json";
2089
+ const outputIdx = args.indexOf("--output");
2090
+ const output = outputIdx !== -1 ? args[outputIdx + 1] : undefined;
2091
+ const tagIdx = args.indexOf("--tag");
2092
+ const tag = tagIdx !== -1 ? args[tagIdx + 1] : undefined;
2093
+ if (!["json", "markdown", "obsidian"].includes(format)) {
2094
+ console.error(`Error: Invalid format: ${format}. Use json, markdown, or obsidian.`);
2095
+ process.exit(1);
2096
+ }
2097
+ const result = await memos.export({
2098
+ format: format,
2099
+ output,
2100
+ tag,
2101
+ });
2102
+ if (jsonFlag) {
2103
+ console.log(JSON.stringify(result, null, 2));
2104
+ }
2105
+ else {
2106
+ if (format === "json") {
2107
+ // Write to file
2108
+ const outPath = output ?? "./memos-export/memories.json";
2109
+ const dir = dirname(outPath);
2110
+ if (!existsSync(dir)) {
2111
+ const fs = await import("fs");
2112
+ fs.mkdirSync(dir, { recursive: true });
2113
+ }
2114
+ writeFileSync(outPath, result.data);
2115
+ console.log(`Exported ${result.count} memories to ${outPath}`);
2116
+ }
2117
+ else {
2118
+ console.log(`Exported ${result.count} memories to ${result.data}/`);
2119
+ }
2120
+ }
2121
+ break;
2122
+ }
2123
+ case "backup": {
2124
+ const outputIdx = args.indexOf("--output");
2125
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
2126
+ const defaultPath = `./memos-backup-${timestamp}.db`;
2127
+ const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : defaultPath;
2128
+ const resolvedDb = resolve(dbPath ??
2129
+ (await import("path")).join(process.env.HOME || process.env.USERPROFILE || ".", ".memos/memos.db"));
2130
+ const resolvedOut = resolve(outputPath);
2131
+ if (!existsSync(resolvedDb)) {
2132
+ console.error(`Error: Database not found at ${resolvedDb}`);
2133
+ console.error(`Run 'memos init' to create a database, or pass --db <path> to use a different one.`);
2134
+ process.exit(1);
2135
+ }
2136
+ // Close before copying
2137
+ await memos.close();
2138
+ copyFileSync(resolvedDb, resolvedOut);
2139
+ // Get stats for manifest
2140
+ const dbStat = statSync(resolvedDb);
2141
+ // Re-open to get counts
2142
+ const tmpMemos = new MemOS({ dbPath: resolvedDb });
2143
+ await tmpMemos.init();
2144
+ const graph = await tmpMemos.getGraph();
2145
+ await tmpMemos.close();
2146
+ const manifest = {
2147
+ timestamp: new Date().toISOString(),
2148
+ version: getSdkVersion(),
2149
+ nodeCount: graph.nodes.length,
2150
+ edgeCount: graph.edges.length,
2151
+ dbSizeBytes: dbStat.size,
2152
+ };
2153
+ const manifestPath = `${resolvedOut}.manifest.json`;
2154
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
2155
+ if (jsonFlag) {
2156
+ console.log(JSON.stringify({ backup: resolvedOut, manifest }, null, 2));
2157
+ }
2158
+ else {
2159
+ console.log(`Backup created: ${resolvedOut}`);
2160
+ console.log(` Nodes: ${manifest.nodeCount}, Edges: ${manifest.edgeCount}`);
2161
+ console.log(` DB size: ${(manifest.dbSizeBytes / 1024).toFixed(1)} KB`);
2162
+ console.log(` Manifest: ${manifestPath}`);
2163
+ }
2164
+ break;
2165
+ }
2166
+ case "restore": {
2167
+ const path = args[1];
2168
+ if (!path) {
2169
+ console.error("Error: backup path is required.\n Usage: memos restore <path>");
2170
+ process.exit(1);
2171
+ }
2172
+ const resolvedBackup = resolve(path);
2173
+ const manifestPath = `${resolvedBackup}.manifest.json`;
2174
+ if (!existsSync(resolvedBackup)) {
2175
+ console.error(`Error: Backup file not found: ${resolvedBackup}`);
2176
+ process.exit(1);
2177
+ }
2178
+ // Validate manifest
2179
+ if (existsSync(manifestPath)) {
2180
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
2181
+ if (jsonFlag) {
2182
+ console.log(JSON.stringify({ manifest }, null, 2));
2183
+ }
2184
+ else {
2185
+ console.log(`Backup manifest:`);
2186
+ console.log(` Timestamp: ${manifest.timestamp}`);
2187
+ console.log(` Version: ${manifest.version}`);
2188
+ console.log(` Nodes: ${manifest.nodeCount}, Edges: ${manifest.edgeCount}`);
2189
+ }
2190
+ }
2191
+ // Stop sweep, close DB
2192
+ await memos.close();
2193
+ const resolvedDb = resolve(dbPath ??
2194
+ (await import("path")).join(process.env.HOME || process.env.USERPROFILE || ".", ".memos/memos.db"));
2195
+ const dbDir = dirname(resolvedDb);
2196
+ if (!existsSync(dbDir)) {
2197
+ const fs = await import("fs");
2198
+ fs.mkdirSync(dbDir, { recursive: true });
2199
+ }
2200
+ copyFileSync(resolvedBackup, resolvedDb);
2201
+ // Re-open and report
2202
+ const restored = new MemOS({ dbPath: resolvedDb });
2203
+ await restored.init();
2204
+ const graph = await restored.getGraph();
2205
+ await restored.close();
2206
+ if (!jsonFlag) {
2207
+ console.log(`\nRestored from: ${resolvedBackup}`);
2208
+ console.log(` Nodes: ${graph.nodes.length}, Edges: ${graph.edges.length}`);
2209
+ }
2210
+ break;
2211
+ }
2212
+ case "doctor": {
2213
+ const jsonOut = jsonFlag;
2214
+ const report = {};
2215
+ const problems = [];
2216
+ const suggestions = [];
2217
+ // Playbook anchors appended to suggestions below; the full
2218
+ // Symptom → Diagnose → Fix entries live in docs/troubleshooting.md.
2219
+ const PLAYBOOK = "https://github.com/Markgatcha/memos/blob/main/docs/troubleshooting.md";
2220
+ // 1. Store health.
2221
+ const diag = await memos.diagnostics();
2222
+ report.store = {
2223
+ totalMemories: diag.totalNodes,
2224
+ totalEdges: diag.totalEdges,
2225
+ nodesWithEmbeddings: diag.nodesWithEmbeddings,
2226
+ dbSizeBytes: diag.dbSizeBytes ?? null,
2227
+ };
2228
+ if (jsonOut) {
2229
+ // detailed JSON path prints below
2230
+ }
2231
+ else {
2232
+ console.log(`Store: ${diag.totalNodes} memories, ${diag.totalEdges} edges, ` +
2233
+ `${diag.nodesWithEmbeddings} with embeddings.`);
2234
+ }
2235
+ // 2. Embedding coverage — partial coverage silently weakens the
2236
+ // semantic leg.
2237
+ if (diag.totalNodes > 0 && diag.nodesWithEmbeddings < diag.totalNodes) {
2238
+ problems.push(`Only ${diag.nodesWithEmbeddings}/${diag.totalNodes} memories have embeddings.`);
2239
+ suggestions.push(`Run \`memos reindex-embeddings\` to backfill missing vectors. ` +
2240
+ `Playbook: ${PLAYBOOK}#search-returns-nothing-or-weak-results`);
2241
+ }
2242
+ // 3. Embedding model mix — mixed models mean the semantic leg is
2243
+ // comparing against a subset only.
2244
+ const storage = memos
2245
+ .storage;
2246
+ if (storage.getEmbeddingModelCounts) {
2247
+ const counts = (await storage.getEmbeddingModelCounts?.()) ?? [];
2248
+ report.embeddingModels = counts;
2249
+ if (counts.length > 1) {
2250
+ problems.push(`Embeddings from ${counts.length} different models: ` +
2251
+ counts
2252
+ .map((c) => `${c.model}×${c.count}`)
2253
+ .join(", ") +
2254
+ " — vectors from different models are never compared.");
2255
+ suggestions.push("Run `memos reindex-embeddings --purge-stale` after switching models. " +
2256
+ `Playbook: ${PLAYBOOK}#search-returns-nothing-or-weak-results`);
2257
+ }
2258
+ if (!jsonOut) {
2259
+ console.log("Embedding models: " +
2260
+ (counts.length
2261
+ ? counts
2262
+ .map((c) => `${c.model} (${c.count})`)
2263
+ .join(", ")
2264
+ : "none stored"));
2265
+ }
2266
+ }
2267
+ // 4. Provider probe — embed one query and report what actually loaded.
2268
+ const embeddings = cliEmbeddingsConfig(projectConfig?.embeddings);
2269
+ if (embeddings?.provider && embeddings.baseUrl) {
2270
+ const started = Date.now();
2271
+ try {
2272
+ const probe = await fetch(`${embeddings.baseUrl.replace(/\/+$/, "")}/models`, { signal: AbortSignal.timeout(4_000) });
2273
+ report.embeddingEndpoint = {
2274
+ url: embeddings.baseUrl,
2275
+ reachable: probe.ok,
2276
+ ms: Date.now() - started,
2277
+ };
2278
+ if (!jsonOut) {
2279
+ console.log(`Embedding endpoint ${embeddings.baseUrl}: reachable (${probe.status}) in ${Date.now() - started}ms.`);
2280
+ }
2281
+ }
2282
+ catch (err) {
2283
+ problems.push(`Embedding endpoint ${embeddings.baseUrl} unreachable: ` +
2284
+ `${err instanceof Error ? err.message : String(err)}`);
2285
+ suggestions.push("Start llama-server (--embedding) or unset MEMOS_EMBEDDING_* to fall back to the hash embedder. " +
2286
+ `Playbook: ${PLAYBOOK}#search-returns-nothing-or-weak-results`);
2287
+ }
2288
+ }
2289
+ else {
2290
+ // Default path (no MEMOS_EMBEDDING_*): the SDK resolves its
2291
+ // default provider during the semantic probe below; the
2292
+ // runtime-info report after step 5 shows what actually loaded.
2293
+ }
2294
+ // 5. Semantic probe — does a search actually come back?
2295
+ const t0 = Date.now();
2296
+ const probeResults = await memos.search({
2297
+ query: "doctor connectivity probe",
2298
+ limit: 1,
2299
+ });
2300
+ report.semanticProbe = {
2301
+ ok: true,
2302
+ ms: Date.now() - t0,
2303
+ hits: probeResults.length,
2304
+ };
2305
+ if (!jsonOut) {
2306
+ console.log(`Semantic search probe: ok (${Date.now() - t0}ms, ${probeResults.length} candidate).`);
2307
+ }
2308
+ // 5b. Provider runtime info — the semantic probe above forced the
2309
+ // provider to resolve, so fallbackActive is accurate here.
2310
+ if (!(embeddings?.provider && embeddings.baseUrl)) {
2311
+ const info = memos.getEmbeddingRuntimeInfo();
2312
+ report.embeddingProvider = info;
2313
+ if (!jsonOut) {
2314
+ if (!info) {
2315
+ console.log("Embeddings disabled — semantic search falls back to keyword scoring.");
2316
+ }
2317
+ else if (info.fallbackActive) {
2318
+ problems.push(`Embedding fallback active: ${info.fallbackReason}.`);
2319
+ suggestions.push("Install @huggingface/transformers for real local embeddings: pnpm add @huggingface/transformers. " +
2320
+ `Playbook: ${PLAYBOOK}#embeddings-silently-fell-back-to-local-hash`);
2321
+ console.log(`Embedding provider: ${info.resolvedProvider} (fallback — ${info.fallbackReason}).`);
2322
+ }
2323
+ else {
2324
+ console.log(`Embedding provider: ${info.resolvedProvider} (${info.resolvedModel}, ${info.requestedDimensions}d).`);
2325
+ }
2326
+ }
2327
+ }
2328
+ // 6. Rerank endpoint probe (when configured).
2329
+ const rerankUrl = process.env.MEMOS_RERANK_URL;
2330
+ if (rerankUrl) {
2331
+ try {
2332
+ const started = Date.now();
2333
+ await fetch(`${rerankUrl.replace(/\/+$/, "")}/health`, {
2334
+ signal: AbortSignal.timeout(4_000),
2335
+ });
2336
+ report.rerank = {
2337
+ url: rerankUrl,
2338
+ reachable: true,
2339
+ ms: Date.now() - started,
2340
+ };
2341
+ if (!jsonOut) {
2342
+ console.log(`Rerank endpoint ${rerankUrl}: reachable.`);
2343
+ }
2344
+ }
2345
+ catch {
2346
+ problems.push(`Rerank endpoint ${rerankUrl} unreachable — reranking will silently fall back.`);
2347
+ suggestions.push(`Rerank is optional — an unreachable endpoint means reranking silently falls back to the base ranking. ` +
2348
+ `Playbook: ${PLAYBOOK}#search-returns-nothing-or-weak-results`);
2349
+ }
2350
+ }
2351
+ report.problems = problems;
2352
+ report.suggestions = suggestions;
2353
+ if (jsonOut) {
2354
+ console.log(JSON.stringify(report, null, 2));
2355
+ }
2356
+ else {
2357
+ if (problems.length === 0) {
2358
+ console.log("Doctor: no problems found.");
2359
+ }
2360
+ else {
2361
+ console.log(`Doctor: ${problems.length} problem(s):`);
2362
+ for (const problem of problems)
2363
+ console.log(` - ${problem}`);
2364
+ for (const suggestion of suggestions)
2365
+ console.log(` → ${suggestion}`);
2366
+ }
2367
+ }
2368
+ break;
2369
+ }
2370
+ case "reindex-embeddings": {
2371
+ const purgeStale = args.includes("--purge-stale");
2372
+ try {
2373
+ const result = await memos.reindexEmbeddings({ purgeStale });
2374
+ if (jsonFlag) {
2375
+ console.log(JSON.stringify(result));
2376
+ }
2377
+ else {
2378
+ console.log(`Re-embedded ${result.reembedded} memories with "${result.model}"` +
2379
+ `${result.purged ? `, purged ${result.purged} stale vectors` : ""}` +
2380
+ `${result.failed ? `, ${result.failed} FAILED` : ""}.`);
2381
+ }
2382
+ }
2383
+ catch (err) {
2384
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
2385
+ process.exit(1);
2386
+ }
2387
+ break;
2388
+ }
2389
+ case "init": {
2390
+ await runInitWizard(args);
2391
+ break;
2392
+ }
2393
+ case "connect": {
2394
+ const target = (args[1] ?? "").toLowerCase();
2395
+ const write = args.includes("--write");
2396
+ const force = args.includes("--force");
2397
+ const { getMcpTools } = await import("./mcp.js");
2398
+ const result = connectTarget(target, {
2399
+ dbPath,
2400
+ toolNames: getMcpTools().map((t) => t.name),
2401
+ });
2402
+ if (!result)
2403
+ break;
2404
+ if (!write) {
2405
+ console.log(result.instructions);
2406
+ console.log("\nRe-run with --write to write the config file directly " +
2407
+ "(add --force to overwrite an existing file).");
2408
+ break;
2409
+ }
2410
+ if (!result.configPath) {
2411
+ console.log(result.instructions);
2412
+ break;
2413
+ }
2414
+ if (existsSyncSafe(result.configPath) && !force) {
2415
+ console.error(`Error: ${result.configPath} already exists. Re-run with --force to overwrite.`);
2416
+ process.exit(1);
2417
+ }
2418
+ try {
2419
+ writeFileSafe(result.configPath, result.fileContents ?? "");
2420
+ console.log(`Wrote ${result.configPath}.`);
2421
+ console.log("Restart the harness so it picks up the new MCP server. " +
2422
+ "All data stays in the local SQLite store.");
2423
+ }
2424
+ catch (err) {
2425
+ console.error(`Error writing ${result.configPath}: ${err instanceof Error ? err.message : String(err)}`);
2426
+ process.exit(1);
2427
+ }
2428
+ break;
2429
+ }
2430
+ case "extract-facts": {
2431
+ // `memos extract-facts --transcript <path>` — extract durable facts
2432
+ // from a session transcript and store them.
2433
+ const transcriptIdx = args.indexOf("--transcript");
2434
+ const transcriptPath = transcriptIdx !== -1 ? args[transcriptIdx + 1] : undefined;
2435
+ const nsIdx = args.indexOf("--namespace");
2436
+ const namespace = nsIdx !== -1 ? args[nsIdx + 1] : "default";
2437
+ const confIdx = args.indexOf("--min-confidence");
2438
+ const minConfidence = confIdx !== -1 ? parseFloat(args[confIdx + 1]) : 0.6;
2439
+ const dryRun = args.includes("--dry-run");
2440
+ if (!transcriptPath) {
2441
+ console.error("Error: --transcript <path> is required.\n" +
2442
+ " Usage: memos extract-facts --transcript <path> [--namespace <ns>] [--min-confidence <n>] [--dry-run]");
2443
+ process.exit(1);
2444
+ }
2445
+ const { parseTranscriptFile } = await import("./session-hooks.js");
2446
+ let messages;
2447
+ try {
2448
+ // Claude Code JSONL first; falls back to plain text when the
2449
+ // JSONL parse yields no messages (parseTranscriptFile).
2450
+ messages = parseTranscriptFile(transcriptPath);
2451
+ }
2452
+ catch (err) {
2453
+ console.error(`Error reading transcript: ${err instanceof Error ? err.message : String(err)}`);
2454
+ process.exit(1);
2455
+ }
2456
+ if (messages.length === 0) {
2457
+ console.log("No messages found in transcript.");
2458
+ break;
2459
+ }
2460
+ const result = await memos.extractFacts(messages, {
2461
+ autoStore: !dryRun,
2462
+ minConfidence,
2463
+ namespace,
2464
+ });
2465
+ if (jsonFlag) {
2466
+ console.log(JSON.stringify({
2467
+ facts: result.facts.length,
2468
+ stored: result.storedIds.length,
2469
+ duplicates: result.duplicates,
2470
+ dryRun,
2471
+ }));
2472
+ }
2473
+ else {
2474
+ console.log(`Extracted ${result.facts.length} fact(s)` +
2475
+ (dryRun
2476
+ ? " (dry run, not stored)."
2477
+ : `, stored ${result.storedIds.length} (${result.duplicates} duplicates skipped).`));
2478
+ }
2479
+ break;
2480
+ }
2481
+ case "session-hook": {
2482
+ // `memos session-hook` — session-end hook entrypoint.
2483
+ // Claude Code: reads hook JSON from stdin, extracts the transcript
2484
+ // path. OpenCode: the generated plugin passes --opencode-session
2485
+ // <id>. Stores durable facts. Fail-open: never break teardown.
2486
+ if (process.env.MEMOS_SKIP_SESSION_HOOK === "1") {
2487
+ if (!jsonFlag)
2488
+ console.log("Session hook skipped (MEMOS_SKIP_SESSION_HOOK=1).");
2489
+ break;
2490
+ }
2491
+ const { parseTranscriptFile, findOpenCodeSessionMessages } = await import("./session-hooks.js");
2492
+ const { readFileSync } = await import("node:fs");
2493
+ const ocIdx = args.indexOf("--opencode-session");
2494
+ const openCodeSessionId = ocIdx !== -1 ? args[ocIdx + 1] : undefined;
2495
+ const nsIdx = args.indexOf("--namespace");
2496
+ const namespace = nsIdx !== -1 ? args[nsIdx + 1] : "default";
2497
+ try {
2498
+ let messages;
2499
+ if (openCodeSessionId) {
2500
+ // OpenCode path: look the ended session up in OpenCode's
2501
+ // data dir ($XDG_DATA_HOME/opencode or the platform default).
2502
+ messages = findOpenCodeSessionMessages(openCodeSessionId);
2503
+ if (messages.length === 0) {
2504
+ // Fail-open: session data not found (custom location,
2505
+ // already cleaned up, …) — nothing to extract.
2506
+ if (!jsonFlag)
2507
+ console.log("Session hook: OpenCode session not found, nothing to extract.");
2508
+ break;
2509
+ }
2510
+ }
2511
+ else {
2512
+ // Read hook input from stdin (Claude Code passes JSON).
2513
+ let input = {};
2514
+ try {
2515
+ const stdin = readFileSync(0, "utf-8").trim();
2516
+ if (stdin)
2517
+ input = JSON.parse(stdin);
2518
+ }
2519
+ catch {
2520
+ // No stdin or invalid JSON — try --transcript flag instead.
2521
+ }
2522
+ const transcriptIdx = args.indexOf("--transcript");
2523
+ const transcriptPath = (transcriptIdx !== -1 ? args[transcriptIdx + 1] : undefined) ??
2524
+ input.transcript_path;
2525
+ if (!transcriptPath) {
2526
+ // Fail-open: no transcript available, nothing to do.
2527
+ if (!jsonFlag)
2528
+ console.log("Session hook: no transcript path, nothing to extract.");
2529
+ break;
2530
+ }
2531
+ // Claude JSONL first, plain-text fallback when it yields nothing.
2532
+ messages = parseTranscriptFile(transcriptPath);
2533
+ }
2534
+ if (messages.length === 0)
2535
+ break;
2536
+ const result = await memos.extractFacts(messages, {
2537
+ autoStore: true,
2538
+ minConfidence: 0.6,
2539
+ namespace,
2540
+ });
2541
+ if (jsonFlag) {
2542
+ console.log(JSON.stringify({
2543
+ ok: true,
2544
+ facts: result.facts.length,
2545
+ stored: result.storedIds.length,
2546
+ duplicates: result.duplicates,
2547
+ }));
2548
+ }
2549
+ else {
2550
+ console.log(`Session hook: extracted ${result.facts.length} fact(s), stored ${result.storedIds.length}.`);
2551
+ }
2552
+ }
2553
+ catch (err) {
2554
+ // Fail-open: log but don't break session teardown.
2555
+ console.error(`Session hook failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
2556
+ }
2557
+ break;
2558
+ }
2559
+ case "dashboard": {
2560
+ // `memos dashboard` — start a local web dashboard.
2561
+ const portIdx = args.indexOf("--port");
2562
+ const port = portIdx !== -1 ? parseInt(args[portIdx + 1], 10) : undefined;
2563
+ const noOpen = args.includes("--no-open");
2564
+ const { startDashboard } = await import("./dashboard.js");
2565
+ const { url, close } = await startDashboard(memos, {
2566
+ ...(port !== undefined && !Number.isNaN(port) ? { port } : {}),
2567
+ });
2568
+ console.log(`\nMemOS dashboard running at ${url}`);
2569
+ console.log("Press Ctrl+C to stop.\n");
2570
+ if (!noOpen) {
2571
+ // Try to open the browser (fail silently if unavailable).
2572
+ const { exec } = await import("node:child_process");
2573
+ const openCmd = process.platform === "darwin"
2574
+ ? "open"
2575
+ : process.platform === "win32"
2576
+ ? "start"
2577
+ : "xdg-open";
2578
+ exec(`${openCmd} "${url}"`, () => { });
2579
+ }
2580
+ // Keep running until interrupted.
2581
+ await new Promise((resolve) => {
2582
+ const shutdown = async () => {
2583
+ await close();
2584
+ resolve();
2585
+ };
2586
+ process.once("SIGINT", shutdown);
2587
+ process.once("SIGTERM", shutdown);
2588
+ });
2589
+ break;
2590
+ }
2591
+ case "log": {
2592
+ // `memos log verify|show|checkpoint` — tamper-evident mutation log.
2593
+ const subcommand = args[1];
2594
+ const getFlag = (name) => {
2595
+ const idx = args.indexOf(name);
2596
+ return idx !== -1 ? args[idx + 1] : undefined;
2597
+ };
2598
+ if (subcommand === "verify") {
2599
+ const result = await memos.verifyTamperLog({
2600
+ expectTip: getFlag("--expect"),
2601
+ });
2602
+ if (jsonFlag) {
2603
+ console.log(JSON.stringify(result, null, 2));
2604
+ break;
2605
+ }
2606
+ if (!result.supported) {
2607
+ console.log("Tamper log not supported by this storage backend.");
2608
+ break;
2609
+ }
2610
+ console.log(`Tamper log: ${result.entriesChecked} entries checked, ${result.unloggedNodes} pre-log node(s).`);
2611
+ if (result.legacyEntries > 0) {
2612
+ console.log(`${result.legacyEntries} ${result.legacyEntries === 1 ? "entry predates" : "entries predate"} the full-state format: chain linkage verified, content cross-checked.`);
2613
+ }
2614
+ if (result.ok) {
2615
+ console.log("OK — chain intact, all nodes match their log entries.");
2616
+ }
2617
+ else {
2618
+ console.log("FAILED — tampering detected:");
2619
+ for (const issue of result.issues) {
2620
+ console.log(` [${issue.kind}] ${issue.detail}`);
2621
+ }
2622
+ process.exit(1);
2623
+ }
2624
+ break;
2625
+ }
2626
+ if (subcommand === "show") {
2627
+ const entries = await memos.readTamperLog();
2628
+ if (entries.length === 0) {
2629
+ console.log("Tamper log is empty or not supported by this storage backend.");
2630
+ break;
2631
+ }
2632
+ const limit = Math.max(1, parseInt(getFlag("--limit") ?? "20", 10));
2633
+ const tail = entries.slice(-limit);
2634
+ if (jsonFlag) {
2635
+ console.log(JSON.stringify(tail, null, 2));
2636
+ break;
2637
+ }
2638
+ for (const e of tail) {
2639
+ console.log(`#${e.seq} [${new Date(e.ts).toISOString()}] ${e.op} ${e.nodeId.slice(0, 8)} state:${e.nodeHash.slice(0, 12)} entry:${e.entryHash.slice(0, 12)}`);
2640
+ }
2641
+ break;
2642
+ }
2643
+ if (subcommand === "checkpoint") {
2644
+ const result = await memos.verifyTamperLog();
2645
+ if (!result.supported) {
2646
+ console.log("Tamper log not supported by this storage backend.");
2647
+ break;
2648
+ }
2649
+ if (jsonFlag) {
2650
+ console.log(JSON.stringify({ tipHash: result.tipHash }, null, 2));
2651
+ break;
2652
+ }
2653
+ if (!result.ok) {
2654
+ console.error("Refusing to checkpoint a broken chain — run `memos log verify` first.");
2655
+ process.exit(1);
2656
+ }
2657
+ console.log(result.tipHash ?? "(empty log — nothing to anchor yet)");
2658
+ console.log("Write this hash down somewhere the DB writer can't reach (paper, another machine, a signed commit).");
2659
+ console.log("Later: `memos log verify --expect <hash>` proves the chain still ends here.");
2660
+ break;
2661
+ }
2662
+ console.error("Error: unknown log subcommand.\n Usage: memos log <verify [--expect <hash>]|show [--limit N]|checkpoint>");
2663
+ process.exit(1);
2664
+ }
2665
+ case "sync": {
2666
+ // `memos sync export|import|status` — encrypted cross-machine sync.
2667
+ const subcommand = args[1];
2668
+ const { createBundle, decryptBundle, writeBundle, readBundle, importRecords, } = await import("./sync.js");
2669
+ const getFlag = (name) => {
2670
+ const idx = args.indexOf(name);
2671
+ return idx !== -1 ? args[idx + 1] : undefined;
2672
+ };
2673
+ if (subcommand === "export") {
2674
+ const output = getFlag("--output");
2675
+ if (!output) {
2676
+ console.error("Error: --output <file> is required.\n Usage: memos sync export --output <file> [--passphrase <passphrase>|--passphrase-file <path>]");
2677
+ process.exit(1);
2678
+ }
2679
+ const key = await resolveSyncKeyInteractive({
2680
+ passphrase: getFlag("--passphrase"),
2681
+ passphraseFile: getFlag("--passphrase-file"),
2682
+ });
2683
+ const graph = await memos.getGraph();
2684
+ // Export the full record: everything lands inside the
2685
+ // encrypted payload (v2 bundle) — no plaintext IDs/timestamps.
2686
+ const records = (graph.nodes ?? []).map((n) => ({
2687
+ id: n.id,
2688
+ content: n.content,
2689
+ summary: n.summary,
2690
+ type: n.type,
2691
+ tags: n.tags ?? [],
2692
+ importance: n.importance ?? 0.5,
2693
+ createdAt: n.createdAt,
2694
+ updatedAt: n.updatedAt,
2695
+ namespace: n.namespace ?? "default",
2696
+ metadata: n.metadata ?? {},
2697
+ source: n.source,
2698
+ provenance: n.provenance,
2699
+ pool: n.pool,
2700
+ expiresAt: n.expiresAt ?? null,
2701
+ validFrom: n.validFrom ?? null,
2702
+ validTo: n.validTo ?? null,
2703
+ harness: n.harness,
2704
+ }));
2705
+ const bundle = createBundle(records, key);
2706
+ writeBundle(output, bundle);
2707
+ console.log(`Exported ${records.length} memories to ${output} (encrypted).`);
2708
+ break;
2709
+ }
2710
+ if (subcommand === "import") {
2711
+ const input = getFlag("--input");
2712
+ if (!input) {
2713
+ console.error("Error: --input <file> is required.\n Usage: memos sync import --input <file> [--strategy skip-existing|last-write-wins]");
2714
+ process.exit(1);
2715
+ }
2716
+ const strategy = (getFlag("--strategy") ?? "skip-existing");
2717
+ if (strategy !== "skip-existing" && strategy !== "last-write-wins") {
2718
+ console.error("Error: --strategy must be skip-existing or last-write-wins");
2719
+ process.exit(1);
2720
+ }
2721
+ const key = await resolveSyncKeyInteractive({
2722
+ passphrase: getFlag("--passphrase"),
2723
+ passphraseFile: getFlag("--passphrase-file"),
2724
+ });
2725
+ const bundle = readBundle(input);
2726
+ let records;
2727
+ try {
2728
+ records = decryptBundle(bundle, key);
2729
+ }
2730
+ catch (err) {
2731
+ console.error(`Error: failed to decrypt bundle (wrong key?): ${err instanceof Error ? err.message : String(err)}`);
2732
+ process.exit(1);
2733
+ }
2734
+ const { imported, skipped, updated, errors } = await importRecords(memos, records, strategy);
2735
+ if (jsonFlag) {
2736
+ console.log(JSON.stringify({ imported, skipped, updated, errors }));
2737
+ }
2738
+ else {
2739
+ console.log(`Import complete: ${imported} imported, ${skipped} skipped, ${updated} updated.`);
2740
+ if (errors.length > 0) {
2741
+ console.log(`Errors: ${errors.length}`);
2742
+ errors.slice(0, 5).forEach((e) => console.log(` ${e}`));
2743
+ }
2744
+ }
2745
+ break;
2746
+ }
2747
+ if (subcommand === "status") {
2748
+ const input = getFlag("--input");
2749
+ if (!input) {
2750
+ console.error("Error: --input <file> is required.\n Usage: memos sync status --input <file>");
2751
+ process.exit(1);
2752
+ }
2753
+ const bundle = readBundle(input);
2754
+ console.log(`Bundle: ${bundle.records.length} records, exported ${bundle.exportedAt}`);
2755
+ console.log(`Version: ${bundle.version}`);
2756
+ // Don't decrypt — just show metadata (key not required for status)
2757
+ break;
2758
+ }
2759
+ console.error("Error: subcommand required.\n" +
2760
+ " Usage: memos sync <export|import|status> [options]\n" +
2761
+ " See: memos sync --help");
2762
+ process.exit(1);
2763
+ break;
2764
+ }
2765
+ case "trio": {
2766
+ // Boot the full AI Trio: MemOS (this process, memory) + LLM-Guardian
2767
+ // (optimization) + Universal-MCP-Toolkit (tools). With `--up` the
2768
+ // command locates the sibling repos and spawns Guardian + UMT from
2769
+ // their entrypoints; without it, it prints the compose plan.
2770
+ const up = args.includes("--up");
2771
+ const flagIdx = (name) => args.indexOf(name) !== -1 ? args[args.indexOf(name) + 1] : undefined;
2772
+ const home = process.env.HOME || process.env.USERPROFILE || ".";
2773
+ const candidates = [
2774
+ flagIdx("--guardian"),
2775
+ flagIdx("--umt"),
2776
+ join(home, "llm-guardian"),
2777
+ join(home, "universal-mcp-toolkit"),
2778
+ resolve(process.cwd(), "..", "llm-guardian"),
2779
+ resolve(process.cwd(), "..", "universal-mcp-toolkit"),
2780
+ ].filter(Boolean);
2781
+ const findRepo = (name) => candidates.find((c) => existsSync(join(c, "package.json")) && c.includes(name));
2782
+ const guardian = findRepo("llm-guardian");
2783
+ const umt = findRepo("universal-mcp-toolkit");
2784
+ if (!jsonFlag) {
2785
+ console.log("AI Trio — compose plan:");
2786
+ console.log(` [memory] memos (this process) db=${dbPath ?? join(home, ".memos/memos.db")}`);
2787
+ console.log(` [optimize] llm-guardian ${guardian ? `→ ${guardian}` : "(not found; pass --guardian <path>)"}`);
2788
+ console.log(` [tools] universal-mcp-toolkit ${umt ? `→ ${umt}` : "(not found; pass --umt <path>)"}`);
2789
+ }
2790
+ if (!up) {
2791
+ if (!jsonFlag)
2792
+ console.log("\nRun with --up to launch Guardian + UMT alongside this MemOS instance.");
2793
+ break;
2794
+ }
2795
+ const children = [];
2796
+ const spawn = async (cmd, cwd, env) => {
2797
+ const { spawn: _spawn } = await import("child_process");
2798
+ const child = _spawn(cmd, ["run", "start"], {
2799
+ cwd,
2800
+ env: { ...process.env, ...env },
2801
+ stdio: "inherit",
2802
+ shell: true,
2803
+ });
2804
+ child.on("error", (err) => {
2805
+ console.error(`Failed to launch ${cmd} in ${cwd}: ${err.message}`);
2806
+ });
2807
+ children.push(child);
2808
+ return child;
2809
+ };
2810
+ // Guardian must run under Node (not Bun) when MemOS memory is enabled,
2811
+ // because MemOS uses better-sqlite3 — a native Node module Bun cannot
2812
+ // load. If the user has not set MemOS env vars (standalone Guardian),
2813
+ // Bun is fine and faster to start.
2814
+ const memosEnabled = !!process.env.MEMOS_NAMESPACE ||
2815
+ !!process.env.MEMOS_STORAGE_PATH ||
2816
+ !!dbPath;
2817
+ if (guardian) {
2818
+ const guardianEnv = {};
2819
+ if (memosEnabled) {
2820
+ guardianEnv.MEMOS_NAMESPACE =
2821
+ process.env.MEMOS_NAMESPACE || "default";
2822
+ guardianEnv.MEMOS_STORAGE_PATH =
2823
+ process.env.MEMOS_STORAGE_PATH ||
2824
+ dbPath ||
2825
+ join(home, ".memos/memos.db");
2826
+ }
2827
+ await spawn(memosEnabled ? "node" : "bun", guardian, guardianEnv);
2828
+ if (!jsonFlag)
2829
+ console.log(` → llm-guardian via ${memosEnabled ? "node" : "bun"}${memosEnabled ? " (MemOS memory enabled)" : ""}`);
2830
+ }
2831
+ else if (!jsonFlag) {
2832
+ console.warn(" ⚠ skipping llm-guardian: not found. Pass --guardian <path> or clone it next to memos.");
2833
+ }
2834
+ if (umt) {
2835
+ await spawn("npx", umt, {});
2836
+ }
2837
+ else if (!jsonFlag) {
2838
+ console.warn(" ⚠ skipping universal-mcp-toolkit: not found. Pass --umt <path> or clone it next to memos.");
2839
+ }
2840
+ if (!guardian && !umt) {
2841
+ console.error("Error: no sibling repos located. Clone llm-guardian and/or universal-mcp-toolkit next to memos, or pass --guardian / --umt.");
2842
+ process.exit(1);
2843
+ }
2844
+ if (!jsonFlag && (guardian || umt))
2845
+ console.log("\nAI Trio is starting. MemOS is live (this process); Guardian + UMT launching above.");
2846
+ const shutdown = () => {
2847
+ for (const c of children)
2848
+ c.kill();
2849
+ process.exit(0);
2850
+ };
2851
+ process.on("SIGINT", shutdown);
2852
+ process.on("SIGTERM", shutdown);
2853
+ // Keep the parent alive while children run.
2854
+ await new Promise(() => { });
2855
+ break;
2856
+ }
2857
+ default:
2858
+ console.error(`Unknown command: ${command}`);
2859
+ const suggestion = suggestCommand(command);
2860
+ if (suggestion) {
2861
+ console.error(`Did you mean "memos ${suggestion}"?`);
2862
+ }
2863
+ printHelp();
2864
+ process.exit(1);
2865
+ }
2866
+ }
2867
+ finally {
2868
+ // Only close if we haven't already (backup/restore handle their own close)
2869
+ if (command !== "backup" && command !== "restore") {
2870
+ await memos.close();
2871
+ }
2872
+ }
2873
+ }
2874
+ main().catch((err) => {
2875
+ // err.message can echo key material from a failing operation — redact
2876
+ // before it hits stderr (CodeQL js/clear-text-logging).
2877
+ console.error("Fatal:", redactSecrets(err instanceof Error ? err.message : String(err)));
2878
+ process.exit(1);
2879
+ });
2880
+ //# sourceMappingURL=cli.js.map