ihow-memory 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +15 -0
- package/README.md +250 -0
- package/TRADEMARK.md +24 -0
- package/bin/ihow-memory.mjs +53 -0
- package/dist/cli.js +1084 -0
- package/dist/core.js +85 -0
- package/dist/engine/fts.js +210 -0
- package/dist/engine/manifest.js +45 -0
- package/dist/engine/retrieval.js +324 -0
- package/dist/governance.js +369 -0
- package/dist/http/console.js +287 -0
- package/dist/mcp/server.js +235 -0
- package/dist/store/events.js +17 -0
- package/dist/store/files.js +61 -0
- package/dist/store/lock.js +35 -0
- package/dist/telemetry.js +98 -0
- package/dist/types.js +3 -0
- package/dist/workspace.js +151 -0
- package/package.json +62 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1084 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --experimental-strip-types
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { openCore } from './core.js';
|
|
8
|
+
import { defaultRoot, ensureWorkspace, resolveWorkspace } from './workspace.js';
|
|
9
|
+
import { resolveEngineConfig } from './engine/retrieval.js';
|
|
10
|
+
import { sqliteRuntimeStatus } from './engine/fts.js';
|
|
11
|
+
import * as telemetry from './telemetry.js';
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const [command = 'help', ...tail] = argv;
|
|
14
|
+
const options = {};
|
|
15
|
+
const rest = [];
|
|
16
|
+
for(let index = 0; index < tail.length; index += 1){
|
|
17
|
+
const arg = tail[index];
|
|
18
|
+
if (arg === '--space') options.space = tail[++index];
|
|
19
|
+
else if (arg === '--root') options.root = tail[++index];
|
|
20
|
+
else if (arg === '--memory-root') options.memoryRoot = tail[++index];
|
|
21
|
+
else if (arg === '--state-root') options.stateRoot = tail[++index];
|
|
22
|
+
else if (arg === '--cwd') options.cwd = tail[++index];
|
|
23
|
+
else if (arg === '--engine') options.engine = tail[++index];
|
|
24
|
+
else if (arg === '--vector-provider-command') options.vectorProviderCommand = tail[++index];
|
|
25
|
+
else if (arg === '--vector-model') options.vectorModel = tail[++index];
|
|
26
|
+
else if (arg === '--vector-timeout-ms') options.vectorTimeoutMs = Number(tail[++index]);
|
|
27
|
+
else if (arg === '--runtime') {
|
|
28
|
+
const runtime = tail[++index];
|
|
29
|
+
if (runtime === 'claude-code' || runtime === 'codex' || runtime === 'cursor') options.runtime = runtime;
|
|
30
|
+
else throw new Error('unsupported_runtime_use_claude-code_codex_or_cursor');
|
|
31
|
+
} else if (arg === '--share-diagnostics') options.shareDiagnostics = true;
|
|
32
|
+
else if (arg === '--json') options.json = true;
|
|
33
|
+
else if (arg === '--limit') options.limit = Number(tail[++index]);
|
|
34
|
+
else if (arg === '--dry-run') options.dryRun = true;
|
|
35
|
+
else if (arg === '--real-write') options.realWrite = true;
|
|
36
|
+
else if (arg === '--actor') options.actor = tail[++index];
|
|
37
|
+
else rest.push(arg);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
command,
|
|
41
|
+
options,
|
|
42
|
+
rest
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function printJson(value) {
|
|
46
|
+
console.log(JSON.stringify(value, null, 2));
|
|
47
|
+
}
|
|
48
|
+
function workspaceMcpConfigSnippet(memoryRoot, stateRoot, runtimeDir) {
|
|
49
|
+
return {
|
|
50
|
+
mcpServers: {
|
|
51
|
+
'ihow-memory': {
|
|
52
|
+
command: 'node',
|
|
53
|
+
args: [
|
|
54
|
+
'mcp/server.js',
|
|
55
|
+
'--memory-root',
|
|
56
|
+
memoryRoot,
|
|
57
|
+
'--state-root',
|
|
58
|
+
stateRoot
|
|
59
|
+
],
|
|
60
|
+
cwd: runtimeDir
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function packageDir() {
|
|
66
|
+
return path.resolve(new URL('..', import.meta.url).pathname);
|
|
67
|
+
}
|
|
68
|
+
function runtimeLabel(runtime) {
|
|
69
|
+
if (runtime === 'claude-code') return 'Claude Code';
|
|
70
|
+
if (runtime === 'codex') return 'Codex';
|
|
71
|
+
if (runtime === 'cursor') return 'Cursor';
|
|
72
|
+
return 'generic MCP client';
|
|
73
|
+
}
|
|
74
|
+
function codexTomlSnippet(memoryRoot, stateRoot, runtimeDir) {
|
|
75
|
+
return `[mcp_servers.ihow-memory]
|
|
76
|
+
command = "node"
|
|
77
|
+
args = [
|
|
78
|
+
"mcp/server.js",
|
|
79
|
+
"--memory-root",
|
|
80
|
+
"${memoryRoot}",
|
|
81
|
+
"--state-root",
|
|
82
|
+
"${stateRoot}"
|
|
83
|
+
]
|
|
84
|
+
cwd = "${runtimeDir}"`;
|
|
85
|
+
}
|
|
86
|
+
function runtimeConfigSnippet(workspace, runtime) {
|
|
87
|
+
const stateRoot = workspace.root;
|
|
88
|
+
const runtimeDir = path.join(workspace.spaceDir, '.runtime');
|
|
89
|
+
if (runtime === 'codex') return codexTomlSnippet(workspace.memoryDir, stateRoot, runtimeDir);
|
|
90
|
+
return workspaceMcpConfigSnippet(workspace.memoryDir, stateRoot, runtimeDir);
|
|
91
|
+
}
|
|
92
|
+
function printRuntimeSnippet(snippet, runtime) {
|
|
93
|
+
const label = runtimeLabel(runtime);
|
|
94
|
+
console.log(`\n${label} MCP config snippet:`);
|
|
95
|
+
if (typeof snippet === 'string') console.log(snippet);
|
|
96
|
+
else printJson(snippet);
|
|
97
|
+
}
|
|
98
|
+
function initBackupGuidance(runtime) {
|
|
99
|
+
if (runtime === 'codex') return 'Before editing Codex config, copy the existing config file or commit it first.';
|
|
100
|
+
if (runtime === 'claude-code') return 'Before editing Claude Code MCP settings, make a copy of the current settings file.';
|
|
101
|
+
if (runtime === 'cursor') return 'Before editing Cursor MCP settings, copy the current MCP/settings JSON.';
|
|
102
|
+
return 'Before writing this snippet into any runtime config, back up the existing config file.';
|
|
103
|
+
}
|
|
104
|
+
async function installRuntimeBundle(workspace) {
|
|
105
|
+
const source = path.join(packageDir(), 'dist');
|
|
106
|
+
const target = path.join(workspace.spaceDir, '.runtime');
|
|
107
|
+
try {
|
|
108
|
+
await fs.access(path.join(source, 'mcp', 'server.js'));
|
|
109
|
+
} catch {
|
|
110
|
+
throw new Error('runtime_bundle_missing_run_npm_build');
|
|
111
|
+
}
|
|
112
|
+
await fs.rm(target, {
|
|
113
|
+
recursive: true,
|
|
114
|
+
force: true
|
|
115
|
+
});
|
|
116
|
+
await fs.cp(source, target, {
|
|
117
|
+
recursive: true
|
|
118
|
+
});
|
|
119
|
+
await fs.writeFile(path.join(target, 'package.json'), `${JSON.stringify({
|
|
120
|
+
type: 'module'
|
|
121
|
+
}, null, 2)}\n`, 'utf8');
|
|
122
|
+
return target;
|
|
123
|
+
}
|
|
124
|
+
function commandExists(bin) {
|
|
125
|
+
const probe = spawnSync(process.platform === 'win32' ? 'where' : 'which', [
|
|
126
|
+
bin
|
|
127
|
+
], {
|
|
128
|
+
encoding: 'utf8'
|
|
129
|
+
});
|
|
130
|
+
return probe.status === 0;
|
|
131
|
+
}
|
|
132
|
+
function mcpServerSpec(workspace) {
|
|
133
|
+
const serverEntry = path.join(workspace.spaceDir, '.runtime', 'mcp', 'server.js');
|
|
134
|
+
return {
|
|
135
|
+
command: 'node',
|
|
136
|
+
args: [
|
|
137
|
+
serverEntry,
|
|
138
|
+
'--memory-root',
|
|
139
|
+
workspace.memoryDir,
|
|
140
|
+
'--state-root',
|
|
141
|
+
workspace.root
|
|
142
|
+
]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
async function writeJsonMcpConfig(targetPath, runtime, spec, options) {
|
|
146
|
+
let config = {};
|
|
147
|
+
let existed = false;
|
|
148
|
+
let raw = null;
|
|
149
|
+
try {
|
|
150
|
+
raw = await fs.readFile(targetPath, 'utf8');
|
|
151
|
+
existed = true;
|
|
152
|
+
} catch (err) {
|
|
153
|
+
const code = err.code;
|
|
154
|
+
if (code === 'ENOENT') {
|
|
155
|
+
existed = false;
|
|
156
|
+
} else {
|
|
157
|
+
throw new Error(`connect_cannot_read_config: ${targetPath}: ${err.message}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (raw !== null) {
|
|
161
|
+
try {
|
|
162
|
+
const parsed = JSON.parse(raw);
|
|
163
|
+
if (!parsed || typeof parsed !== 'object') throw new Error('config is not a JSON object');
|
|
164
|
+
config = parsed;
|
|
165
|
+
} catch (err) {
|
|
166
|
+
throw new Error(`connect_refuse_overwrite_unparseable_config: ${targetPath} exists but is not valid JSON (${err.message}). Aborting to avoid data loss — fix/remove the file or use the runtime's official CLI.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let backup = '';
|
|
170
|
+
if (existed && !options.dryRun) {
|
|
171
|
+
backup = `${targetPath}.ihow-bak-${Date.now()}`;
|
|
172
|
+
await fs.copyFile(targetPath, backup);
|
|
173
|
+
}
|
|
174
|
+
const servers = config.mcpServers && typeof config.mcpServers === 'object' ? config.mcpServers : {};
|
|
175
|
+
servers['ihow-memory'] = {
|
|
176
|
+
type: 'stdio',
|
|
177
|
+
command: spec.command,
|
|
178
|
+
args: spec.args
|
|
179
|
+
};
|
|
180
|
+
config.mcpServers = servers;
|
|
181
|
+
if (!options.dryRun) {
|
|
182
|
+
await fs.mkdir(path.dirname(targetPath), {
|
|
183
|
+
recursive: true
|
|
184
|
+
});
|
|
185
|
+
const tmp = `${targetPath}.ihow-tmp-${process.pid}`;
|
|
186
|
+
await fs.writeFile(tmp, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
187
|
+
await fs.rename(tmp, targetPath);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
runtime,
|
|
192
|
+
method: 'direct-json',
|
|
193
|
+
target: targetPath,
|
|
194
|
+
backup,
|
|
195
|
+
dryRun: !!options.dryRun,
|
|
196
|
+
existed
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function connectViaClaudeCli(spec, options) {
|
|
200
|
+
if (!commandExists('claude')) return null;
|
|
201
|
+
const exists = spawnSync('claude', [
|
|
202
|
+
'mcp',
|
|
203
|
+
'get',
|
|
204
|
+
'ihow-memory'
|
|
205
|
+
], {
|
|
206
|
+
encoding: 'utf8'
|
|
207
|
+
}).status === 0;
|
|
208
|
+
if (options.dryRun) {
|
|
209
|
+
return {
|
|
210
|
+
ok: true,
|
|
211
|
+
runtime: 'claude-code',
|
|
212
|
+
method: 'official-cli:claude',
|
|
213
|
+
alreadyExists: exists,
|
|
214
|
+
dryRun: true
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (exists) spawnSync('claude', [
|
|
218
|
+
'mcp',
|
|
219
|
+
'remove',
|
|
220
|
+
'ihow-memory',
|
|
221
|
+
'--scope',
|
|
222
|
+
'user'
|
|
223
|
+
], {
|
|
224
|
+
encoding: 'utf8'
|
|
225
|
+
});
|
|
226
|
+
const json = JSON.stringify({
|
|
227
|
+
type: 'stdio',
|
|
228
|
+
command: spec.command,
|
|
229
|
+
args: spec.args
|
|
230
|
+
});
|
|
231
|
+
const add = spawnSync('claude', [
|
|
232
|
+
'mcp',
|
|
233
|
+
'add-json',
|
|
234
|
+
'--scope',
|
|
235
|
+
'user',
|
|
236
|
+
'ihow-memory',
|
|
237
|
+
json
|
|
238
|
+
], {
|
|
239
|
+
encoding: 'utf8'
|
|
240
|
+
});
|
|
241
|
+
if (add.status !== 0) {
|
|
242
|
+
throw new Error(`claude_mcp_add_failed: ${(add.stderr || add.stdout || '').slice(0, 300)}`);
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
ok: true,
|
|
246
|
+
runtime: 'claude-code',
|
|
247
|
+
method: 'official-cli:claude',
|
|
248
|
+
target: '~/.claude.json (claude mcp add-json --scope user)',
|
|
249
|
+
replaced: exists
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function connectViaCodexCli(spec, options) {
|
|
253
|
+
if (!commandExists('codex')) {
|
|
254
|
+
throw new Error('codex_cli_not_found: install the Codex CLI to connect codex (or run init for manual TOML).');
|
|
255
|
+
}
|
|
256
|
+
const exists = spawnSync('codex', [
|
|
257
|
+
'mcp',
|
|
258
|
+
'get',
|
|
259
|
+
'ihow-memory'
|
|
260
|
+
], {
|
|
261
|
+
encoding: 'utf8'
|
|
262
|
+
}).status === 0;
|
|
263
|
+
if (options.dryRun) {
|
|
264
|
+
return {
|
|
265
|
+
ok: true,
|
|
266
|
+
runtime: 'codex',
|
|
267
|
+
method: 'official-cli:codex',
|
|
268
|
+
alreadyExists: exists,
|
|
269
|
+
dryRun: true
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (exists) spawnSync('codex', [
|
|
273
|
+
'mcp',
|
|
274
|
+
'remove',
|
|
275
|
+
'ihow-memory'
|
|
276
|
+
], {
|
|
277
|
+
encoding: 'utf8'
|
|
278
|
+
});
|
|
279
|
+
const add = spawnSync('codex', [
|
|
280
|
+
'mcp',
|
|
281
|
+
'add',
|
|
282
|
+
'ihow-memory',
|
|
283
|
+
'--',
|
|
284
|
+
spec.command,
|
|
285
|
+
...spec.args
|
|
286
|
+
], {
|
|
287
|
+
encoding: 'utf8'
|
|
288
|
+
});
|
|
289
|
+
if (add.status !== 0) {
|
|
290
|
+
throw new Error(`codex_mcp_add_failed: ${(add.stderr || add.stdout || '').slice(0, 300)}`);
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
ok: true,
|
|
294
|
+
runtime: 'codex',
|
|
295
|
+
method: 'official-cli:codex',
|
|
296
|
+
target: '~/.codex/config.toml (codex mcp add)',
|
|
297
|
+
replaced: exists
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
async function connectRuntime(workspace, runtime, options) {
|
|
301
|
+
const home = os.homedir();
|
|
302
|
+
const spec = mcpServerSpec(workspace);
|
|
303
|
+
if (runtime === 'claude-code') {
|
|
304
|
+
const viaCli = connectViaClaudeCli(spec, options);
|
|
305
|
+
if (viaCli) return viaCli;
|
|
306
|
+
return writeJsonMcpConfig(path.join(home, '.claude.json'), runtime, spec, options);
|
|
307
|
+
}
|
|
308
|
+
if (runtime === 'codex') {
|
|
309
|
+
return connectViaCodexCli(spec, options);
|
|
310
|
+
}
|
|
311
|
+
if (runtime === 'cursor') {
|
|
312
|
+
return writeJsonMcpConfig(path.join(home, '.cursor', 'mcp.json'), runtime, spec, options);
|
|
313
|
+
}
|
|
314
|
+
throw new Error(`connect_unsupported_runtime: ${runtime}`);
|
|
315
|
+
}
|
|
316
|
+
function help() {
|
|
317
|
+
console.log(`iHow Memory Core A0.1
|
|
318
|
+
|
|
319
|
+
Usage:
|
|
320
|
+
ihow-memory init [--space name] [--root path] [--runtime claude-code|codex|cursor]
|
|
321
|
+
ihow-memory status [--space name] [--root path] [--memory-root path] [--state-root path] [--json]
|
|
322
|
+
ihow-memory doctor [--space name] [--root path] [--memory-root path] [--state-root path] [--runtime claude-code|codex|cursor] [--share-diagnostics] [--json]
|
|
323
|
+
ihow-memory proof [--root path] [--space name] [--engine fts|vector-gguf]
|
|
324
|
+
ihow-memory reindex [--memory-root path] [--state-root path] [--json]
|
|
325
|
+
ihow-memory search <query> [--limit n]
|
|
326
|
+
ihow-memory read <memory/path.md>
|
|
327
|
+
ihow-memory write-candidate <text> [--space name]
|
|
328
|
+
ihow-memory promote <candidate-path> [--scope name] [--title title]
|
|
329
|
+
ihow-memory durable-promote <candidate-path> (--dry-run | --real-write) [--scope name] [--title title] [--path path]
|
|
330
|
+
ihow-memory feedback [--runtime claude-code|codex|cursor]
|
|
331
|
+
ihow-memory reset --space name [--root path]
|
|
332
|
+
ihow-memory console [--port 8788] [--host 127.0.0.1] [--memory-root path] # read-only local web UI
|
|
333
|
+
ihow-memory connect --runtime claude-code|codex|cursor [--dry-run] [--json] # auto-config MCP (official CLI for claude/codex; safe backup+merge for cursor)
|
|
334
|
+
ihow-memory telemetry [on|off|status] # anonymous usage telemetry — OFF by default; only event/runtime/version, never memory content
|
|
335
|
+
|
|
336
|
+
Defaults:
|
|
337
|
+
root: ${defaultRoot()}
|
|
338
|
+
space: derived from cwd unless --space is provided
|
|
339
|
+
`);
|
|
340
|
+
}
|
|
341
|
+
async function isWritable(dir) {
|
|
342
|
+
try {
|
|
343
|
+
await fs.mkdir(dir, {
|
|
344
|
+
recursive: true
|
|
345
|
+
});
|
|
346
|
+
const probe = path.join(dir, `.write-test-${process.pid}-${Date.now()}`);
|
|
347
|
+
await fs.writeFile(probe, 'ok', 'utf8');
|
|
348
|
+
await fs.rm(probe, {
|
|
349
|
+
force: true
|
|
350
|
+
});
|
|
351
|
+
return true;
|
|
352
|
+
} catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async function latestAuditSummary(eventsDir) {
|
|
357
|
+
let entries;
|
|
358
|
+
try {
|
|
359
|
+
entries = await fs.readdir(eventsDir);
|
|
360
|
+
} catch {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
const files = entries.filter((entry)=>entry.endsWith('.ndjson')).sort();
|
|
364
|
+
const latest = files.at(-1);
|
|
365
|
+
if (!latest) return null;
|
|
366
|
+
const eventPath = path.join(eventsDir, latest);
|
|
367
|
+
const lines = (await fs.readFile(eventPath, 'utf8')).trim().split('\n').filter(Boolean);
|
|
368
|
+
const last = lines.at(-1);
|
|
369
|
+
if (!last) return null;
|
|
370
|
+
return {
|
|
371
|
+
path: eventPath,
|
|
372
|
+
event: JSON.parse(last)
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
const SECRET_PATTERNS = [
|
|
376
|
+
[
|
|
377
|
+
/\b(Bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi,
|
|
378
|
+
'$1[redacted]'
|
|
379
|
+
],
|
|
380
|
+
[
|
|
381
|
+
/\b(sk-[A-Za-z0-9_-]{8,})\b/g,
|
|
382
|
+
'[redacted]'
|
|
383
|
+
],
|
|
384
|
+
[
|
|
385
|
+
/\b(ghp_[A-Za-z0-9_]{8,})\b/g,
|
|
386
|
+
'[redacted]'
|
|
387
|
+
],
|
|
388
|
+
[
|
|
389
|
+
/\b(github_pat_[A-Za-z0-9_]{8,})\b/g,
|
|
390
|
+
'[redacted]'
|
|
391
|
+
],
|
|
392
|
+
[
|
|
393
|
+
/\b(AKIA[0-9A-Z]{16})\b/g,
|
|
394
|
+
'[redacted]'
|
|
395
|
+
],
|
|
396
|
+
[
|
|
397
|
+
/\b(token|password|passwd|secret|api[_-]?key|authorization|cookie)\b\s*[:=]\s*[^\s"',;]+/gi,
|
|
398
|
+
'$1=[redacted]'
|
|
399
|
+
]
|
|
400
|
+
];
|
|
401
|
+
function redactSecrets(value) {
|
|
402
|
+
return SECRET_PATTERNS.reduce((text, [pattern, replacement])=>text.replace(pattern, replacement), value);
|
|
403
|
+
}
|
|
404
|
+
function redactionHints(options = {}, status) {
|
|
405
|
+
const workspace = resolveWorkspace(options);
|
|
406
|
+
const statusWorkspace = status?.workspace || {};
|
|
407
|
+
const index = status?.index || {};
|
|
408
|
+
const hints = [
|
|
409
|
+
[
|
|
410
|
+
os.homedir(),
|
|
411
|
+
'<home>'
|
|
412
|
+
],
|
|
413
|
+
[
|
|
414
|
+
process.cwd(),
|
|
415
|
+
'<cwd>'
|
|
416
|
+
],
|
|
417
|
+
[
|
|
418
|
+
packageDir(),
|
|
419
|
+
'<package-dir>'
|
|
420
|
+
],
|
|
421
|
+
[
|
|
422
|
+
workspace.root,
|
|
423
|
+
'<state-root>'
|
|
424
|
+
],
|
|
425
|
+
[
|
|
426
|
+
workspace.spaceDir,
|
|
427
|
+
'<workspace>'
|
|
428
|
+
],
|
|
429
|
+
[
|
|
430
|
+
workspace.memoryDir,
|
|
431
|
+
'<memory-root>'
|
|
432
|
+
]
|
|
433
|
+
];
|
|
434
|
+
for (const [key, label] of [
|
|
435
|
+
[
|
|
436
|
+
'root',
|
|
437
|
+
'<state-root>'
|
|
438
|
+
],
|
|
439
|
+
[
|
|
440
|
+
'path',
|
|
441
|
+
'<workspace>'
|
|
442
|
+
],
|
|
443
|
+
[
|
|
444
|
+
'memoryRoot',
|
|
445
|
+
'<memory-root>'
|
|
446
|
+
]
|
|
447
|
+
]){
|
|
448
|
+
if (typeof statusWorkspace[key] === 'string') hints.push([
|
|
449
|
+
statusWorkspace[key],
|
|
450
|
+
label
|
|
451
|
+
]);
|
|
452
|
+
}
|
|
453
|
+
for (const [key, label] of [
|
|
454
|
+
[
|
|
455
|
+
'path',
|
|
456
|
+
'<index>'
|
|
457
|
+
],
|
|
458
|
+
[
|
|
459
|
+
'manifestPath',
|
|
460
|
+
'<index-manifest>'
|
|
461
|
+
]
|
|
462
|
+
]){
|
|
463
|
+
if (typeof index[key] === 'string') hints.push([
|
|
464
|
+
index[key],
|
|
465
|
+
label
|
|
466
|
+
]);
|
|
467
|
+
}
|
|
468
|
+
return hints.filter(([absolute])=>path.isAbsolute(absolute)).sort((a, b)=>b[0].length - a[0].length);
|
|
469
|
+
}
|
|
470
|
+
function redactPaths(value, hints) {
|
|
471
|
+
let text = value;
|
|
472
|
+
for (const [absolute, label] of hints){
|
|
473
|
+
const normalized = absolute.replace(/\\/g, '/');
|
|
474
|
+
text = text.split(absolute).join(label);
|
|
475
|
+
text = text.split(normalized).join(label);
|
|
476
|
+
}
|
|
477
|
+
return text.replace(/(^|[\s"'`=([{:,])\/(?:[^\s"'`)\]}{,;]|\\ )+/g, (_match, prefix)=>`${prefix}<path>`);
|
|
478
|
+
}
|
|
479
|
+
function sanitizeString(value, hints) {
|
|
480
|
+
return redactPaths(redactSecrets(value), hints).slice(0, 1000);
|
|
481
|
+
}
|
|
482
|
+
function sanitizeValue(value, hints) {
|
|
483
|
+
if (typeof value === 'string') return sanitizeString(value, hints);
|
|
484
|
+
if (Array.isArray(value)) return value.map((entry)=>sanitizeValue(entry, hints));
|
|
485
|
+
if (value && typeof value === 'object') {
|
|
486
|
+
const output = {};
|
|
487
|
+
for (const [key, entry] of Object.entries(value)){
|
|
488
|
+
if (/token|password|secret|api[_-]?key|authorization|cookie/i.test(key)) {
|
|
489
|
+
output[key] = '[redacted]';
|
|
490
|
+
} else {
|
|
491
|
+
output[key] = sanitizeValue(entry, hints);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return output;
|
|
495
|
+
}
|
|
496
|
+
return value;
|
|
497
|
+
}
|
|
498
|
+
function sanitizeDoctorResult(result, options) {
|
|
499
|
+
const hints = redactionHints(options, result.status);
|
|
500
|
+
return sanitizeValue(result, hints);
|
|
501
|
+
}
|
|
502
|
+
function friendlyError(error) {
|
|
503
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
504
|
+
return sanitizeString(raw, redactionHints()).slice(0, 500);
|
|
505
|
+
}
|
|
506
|
+
function nodeVersionAtLeast(actual, expected) {
|
|
507
|
+
return actual.localeCompare(expected, undefined, {
|
|
508
|
+
numeric: true
|
|
509
|
+
}) >= 0;
|
|
510
|
+
}
|
|
511
|
+
async function doctor(options) {
|
|
512
|
+
const checks = [];
|
|
513
|
+
const workspace = resolveWorkspace(options);
|
|
514
|
+
const nodeOk = nodeVersionAtLeast(process.versions.node, '22.12.0');
|
|
515
|
+
const sqliteStatus = sqliteRuntimeStatus();
|
|
516
|
+
const writable = await isWritable(workspace.memoryDir);
|
|
517
|
+
let status;
|
|
518
|
+
checks.push({
|
|
519
|
+
name: 'node',
|
|
520
|
+
ok: nodeOk,
|
|
521
|
+
detail: `v${process.versions.node}`,
|
|
522
|
+
hint: nodeOk ? undefined : 'Install Node >= 22.12, then rerun: ihow-memory doctor. Example: nvm install 22 && nvm use 22.',
|
|
523
|
+
severity: nodeOk ? 'info' : 'error',
|
|
524
|
+
required: true
|
|
525
|
+
});
|
|
526
|
+
checks.push({
|
|
527
|
+
name: 'sqlite',
|
|
528
|
+
ok: sqliteStatus.ok,
|
|
529
|
+
detail: sqliteStatus.detail,
|
|
530
|
+
hint: sqliteStatus.ok ? undefined : 'Use a Node build with node:sqlite. The supported path is Node >= 22.12 from nodejs.org, nvm, fnm, or Volta.',
|
|
531
|
+
severity: sqliteStatus.ok ? 'info' : 'error',
|
|
532
|
+
required: true
|
|
533
|
+
});
|
|
534
|
+
checks.push({
|
|
535
|
+
name: 'memory-root',
|
|
536
|
+
ok: writable,
|
|
537
|
+
detail: workspace.memoryDir,
|
|
538
|
+
hint: writable ? undefined : 'Choose a writable location: ihow-memory init --root <writable-dir> or ihow-memory doctor --memory-root <writable-memory-dir> --state-root <writable-state-dir>.',
|
|
539
|
+
severity: writable ? 'info' : 'error',
|
|
540
|
+
required: true
|
|
541
|
+
});
|
|
542
|
+
checks.push({
|
|
543
|
+
name: 'runtime',
|
|
544
|
+
ok: Boolean(options.runtime),
|
|
545
|
+
detail: options.runtime ? `${runtimeLabel(options.runtime)} selected` : 'not selected',
|
|
546
|
+
hint: options.runtime ? `Run ihow-memory init --runtime ${options.runtime} and paste the snippet into ${runtimeLabel(options.runtime)} after backing up existing config.` : 'Run ihow-memory init --runtime claude-code, --runtime codex, or --runtime cursor to print a ready-to-paste MCP snippet.',
|
|
547
|
+
severity: options.runtime ? 'info' : 'warning',
|
|
548
|
+
required: false
|
|
549
|
+
});
|
|
550
|
+
if (nodeOk && sqliteStatus.ok && writable) {
|
|
551
|
+
try {
|
|
552
|
+
const core = await openCore(options);
|
|
553
|
+
status = await core.status();
|
|
554
|
+
} catch (error) {
|
|
555
|
+
checks.push({
|
|
556
|
+
name: 'core-status',
|
|
557
|
+
ok: false,
|
|
558
|
+
detail: friendlyError(error),
|
|
559
|
+
hint: 'Run ihow-memory doctor --share-diagnostics and include the redacted output in a feedback issue.',
|
|
560
|
+
severity: 'error',
|
|
561
|
+
required: true
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
const engineConfig = resolveEngineConfig(options);
|
|
566
|
+
if (status) {
|
|
567
|
+
const provider = status.provider;
|
|
568
|
+
const index = status.index;
|
|
569
|
+
const sync = status.sync;
|
|
570
|
+
const providerDetail = provider.fallback ? `active=fts fallbackFrom=${provider.fallbackFrom} lastError=${provider.lastError}` : `active=${provider.id} ready=${provider.ready}`;
|
|
571
|
+
checks.push({
|
|
572
|
+
name: 'engine',
|
|
573
|
+
ok: provider.ready === true,
|
|
574
|
+
detail: providerDetail,
|
|
575
|
+
hint: provider.ready ? undefined : 'FTS should be available locally; run ihow-memory reindex or check workspace paths.',
|
|
576
|
+
severity: provider.ready ? 'info' : 'error',
|
|
577
|
+
required: true
|
|
578
|
+
});
|
|
579
|
+
checks.push({
|
|
580
|
+
name: 'vector',
|
|
581
|
+
ok: true,
|
|
582
|
+
detail: engineConfig.vectorProviderCommand ? `configured requested=${engineConfig.requestedId}` : `not configured requested=${engineConfig.requestedId}`,
|
|
583
|
+
severity: 'info',
|
|
584
|
+
required: false
|
|
585
|
+
});
|
|
586
|
+
checks.push({
|
|
587
|
+
name: 'index-manifest',
|
|
588
|
+
ok: Boolean(index.manifestPath),
|
|
589
|
+
detail: index.lastError ? `${String(index.manifestPath)} lastError=${String(index.lastError)}` : String(index.manifestPath),
|
|
590
|
+
hint: index.manifestPath ? undefined : 'Run ihow-memory reindex to create the local index manifest.',
|
|
591
|
+
severity: index.manifestPath ? 'info' : 'error',
|
|
592
|
+
required: true
|
|
593
|
+
});
|
|
594
|
+
checks.push({
|
|
595
|
+
name: 'cloud',
|
|
596
|
+
ok: provider.cloud === false && sync.enabled === false,
|
|
597
|
+
detail: 'disabled / local only',
|
|
598
|
+
hint: provider.cloud ? 'Disable cloud provider for this local-first proof.' : undefined,
|
|
599
|
+
severity: provider.cloud === false && sync.enabled === false ? 'info' : 'error',
|
|
600
|
+
required: true
|
|
601
|
+
});
|
|
602
|
+
} else {
|
|
603
|
+
checks.push({
|
|
604
|
+
name: 'engine',
|
|
605
|
+
ok: false,
|
|
606
|
+
detail: 'skipped because node/sqlite/memory-root preflight did not pass',
|
|
607
|
+
hint: 'Fix the failed preflight checks above, then rerun ihow-memory doctor.',
|
|
608
|
+
severity: 'warning',
|
|
609
|
+
required: false
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
ok: checks.every((check)=>check.ok || check.required === false),
|
|
614
|
+
checks,
|
|
615
|
+
status
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
async function packageInfo() {
|
|
619
|
+
try {
|
|
620
|
+
const raw = await fs.readFile(path.join(packageDir(), 'package.json'), 'utf8');
|
|
621
|
+
const parsed = JSON.parse(raw);
|
|
622
|
+
return {
|
|
623
|
+
name: parsed.name || 'ihow-memory-core',
|
|
624
|
+
version: parsed.version || 'unknown'
|
|
625
|
+
};
|
|
626
|
+
} catch {
|
|
627
|
+
return {
|
|
628
|
+
name: 'ihow-memory-core',
|
|
629
|
+
version: 'unknown'
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async function diagnosticReport(result, options = {}) {
|
|
634
|
+
const sanitized = sanitizeDoctorResult(result, options);
|
|
635
|
+
const info = await packageInfo();
|
|
636
|
+
const provider = sanitized.status?.provider || {};
|
|
637
|
+
const sync = sanitized.status?.sync || {};
|
|
638
|
+
return {
|
|
639
|
+
schema: 'ihow-memory-diagnostics-v1',
|
|
640
|
+
diagnosticId: crypto.randomUUID(),
|
|
641
|
+
generatedAt: new Date().toISOString(),
|
|
642
|
+
package: info,
|
|
643
|
+
runtime: options.runtime || 'not-selected',
|
|
644
|
+
environment: {
|
|
645
|
+
node: process.versions.node,
|
|
646
|
+
platform: process.platform,
|
|
647
|
+
arch: process.arch
|
|
648
|
+
},
|
|
649
|
+
localOnly: {
|
|
650
|
+
cloud: provider.cloud === false ? 'disabled' : 'unknown',
|
|
651
|
+
sync: sync.enabled === false ? 'disabled' : 'unknown',
|
|
652
|
+
telemetry: await telemetry.isEnabled() ? 'opt-in (on)' : 'off (default)'
|
|
653
|
+
},
|
|
654
|
+
checks: sanitized.checks,
|
|
655
|
+
status: sanitized.status ? {
|
|
656
|
+
workspace: sanitized.status.workspace || {},
|
|
657
|
+
index: sanitized.status.index || {},
|
|
658
|
+
provider,
|
|
659
|
+
sync
|
|
660
|
+
} : undefined,
|
|
661
|
+
redaction: {
|
|
662
|
+
paths: 'redacted',
|
|
663
|
+
secrets: 'redacted',
|
|
664
|
+
fullMemoryContent: 'omitted'
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function githubIssueUrl(body) {
|
|
669
|
+
const url = new URL('https://github.com/iHow1/ihow-memory-core/issues/new');
|
|
670
|
+
url.searchParams.set('title', '[Activation] ');
|
|
671
|
+
url.searchParams.set('body', body);
|
|
672
|
+
return url.toString();
|
|
673
|
+
}
|
|
674
|
+
async function feedbackTemplate(result, options = {}) {
|
|
675
|
+
const report = await diagnosticReport(result, options);
|
|
676
|
+
const body = `## What happened
|
|
677
|
+
[文案待 Commander]
|
|
678
|
+
|
|
679
|
+
## What I expected
|
|
680
|
+
[文案待 Commander]
|
|
681
|
+
|
|
682
|
+
## Steps to reproduce
|
|
683
|
+
1. \`npx ihow-memory init\`
|
|
684
|
+
2. \`ihow-memory doctor\`
|
|
685
|
+
3. [文案待 Commander]
|
|
686
|
+
|
|
687
|
+
## Runtime
|
|
688
|
+
- Runtime: ${options.runtime || 'not selected'}
|
|
689
|
+
- Node: ${process.versions.node}
|
|
690
|
+
- Package: ${report.package.name}@${report.package.version}
|
|
691
|
+
|
|
692
|
+
## Redacted diagnostics
|
|
693
|
+
\`\`\`json
|
|
694
|
+
${JSON.stringify(report, null, 2)}
|
|
695
|
+
\`\`\`
|
|
696
|
+
`;
|
|
697
|
+
return {
|
|
698
|
+
body,
|
|
699
|
+
url: githubIssueUrl(body)
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
async function resetSpace(options) {
|
|
703
|
+
if (!options.space) throw new Error('reset_requires_space');
|
|
704
|
+
if (options.memoryRoot) throw new Error('reset_managed_space_only_pass_root_and_space');
|
|
705
|
+
const workspace = resolveWorkspace(options);
|
|
706
|
+
await fs.rm(workspace.spaceDir, {
|
|
707
|
+
recursive: true,
|
|
708
|
+
force: true
|
|
709
|
+
});
|
|
710
|
+
return {
|
|
711
|
+
ok: true,
|
|
712
|
+
reset: {
|
|
713
|
+
space: workspace.space,
|
|
714
|
+
removed: workspace.spaceDir
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
async function runProof(options) {
|
|
719
|
+
const root = options.root ? path.resolve(options.root) : await fs.mkdtemp(path.join(os.tmpdir(), 'ihow-memory-proof-cli-'));
|
|
720
|
+
const space = options.space || 'proof-local';
|
|
721
|
+
const core = await openCore({
|
|
722
|
+
...options,
|
|
723
|
+
root,
|
|
724
|
+
space
|
|
725
|
+
});
|
|
726
|
+
const marker = `blue-copper-river-${Date.now()}`;
|
|
727
|
+
const initialStatus = await core.status();
|
|
728
|
+
const candidate = await core.write_candidate({
|
|
729
|
+
title: 'agent-a-proof-memory',
|
|
730
|
+
text: `Agent A proof memory marker ${marker}. Local-only citation and audit demo.`,
|
|
731
|
+
sourceAgent: 'agent-a',
|
|
732
|
+
metadata: {
|
|
733
|
+
proof: 'ToC-1B',
|
|
734
|
+
cloud: false,
|
|
735
|
+
model: null
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
const promoted = await core.promote(candidate.path, {
|
|
739
|
+
scope: 'proof',
|
|
740
|
+
title: 'agent-a-proof-memory'
|
|
741
|
+
});
|
|
742
|
+
const agentB = await openCore({
|
|
743
|
+
...options,
|
|
744
|
+
root,
|
|
745
|
+
space
|
|
746
|
+
});
|
|
747
|
+
const hits = await agentB.search(marker, {
|
|
748
|
+
limit: 5
|
|
749
|
+
});
|
|
750
|
+
if (hits.length === 0) throw new Error('proof_search_miss');
|
|
751
|
+
const read = await agentB.read(hits[0].path);
|
|
752
|
+
const finalStatus = await agentB.status();
|
|
753
|
+
const audit = await latestAuditSummary(agentB.workspace.eventsDir);
|
|
754
|
+
const result = {
|
|
755
|
+
ok: true,
|
|
756
|
+
cloud: 'disabled / local only',
|
|
757
|
+
workspace: {
|
|
758
|
+
root,
|
|
759
|
+
space,
|
|
760
|
+
path: agentB.workspace.spaceDir
|
|
761
|
+
},
|
|
762
|
+
initialStatus: {
|
|
763
|
+
provider: initialStatus.provider,
|
|
764
|
+
index: initialStatus.index
|
|
765
|
+
},
|
|
766
|
+
agentA: {
|
|
767
|
+
candidate,
|
|
768
|
+
promoted
|
|
769
|
+
},
|
|
770
|
+
agentB: {
|
|
771
|
+
query: marker,
|
|
772
|
+
hit: hits[0],
|
|
773
|
+
read: {
|
|
774
|
+
path: read.path,
|
|
775
|
+
citation: read.citation,
|
|
776
|
+
containsMarker: read.content.includes(marker)
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
audit,
|
|
780
|
+
finalStatus: {
|
|
781
|
+
provider: finalStatus.provider,
|
|
782
|
+
index: finalStatus.index
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
if (!options.root && process.env.IHOW_MEMORY_KEEP_PROOF !== '1') {
|
|
786
|
+
await fs.rm(root, {
|
|
787
|
+
recursive: true,
|
|
788
|
+
force: true
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
return result;
|
|
792
|
+
}
|
|
793
|
+
async function maybeAskTelemetry() {
|
|
794
|
+
if (await telemetry.hasAsked()) return;
|
|
795
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
796
|
+
console.log('(想匿名帮我们改进? 跑 `ihow-memory telemetry on` —— 只报使用、绝不含记忆内容)');
|
|
797
|
+
await telemetry.markAsked();
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const readline = await import('node:readline');
|
|
801
|
+
const rl = readline.createInterface({
|
|
802
|
+
input: process.stdin,
|
|
803
|
+
output: process.stdout
|
|
804
|
+
});
|
|
805
|
+
const answer = await new Promise((resolve)=>{
|
|
806
|
+
rl.question('\n帮我们改进?(可选)\n ✓ 只报: 何时用了 · 接哪个 agent · 版本 · 报错类型\n ✗ 绝不报: 你的记忆 / 文件 / 项目 — 一字不传\n 随时关: ihow-memory telemetry off\n 参与匿名上报? [y/N] › ', (a)=>resolve(a));
|
|
807
|
+
});
|
|
808
|
+
rl.close();
|
|
809
|
+
const yes = /^y(es)?$/i.test(answer.trim());
|
|
810
|
+
await telemetry.setEnabled(yes);
|
|
811
|
+
console.log(yes ? '✓ 已开启,谢谢!(随时 `ihow-memory telemetry off` 关闭)' : '已跳过,遥测保持关闭。');
|
|
812
|
+
}
|
|
813
|
+
async function main() {
|
|
814
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
815
|
+
const { command, options, rest } = parsed;
|
|
816
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
817
|
+
help();
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
if (command === 'init') {
|
|
821
|
+
const workspace = await ensureWorkspace(resolveWorkspace(options));
|
|
822
|
+
const runtimeDir = await installRuntimeBundle(workspace);
|
|
823
|
+
const snippet = runtimeConfigSnippet(workspace, options.runtime);
|
|
824
|
+
const result = {
|
|
825
|
+
ok: true,
|
|
826
|
+
workspace: {
|
|
827
|
+
root: workspace.root,
|
|
828
|
+
space: workspace.space,
|
|
829
|
+
path: workspace.spaceDir,
|
|
830
|
+
mode: workspace.mode,
|
|
831
|
+
memoryRoot: workspace.memoryDir
|
|
832
|
+
},
|
|
833
|
+
runtime: options.runtime || 'generic',
|
|
834
|
+
runtimeDir,
|
|
835
|
+
backupBeforeWrite: initBackupGuidance(options.runtime),
|
|
836
|
+
mcpConfig: snippet
|
|
837
|
+
};
|
|
838
|
+
if (options.json) printJson(result);
|
|
839
|
+
else {
|
|
840
|
+
console.log('cloud: disabled / local only');
|
|
841
|
+
console.log(`initialized: ${workspace.spaceDir}`);
|
|
842
|
+
console.log(`mode: ${workspace.mode}`);
|
|
843
|
+
console.log(`memory root: ${workspace.memoryDir}`);
|
|
844
|
+
console.log(`runtime bundle: ${runtimeDir}`);
|
|
845
|
+
console.log(`backup first: ${result.backupBeforeWrite}`);
|
|
846
|
+
printRuntimeSnippet(snippet, options.runtime);
|
|
847
|
+
}
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (command === 'connect') {
|
|
851
|
+
if (!options.runtime) {
|
|
852
|
+
console.error('connect requires --runtime claude-code|codex|cursor');
|
|
853
|
+
process.exitCode = 1;
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const workspace = await ensureWorkspace(resolveWorkspace(options));
|
|
857
|
+
if (!options.dryRun) await installRuntimeBundle(workspace);
|
|
858
|
+
const result = await connectRuntime(workspace, options.runtime, {
|
|
859
|
+
dryRun: options.dryRun
|
|
860
|
+
});
|
|
861
|
+
if (options.json) printJson(result);
|
|
862
|
+
else {
|
|
863
|
+
console.log('cloud: disabled / local only');
|
|
864
|
+
if (result.dryRun) {
|
|
865
|
+
const where = result.method === 'direct-json' ? String(result.target) : `${result.method} (already present: ${result.alreadyExists})`;
|
|
866
|
+
console.log(`[dry-run] would register mcpServers.ihow-memory via ${where}`);
|
|
867
|
+
} else {
|
|
868
|
+
console.log(`✓ connected ${runtimeLabel(options.runtime)} → iHow Memory`);
|
|
869
|
+
console.log(`method: ${result.method}`);
|
|
870
|
+
if (result.target) console.log(`target: ${result.target}`);
|
|
871
|
+
if (result.backup) console.log(`backup: ${result.backup}`);
|
|
872
|
+
if (result.replaced) console.log('(replaced an existing ihow-memory entry)');
|
|
873
|
+
console.log(`Restart ${runtimeLabel(options.runtime)} to load the memory tools.`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
if (!result.dryRun) {
|
|
877
|
+
await telemetry.track('connect', {
|
|
878
|
+
runtime: options.runtime
|
|
879
|
+
});
|
|
880
|
+
await maybeAskTelemetry();
|
|
881
|
+
}
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
if (command === 'status') {
|
|
885
|
+
const core = await openCore(options);
|
|
886
|
+
const status = await core.status();
|
|
887
|
+
if (options.json) printJson(status);
|
|
888
|
+
else {
|
|
889
|
+
console.log(`workspace: ${status.workspace.path}`);
|
|
890
|
+
console.log(`space: ${status.workspace.space}`);
|
|
891
|
+
console.log(`mode: ${status.workspace.mode}`);
|
|
892
|
+
console.log(`memory root: ${status.workspace.memoryRoot}`);
|
|
893
|
+
console.log(`provider: ${status.provider.id} (ready=${status.provider.ready}, cloud=${status.provider.cloud}, model=${status.provider.model})`);
|
|
894
|
+
if (status.provider.fallback) {
|
|
895
|
+
console.log(`fallback: ${status.provider.fallbackFrom} -> fts (${status.provider.lastError})`);
|
|
896
|
+
}
|
|
897
|
+
console.log(`index: ${status.index.status}, documents=${status.index.documents}`);
|
|
898
|
+
console.log(`index path: ${status.index.path}`);
|
|
899
|
+
console.log(`sync: enabled=${status.sync.enabled}`);
|
|
900
|
+
}
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (command === 'doctor') {
|
|
904
|
+
const result = await doctor(options);
|
|
905
|
+
const output = options.shareDiagnostics ? await diagnosticReport(result, options) : result;
|
|
906
|
+
if (options.json || options.shareDiagnostics) printJson(output);
|
|
907
|
+
else {
|
|
908
|
+
console.log(`doctor: ${result.ok ? 'ok' : 'failed'}`);
|
|
909
|
+
console.log('cloud: disabled / local only');
|
|
910
|
+
for (const check of result.checks){
|
|
911
|
+
const label = check.ok ? 'ok' : check.required === false ? 'action' : 'fail';
|
|
912
|
+
console.log(`- ${label} ${check.name}: ${check.detail}`);
|
|
913
|
+
if (check.hint) console.log(` hint: ${check.hint}`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (command === 'feedback') {
|
|
920
|
+
const result = await doctor(options);
|
|
921
|
+
const feedback = await feedbackTemplate(result, options);
|
|
922
|
+
if (options.json) printJson(feedback);
|
|
923
|
+
else {
|
|
924
|
+
console.log('No issue was submitted. Review the redacted template, then open the URL yourself.');
|
|
925
|
+
console.log('\nGitHub issue URL:');
|
|
926
|
+
console.log(feedback.url);
|
|
927
|
+
console.log('\nPrefilled issue body:');
|
|
928
|
+
console.log(feedback.body);
|
|
929
|
+
}
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (command === 'telemetry') {
|
|
933
|
+
const sub = process.argv[3];
|
|
934
|
+
if (sub === 'on') {
|
|
935
|
+
await telemetry.setEnabled(true);
|
|
936
|
+
console.log('✓ 匿名遥测已开启(只报使用、绝不含记忆内容)。');
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (sub === 'off') {
|
|
940
|
+
await telemetry.setEnabled(false);
|
|
941
|
+
console.log('✓ 匿名遥测已关闭。');
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
const st = await telemetry.status();
|
|
945
|
+
if (options.json) printJson(st);
|
|
946
|
+
else {
|
|
947
|
+
console.log(`telemetry: ${st.enabled ? 'on' : 'off (default)'}`);
|
|
948
|
+
console.log(`collects: ${st.collects.join(' · ')}`);
|
|
949
|
+
console.log(`never collects: ${st.neverCollects.join(' · ')}`);
|
|
950
|
+
console.log(`endpoint: ${st.endpoint}`);
|
|
951
|
+
}
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
if (command === 'reset') {
|
|
955
|
+
const result = await resetSpace(options);
|
|
956
|
+
if (options.json) printJson(result);
|
|
957
|
+
else {
|
|
958
|
+
console.log(`reset complete: ${result.reset.space}`);
|
|
959
|
+
console.log(`removed demo workspace: ${result.reset.removed}`);
|
|
960
|
+
}
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (command === 'proof') {
|
|
964
|
+
const result = await runProof(options);
|
|
965
|
+
if (options.json) printJson(result);
|
|
966
|
+
else {
|
|
967
|
+
console.log('iHow Memory 10-second proof');
|
|
968
|
+
console.log('cloud: disabled / local only');
|
|
969
|
+
console.log(`workspace: ${result.workspace.path}`);
|
|
970
|
+
console.log(`agent A wrote candidate: ${result.agentA.candidate.path}`);
|
|
971
|
+
console.log(`agent A promoted: ${result.agentA.promoted.path}`);
|
|
972
|
+
const hit = result.agentB.hit;
|
|
973
|
+
const citation = hit.citation;
|
|
974
|
+
console.log(`agent B search hit: ${hit.path}`);
|
|
975
|
+
console.log(`citation: ${citation.path}`);
|
|
976
|
+
console.log(`source: ${hit.source}`);
|
|
977
|
+
if (hit.fallback) {
|
|
978
|
+
const fallback = hit.fallback;
|
|
979
|
+
console.log(`fallback: ${fallback.from} -> ${fallback.to} (${fallback.reason})`);
|
|
980
|
+
}
|
|
981
|
+
console.log(`read contains marker: ${result.agentB.read.containsMarker}`);
|
|
982
|
+
const audit = result.audit;
|
|
983
|
+
const event = audit?.event;
|
|
984
|
+
console.log(`audit event: ${event?.type || 'missing'} ${event?.id || ''}`);
|
|
985
|
+
console.log('PASS proof: A write -> promote -> B search/read with citation and audit');
|
|
986
|
+
}
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (command === 'console') {
|
|
990
|
+
const { createConsoleServer } = await import('./http/console.js');
|
|
991
|
+
const argv = process.argv.slice(2);
|
|
992
|
+
const hostIdx = argv.indexOf('--host');
|
|
993
|
+
const portIdx = argv.indexOf('--port');
|
|
994
|
+
const host = hostIdx >= 0 && argv[hostIdx + 1] ? argv[hostIdx + 1] : '127.0.0.1';
|
|
995
|
+
const port = portIdx >= 0 && argv[portIdx + 1] ? Number(argv[portIdx + 1]) : 8788;
|
|
996
|
+
const server = await createConsoleServer(options);
|
|
997
|
+
server.listen(port, host, ()=>{
|
|
998
|
+
console.log('cloud: disabled / local only');
|
|
999
|
+
console.log(`iHow Memory console (read-only): http://${host}:${port}`);
|
|
1000
|
+
console.log('Open the URL in a browser. Ctrl+C to stop.');
|
|
1001
|
+
});
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const core = await openCore(options);
|
|
1005
|
+
if (command === 'reindex') {
|
|
1006
|
+
const documents = await core.rebuild();
|
|
1007
|
+
const status = await core.status();
|
|
1008
|
+
const result = {
|
|
1009
|
+
ok: true,
|
|
1010
|
+
documents,
|
|
1011
|
+
index: status.index
|
|
1012
|
+
};
|
|
1013
|
+
if (options.json) printJson(result);
|
|
1014
|
+
else {
|
|
1015
|
+
console.log(`reindexed: documents=${documents}`);
|
|
1016
|
+
console.log(`index: ${status.index.path}`);
|
|
1017
|
+
}
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (command === 'search') {
|
|
1021
|
+
const query = rest.join(' ');
|
|
1022
|
+
printJson(await core.search(query, {
|
|
1023
|
+
limit: options.limit
|
|
1024
|
+
}));
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
if (command === 'read') {
|
|
1028
|
+
printJson(await core.read(rest[0]));
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (command === 'write-candidate') {
|
|
1032
|
+
printJson(await core.write_candidate({
|
|
1033
|
+
text: rest.join(' '),
|
|
1034
|
+
sourceAgent: 'cli'
|
|
1035
|
+
}));
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (command === 'promote') {
|
|
1039
|
+
const candidate = rest[0];
|
|
1040
|
+
const target = {};
|
|
1041
|
+
for(let index = 1; index < rest.length; index += 1){
|
|
1042
|
+
if (rest[index] === '--scope') target.scope = rest[++index];
|
|
1043
|
+
else if (rest[index] === '--title') target.title = rest[++index];
|
|
1044
|
+
}
|
|
1045
|
+
printJson(await core.promote(candidate, target));
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (command === 'durable-promote') {
|
|
1049
|
+
const candidate = rest[0];
|
|
1050
|
+
const target = {};
|
|
1051
|
+
for(let index = 1; index < rest.length; index += 1){
|
|
1052
|
+
if (rest[index] === '--scope') target.scope = rest[++index];
|
|
1053
|
+
else if (rest[index] === '--title') target.title = rest[++index];
|
|
1054
|
+
else if (rest[index] === '--path') target.path = rest[++index];
|
|
1055
|
+
}
|
|
1056
|
+
printJson(await core.durable_promote(candidate, {
|
|
1057
|
+
dryRun: options.dryRun,
|
|
1058
|
+
realWrite: options.realWrite,
|
|
1059
|
+
actor: options.actor || 'cli',
|
|
1060
|
+
target
|
|
1061
|
+
}));
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
help();
|
|
1065
|
+
process.exitCode = 1;
|
|
1066
|
+
}
|
|
1067
|
+
main().catch((error)=>{
|
|
1068
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1069
|
+
if (message === 'reset_requires_space') {
|
|
1070
|
+
console.error('reset requires an explicit demo space: ihow-memory reset --space <id> [--root <dir>]');
|
|
1071
|
+
} else if (message === 'reset_managed_space_only_pass_root_and_space') {
|
|
1072
|
+
console.error('reset only removes managed demo spaces. Use --root and --space; existing --memory-root data is never deleted.');
|
|
1073
|
+
} else if (message === 'unsupported_runtime_use_claude-code_codex_or_cursor') {
|
|
1074
|
+
console.error('unsupported runtime. Use --runtime claude-code, --runtime codex, or --runtime cursor.');
|
|
1075
|
+
} else if (message.startsWith('sqlite_unavailable:')) {
|
|
1076
|
+
console.error('SQLite is unavailable. Install Node >= 22.12 with node:sqlite support, then rerun ihow-memory doctor.');
|
|
1077
|
+
} else {
|
|
1078
|
+
console.error(friendlyError(error));
|
|
1079
|
+
}
|
|
1080
|
+
process.exitCode = 1;
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
//# sourceURL=cli.ts
|