memshare-mcp 0.2.6
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 +21 -0
- package/README.md +243 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +702 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/ui.d.ts +28 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +104 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +6 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +176 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/memory/project.d.ts +15 -0
- package/dist/memory/project.d.ts.map +1 -0
- package/dist/memory/project.js +77 -0
- package/dist/memory/project.js.map +1 -0
- package/dist/memory/redact.d.ts +35 -0
- package/dist/memory/redact.d.ts.map +1 -0
- package/dist/memory/redact.js +275 -0
- package/dist/memory/redact.js.map +1 -0
- package/dist/memory/store.d.ts +75 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +325 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/types.d.ts +165 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +88 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/sharing/bundle.d.ts +36 -0
- package/dist/sharing/bundle.d.ts.map +1 -0
- package/dist/sharing/bundle.js +130 -0
- package/dist/sharing/bundle.js.map +1 -0
- package/dist/sharing/export.d.ts +54 -0
- package/dist/sharing/export.d.ts.map +1 -0
- package/dist/sharing/export.js +77 -0
- package/dist/sharing/export.js.map +1 -0
- package/dist/sharing/import.d.ts +59 -0
- package/dist/sharing/import.d.ts.map +1 -0
- package/dist/sharing/import.js +101 -0
- package/dist/sharing/import.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { checkbox, confirm, input, select } from "@inquirer/prompts";
|
|
4
|
+
import { Command, Option } from "commander";
|
|
5
|
+
import { summarisePII } from "../memory/redact.js";
|
|
6
|
+
import { MemoryStore, normaliseTags, parseDuration, parseTagList, resolveMemoryDir, } from "../memory/store.js";
|
|
7
|
+
import { Mode, Visibility } from "../memory/types.js";
|
|
8
|
+
import { readBundleFile, writeBundleFile } from "../sharing/bundle.js";
|
|
9
|
+
import { buildExportBundle, describeSkipReason, selectForExport, } from "../sharing/export.js";
|
|
10
|
+
import { applyImport, planImport, senderTag } from "../sharing/import.js";
|
|
11
|
+
import { c, fail, formatItemLine, formatPii, heading, info, isInteractive, ok, relativeTime, shortId, table, truncate, warn, } from "./ui.js";
|
|
12
|
+
const VERSION = "0.2.6";
|
|
13
|
+
const program = new Command();
|
|
14
|
+
program
|
|
15
|
+
.name("memshare")
|
|
16
|
+
.description("Peer-to-peer AI memory sharing between users -- with consent.\n" +
|
|
17
|
+
"Your memories are plain JSON files on your machine. Nothing is uploaded anywhere.")
|
|
18
|
+
.version(VERSION, "-v, --version")
|
|
19
|
+
.option("--dir <path>", "memory store location (overrides MEMSHARE_DIR)")
|
|
20
|
+
.showHelpAfterError();
|
|
21
|
+
function store() {
|
|
22
|
+
const dir = program.opts().dir;
|
|
23
|
+
return new MemoryStore(dir ? path.resolve(dir) : resolveMemoryDir());
|
|
24
|
+
}
|
|
25
|
+
/** Most commands are meaningless without a store; say so once, clearly. */
|
|
26
|
+
async function requireStore() {
|
|
27
|
+
const s = store();
|
|
28
|
+
if (!s.exists()) {
|
|
29
|
+
throw new UserError(`No memshare store at ${s.root}.\n Run ${c.bold("memshare init")} to create one.`);
|
|
30
|
+
}
|
|
31
|
+
return s;
|
|
32
|
+
}
|
|
33
|
+
class UserError extends Error {
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------- init
|
|
36
|
+
program
|
|
37
|
+
.command("init")
|
|
38
|
+
.description("create the memory store in ~/.memshare (or $MEMSHARE_DIR)")
|
|
39
|
+
.option("--name <displayName>", "the name shown on bundles you export")
|
|
40
|
+
.addOption(new Option("--mode <mode>", "how memories get saved").choices(Mode.options))
|
|
41
|
+
.option("-y, --yes", "accept defaults, ask nothing")
|
|
42
|
+
.action(async (opts) => {
|
|
43
|
+
const s = store();
|
|
44
|
+
const existed = s.exists();
|
|
45
|
+
const current = await s.readConfig();
|
|
46
|
+
let displayName = opts.name ?? current.displayName;
|
|
47
|
+
let mode = opts.mode ?? current.mode;
|
|
48
|
+
if (!opts.yes && isInteractive() && (!opts.name || !opts.mode)) {
|
|
49
|
+
if (!opts.name) {
|
|
50
|
+
displayName = await input({
|
|
51
|
+
message: "Display name (shown to people you share with):",
|
|
52
|
+
default: displayName === "anonymous" ? guessName() : displayName,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (!opts.mode) {
|
|
56
|
+
mode = await select({
|
|
57
|
+
message: "How should memories get saved?",
|
|
58
|
+
default: mode,
|
|
59
|
+
choices: [
|
|
60
|
+
{
|
|
61
|
+
name: "auto - the AI saves as it learns, always private (recommended)",
|
|
62
|
+
value: "auto",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "suggest - the AI proposes, you approve each one",
|
|
66
|
+
value: "suggest",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "manual - nothing is saved unless you ask for it",
|
|
70
|
+
value: "manual",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const config = await s.init({ displayName: displayName.trim() || "anonymous", mode });
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(ok(existed ? `Updated ${c.bold(s.root)}` : `Created ${c.bold(s.root)}`));
|
|
79
|
+
console.log(info(`display name: ${c.bold(config.displayName)}`));
|
|
80
|
+
console.log(info(`mode: ${c.bold(config.mode)}`));
|
|
81
|
+
console.log(info(`PII guard: ${config.autoRedactPII ? "on" : "off"}`));
|
|
82
|
+
console.log();
|
|
83
|
+
console.log(heading("Next:"));
|
|
84
|
+
console.log(` claude mcp add memshare -- npx -y memshare-mcp serve`);
|
|
85
|
+
console.log(` memshare add "I prefer TypeScript" --tags preferences --visibility shareable`);
|
|
86
|
+
console.log();
|
|
87
|
+
});
|
|
88
|
+
// ---------------------------------------------------------------- add
|
|
89
|
+
program
|
|
90
|
+
.command("add")
|
|
91
|
+
.argument("<content>", "the fact to remember, as a standalone sentence")
|
|
92
|
+
.description("add a memory by hand")
|
|
93
|
+
.option("-t, --tags <tags>", "comma-separated tags", collect, [])
|
|
94
|
+
.addOption(new Option("--visibility <visibility>", "private (default) or shareable").choices(Visibility.options))
|
|
95
|
+
.option("--expires <when>", "forget it after e.g. 7d, 12h, or an ISO date")
|
|
96
|
+
.option("--tool <name>", "which tool this came from", "cli")
|
|
97
|
+
.action(async (content, opts) => {
|
|
98
|
+
const s = await requireStore();
|
|
99
|
+
const item = await s.add({
|
|
100
|
+
content,
|
|
101
|
+
tags: parseTagList(opts.tags),
|
|
102
|
+
...(opts.visibility ? { visibility: opts.visibility } : {}),
|
|
103
|
+
...(opts.expires ? { expiresAt: parseDuration(opts.expires) } : {}),
|
|
104
|
+
source: { tool: opts.tool },
|
|
105
|
+
});
|
|
106
|
+
console.log(ok(`Saved ${c.dim(shortId(item.id))} ${c.bold(truncate(item.content, 60))}`));
|
|
107
|
+
console.log(info(`${item.tags.length > 0 ? item.tags.join(", ") : "no tags"} | ${item.visibility}` +
|
|
108
|
+
(item.expiresAt ? ` | expires ${relativeTime(item.expiresAt)}` : "")));
|
|
109
|
+
if (item.visibility === "private") {
|
|
110
|
+
console.log(info(`Private items are never exported. Use --visibility shareable to share.`));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
// ---------------------------------------------------------------- list
|
|
114
|
+
program
|
|
115
|
+
.command("list")
|
|
116
|
+
.alias("ls")
|
|
117
|
+
.description("show what is in your memory store")
|
|
118
|
+
.option("-t, --tags <tags>", "only items with any of these tags", collect, [])
|
|
119
|
+
.addOption(new Option("--visibility <visibility>", "filter").choices(Visibility.options))
|
|
120
|
+
.option("-q, --query <text>", "substring match on content and tags")
|
|
121
|
+
.option("--from <tool>", "only items written by this tool, e.g. claude, chatgpt")
|
|
122
|
+
.option("-n, --limit <n>", "maximum items to show", (v) => Number.parseInt(v, 10))
|
|
123
|
+
.option("--json", "raw JSON output")
|
|
124
|
+
.option("--all", "include expired items")
|
|
125
|
+
.action(async (opts) => {
|
|
126
|
+
const s = await requireStore();
|
|
127
|
+
const items = await s.list({
|
|
128
|
+
tags: parseTagList(opts.tags),
|
|
129
|
+
...(opts.visibility ? { visibility: opts.visibility } : {}),
|
|
130
|
+
...(opts.query ? { query: opts.query } : {}),
|
|
131
|
+
...(opts.from ? { tool: opts.from } : {}),
|
|
132
|
+
...(opts.limit !== undefined ? { limit: opts.limit } : {}),
|
|
133
|
+
...(opts.all ? { includeExpired: true } : {}),
|
|
134
|
+
});
|
|
135
|
+
if (opts.json) {
|
|
136
|
+
console.log(JSON.stringify(items, null, 2));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (items.length === 0) {
|
|
140
|
+
console.log(info("No memories matched."));
|
|
141
|
+
await printPendingHint(s);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
console.log();
|
|
145
|
+
console.log(table(["ID", "MEMORY", "TAGS", "VISIBILITY", "AGE"], items.map((i) => [
|
|
146
|
+
shortId(i.id),
|
|
147
|
+
truncate(i.content, 52),
|
|
148
|
+
i.tags.join(", "),
|
|
149
|
+
i.visibility,
|
|
150
|
+
relativeTime(i.createdAt),
|
|
151
|
+
]), [8, 52, 24, 10, 8]));
|
|
152
|
+
console.log();
|
|
153
|
+
const shareable = items.filter((i) => i.visibility === "shareable").length;
|
|
154
|
+
console.log(info(`${items.length} item(s), ${shareable} shareable.`));
|
|
155
|
+
await printPendingHint(s);
|
|
156
|
+
});
|
|
157
|
+
/**
|
|
158
|
+
* In suggest mode nothing the AI proposes reaches the store until the user
|
|
159
|
+
* reviews it. A user who never discovers `review` ends up with an empty store
|
|
160
|
+
* and no idea why, so say it wherever they are already looking.
|
|
161
|
+
*/
|
|
162
|
+
async function printPendingHint(s) {
|
|
163
|
+
const pending = await s.readSuggestions();
|
|
164
|
+
if (pending.length === 0)
|
|
165
|
+
return;
|
|
166
|
+
console.log(warn(`${pending.length} suggestion(s) waiting for you — nothing is saved until you run ` +
|
|
167
|
+
`${c.bold("memshare review")}.`));
|
|
168
|
+
}
|
|
169
|
+
// ---------------------------------------------------------------- recall
|
|
170
|
+
program
|
|
171
|
+
.command("recall")
|
|
172
|
+
.description("print memories as plain text, ready to paste into any AI tool")
|
|
173
|
+
.option("-t, --tags <tags>", "only items with any of these tags", collect, [])
|
|
174
|
+
.option("-q, --query <text>", "substring match on content and tags")
|
|
175
|
+
.option("-n, --limit <n>", "maximum items", (v) => Number.parseInt(v, 10), 30)
|
|
176
|
+
.action(async (opts) => {
|
|
177
|
+
const s = await requireStore();
|
|
178
|
+
const items = await s.list({
|
|
179
|
+
tags: parseTagList(opts.tags),
|
|
180
|
+
...(opts.query ? { query: opts.query } : {}),
|
|
181
|
+
limit: opts.limit,
|
|
182
|
+
});
|
|
183
|
+
if (items.length === 0) {
|
|
184
|
+
console.log("(no relevant memories)");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
console.log("What you know about me and my work:");
|
|
188
|
+
for (const item of items) {
|
|
189
|
+
const tags = item.tags.length > 0 ? ` (${item.tags.join(", ")})` : "";
|
|
190
|
+
console.log(`- ${item.content}${tags}`);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
// ---------------------------------------------------------------- tags
|
|
194
|
+
program
|
|
195
|
+
.command("tags")
|
|
196
|
+
.description("list every tag, or merge one into another")
|
|
197
|
+
.option("--rename <from>", "the tag to replace (use with --to)")
|
|
198
|
+
.option("--to <to>", "the tag to replace it with")
|
|
199
|
+
.action(async (opts) => {
|
|
200
|
+
const s = await requireStore();
|
|
201
|
+
if (opts.rename || opts.to) {
|
|
202
|
+
if (!opts.rename || !opts.to) {
|
|
203
|
+
throw new UserError("Both --rename and --to are needed.");
|
|
204
|
+
}
|
|
205
|
+
const [from] = normaliseTags([opts.rename]);
|
|
206
|
+
const [to] = normaliseTags([opts.to]);
|
|
207
|
+
if (!from || !to)
|
|
208
|
+
throw new UserError("Tag names cannot be empty.");
|
|
209
|
+
if (from === to)
|
|
210
|
+
throw new UserError("Those are the same tag.");
|
|
211
|
+
// Near-duplicate tags ("project-x" / "projectx") silently break sharing,
|
|
212
|
+
// because an export filtered on one simply misses the other.
|
|
213
|
+
const affected = (await s.all()).filter((i) => i.tags.includes(from));
|
|
214
|
+
if (affected.length === 0) {
|
|
215
|
+
console.log(info(`No memories carry the tag "${from}".`));
|
|
216
|
+
process.exitCode = 1;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
for (const item of affected) {
|
|
220
|
+
const tags = item.tags.map((t) => (t === from ? to : t));
|
|
221
|
+
await s.update(item.id, { tags });
|
|
222
|
+
}
|
|
223
|
+
console.log(ok(`Renamed ${c.bold(from)} to ${c.bold(to)} on ${affected.length} item(s).`));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const tags = await s.listTags();
|
|
227
|
+
console.log(tags.length === 0 ? info("No tags yet.") : tags.join("\n"));
|
|
228
|
+
});
|
|
229
|
+
// ---------------------------------------------------------------- mark
|
|
230
|
+
program
|
|
231
|
+
.command("mark")
|
|
232
|
+
.argument("[ids...]", "memory ids (full or the short form shown by `list`)")
|
|
233
|
+
.description("promote memories to shareable, or pull them back to private")
|
|
234
|
+
.option("-t, --tags <tags>", "mark everything carrying any of these tags", collect, [])
|
|
235
|
+
.option("-q, --query <text>", "mark everything matching this text")
|
|
236
|
+
.option("--shareable", "allow these memories to be included in an export")
|
|
237
|
+
.option("--private", "never export these memories (the default state)")
|
|
238
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
239
|
+
.action(async (ids, opts) => {
|
|
240
|
+
const s = await requireStore();
|
|
241
|
+
if (opts.shareable === opts.private) {
|
|
242
|
+
throw new UserError("Choose one: --shareable or --private.");
|
|
243
|
+
}
|
|
244
|
+
const visibility = opts.shareable ? "shareable" : "private";
|
|
245
|
+
const tags = parseTagList(opts.tags);
|
|
246
|
+
if (ids.length === 0 && tags.length === 0 && !opts.query) {
|
|
247
|
+
throw new UserError("Nothing selected. Give ids, or narrow with --tags / --query.\n" +
|
|
248
|
+
` e.g. ${c.bold("memshare mark --tags project-x --shareable")}`);
|
|
249
|
+
}
|
|
250
|
+
const all = await s.all();
|
|
251
|
+
const selected = ids.length > 0
|
|
252
|
+
? ids
|
|
253
|
+
.map((given) => all.find((i) => i.id === given || shortId(i.id) === given))
|
|
254
|
+
.filter((i) => i !== undefined)
|
|
255
|
+
: await s.list({
|
|
256
|
+
...(tags.length > 0 ? { tags } : {}),
|
|
257
|
+
...(opts.query ? { query: opts.query } : {}),
|
|
258
|
+
});
|
|
259
|
+
const changing = selected.filter((i) => i.visibility !== visibility);
|
|
260
|
+
if (selected.length === 0) {
|
|
261
|
+
console.log(info("Nothing matched."));
|
|
262
|
+
process.exitCode = 1;
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (changing.length === 0) {
|
|
266
|
+
console.log(info(`All ${selected.length} matching item(s) are already ${visibility}.`));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
console.log();
|
|
270
|
+
console.log(heading(`Marking ${changing.length} item(s) as ${c.bold(visibility)}:`));
|
|
271
|
+
for (const item of changing) {
|
|
272
|
+
console.log(` ${c.dim(shortId(item.id))} ${truncate(item.content, 66)}`);
|
|
273
|
+
}
|
|
274
|
+
console.log();
|
|
275
|
+
// Making things shareable is the consent decision. Confirm it.
|
|
276
|
+
if (visibility === "shareable" && !opts.yes && isInteractive()) {
|
|
277
|
+
const go = await confirm({
|
|
278
|
+
message: `Allow these ${changing.length} item(s) to be included in exports?`,
|
|
279
|
+
default: true,
|
|
280
|
+
});
|
|
281
|
+
if (!go) {
|
|
282
|
+
console.log(info("Cancelled. Nothing changed."));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
for (const item of changing)
|
|
287
|
+
await s.update(item.id, { visibility });
|
|
288
|
+
console.log(ok(`${changing.length} item(s) are now ${visibility}.`));
|
|
289
|
+
if (visibility === "shareable") {
|
|
290
|
+
console.log(info(`They are still on your machine only. Sharing takes an explicit ${c.bold("memshare export")}.`));
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
// ---------------------------------------------------------------- forget
|
|
294
|
+
program
|
|
295
|
+
.command("forget")
|
|
296
|
+
.argument("<ids...>", "memory ids (full or the short form shown by `list`)")
|
|
297
|
+
.description("delete memories")
|
|
298
|
+
.action(async (ids) => {
|
|
299
|
+
const s = await requireStore();
|
|
300
|
+
const all = await s.all();
|
|
301
|
+
let removed = 0;
|
|
302
|
+
for (const given of ids) {
|
|
303
|
+
const match = all.find((i) => i.id === given || shortId(i.id) === given);
|
|
304
|
+
if (!match) {
|
|
305
|
+
console.log(fail(`No memory matches ${c.bold(given)}`));
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (await s.remove(match.id)) {
|
|
309
|
+
removed += 1;
|
|
310
|
+
console.log(ok(`Forgot ${c.dim(shortId(match.id))} ${truncate(match.content, 56)}`));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (removed === 0)
|
|
314
|
+
process.exitCode = 1;
|
|
315
|
+
});
|
|
316
|
+
// ---------------------------------------------------------------- prune
|
|
317
|
+
program
|
|
318
|
+
.command("prune")
|
|
319
|
+
.description("delete memories whose expiry has passed")
|
|
320
|
+
.action(async () => {
|
|
321
|
+
const s = await requireStore();
|
|
322
|
+
const removed = await s.pruneExpired();
|
|
323
|
+
console.log(removed === 0 ? info("Nothing expired.") : ok(`Removed ${removed} expired item(s).`));
|
|
324
|
+
});
|
|
325
|
+
// ---------------------------------------------------------------- review
|
|
326
|
+
program
|
|
327
|
+
.command("review")
|
|
328
|
+
.description("approve or reject what the AI suggested (suggest mode)")
|
|
329
|
+
.option("-y, --yes", "accept every suggestion without asking")
|
|
330
|
+
.option("--clear", "reject every suggestion")
|
|
331
|
+
.addOption(new Option("--visibility <visibility>", "visibility for accepted items").choices(Visibility.options))
|
|
332
|
+
.action(async (opts) => {
|
|
333
|
+
const s = await requireStore();
|
|
334
|
+
const suggestions = await s.readSuggestions();
|
|
335
|
+
if (suggestions.length === 0) {
|
|
336
|
+
console.log(info("No pending suggestions."));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (opts.clear) {
|
|
340
|
+
await s.writeSuggestions([]);
|
|
341
|
+
console.log(ok(`Rejected ${suggestions.length} suggestion(s).`));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
console.log();
|
|
345
|
+
console.log(heading(`${suggestions.length} suggestion(s) waiting:`));
|
|
346
|
+
console.log();
|
|
347
|
+
for (const [i, s2] of suggestions.entries()) {
|
|
348
|
+
console.log(` ${c.dim(String(i + 1).padStart(2))}. ${s2.content}`);
|
|
349
|
+
console.log(` ${s2.tags.length > 0 ? c.cyan(s2.tags.join(", ")) : c.dim("(no tags)")} ${c.dim(`via ${s2.source.tool}, ${relativeTime(s2.suggestedAt)}`)}`);
|
|
350
|
+
}
|
|
351
|
+
console.log();
|
|
352
|
+
let acceptedIds;
|
|
353
|
+
if (opts.yes || !isInteractive()) {
|
|
354
|
+
if (!opts.yes) {
|
|
355
|
+
throw new UserError("Nothing to prompt with (not a terminal). Re-run with --yes to accept all, or --clear to reject all.");
|
|
356
|
+
}
|
|
357
|
+
acceptedIds = suggestions.map((s2) => s2.id);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
acceptedIds = await checkbox({
|
|
361
|
+
message: "Which should be saved? (space to toggle, enter to confirm)",
|
|
362
|
+
pageSize: 15,
|
|
363
|
+
choices: suggestions.map((s2) => ({
|
|
364
|
+
name: `${truncate(s2.content, 70)}${s2.tags.length > 0 ? ` [${s2.tags.join(", ")}]` : ""}`,
|
|
365
|
+
value: s2.id,
|
|
366
|
+
checked: true,
|
|
367
|
+
})),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const visibility = opts.visibility ?? undefined;
|
|
371
|
+
let saved = 0;
|
|
372
|
+
for (const suggestion of suggestions) {
|
|
373
|
+
if (!acceptedIds.includes(suggestion.id))
|
|
374
|
+
continue;
|
|
375
|
+
await s.add({
|
|
376
|
+
content: suggestion.content,
|
|
377
|
+
tags: suggestion.tags,
|
|
378
|
+
...(visibility ? { visibility } : {}),
|
|
379
|
+
confidence: "inferred",
|
|
380
|
+
source: suggestion.source,
|
|
381
|
+
});
|
|
382
|
+
saved += 1;
|
|
383
|
+
}
|
|
384
|
+
// Everything reviewed leaves the queue, accepted or not.
|
|
385
|
+
await s.writeSuggestions([]);
|
|
386
|
+
console.log();
|
|
387
|
+
console.log(ok(`Saved ${saved}, rejected ${suggestions.length - saved}.`));
|
|
388
|
+
});
|
|
389
|
+
// ---------------------------------------------------------------- export
|
|
390
|
+
program
|
|
391
|
+
.command("export")
|
|
392
|
+
.description("export shareable memories as a bundle file")
|
|
393
|
+
.option("-t, --tags <tags>", "only items with any of these tags", collect, [])
|
|
394
|
+
.option("-q, --query <text>", "substring match on content and tags")
|
|
395
|
+
.option("--from <tool>", "only items written by this tool, e.g. chatgpt")
|
|
396
|
+
.option("--for <recipient>", "who this bundle is for (recorded in the bundle)")
|
|
397
|
+
.option("--note <text>", "a note for the recipient")
|
|
398
|
+
.option("--expires <when>", "recipient should refuse it after e.g. 7d, 30d")
|
|
399
|
+
.option("-o, --out <file>", "where to write the bundle")
|
|
400
|
+
.option("--preview", "show what would be exported, write nothing")
|
|
401
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
402
|
+
.option("--include-private", "also export items marked private (asks first)")
|
|
403
|
+
.option("--redact-blocked", "include PII-flagged items with the PII masked out")
|
|
404
|
+
.option("--no-scan", "skip PII detection entirely (not recommended)")
|
|
405
|
+
.action(async (opts) => {
|
|
406
|
+
const s = await requireStore();
|
|
407
|
+
const config = await s.readConfig();
|
|
408
|
+
const selection = await selectForExport(s, {
|
|
409
|
+
tags: parseTagList(opts.tags),
|
|
410
|
+
...(opts.query ? { query: opts.query } : {}),
|
|
411
|
+
...(opts.from ? { tool: opts.from } : {}),
|
|
412
|
+
...(opts.includePrivate ? { includePrivate: true } : {}),
|
|
413
|
+
scanForPii: opts.scan,
|
|
414
|
+
});
|
|
415
|
+
console.log();
|
|
416
|
+
console.log(heading(`Export preview${opts.for ? ` for ${c.bold(opts.for)}` : ""}`));
|
|
417
|
+
console.log();
|
|
418
|
+
if (selection.included.length > 0) {
|
|
419
|
+
console.log(c.green(`Will be included (${selection.included.length}):`));
|
|
420
|
+
for (const candidate of selection.included) {
|
|
421
|
+
console.log(` ${formatItemLine(candidate.item)}`);
|
|
422
|
+
}
|
|
423
|
+
console.log();
|
|
424
|
+
}
|
|
425
|
+
if (selection.blocked.length > 0) {
|
|
426
|
+
console.log(c.yellow(`Blocked -- looks sensitive (${selection.blocked.length}):`));
|
|
427
|
+
for (const candidate of selection.blocked) {
|
|
428
|
+
console.log(` ${formatItemLine(candidate.item)}`);
|
|
429
|
+
console.log(` ${c.yellow("contains:")} ${formatPii(candidate.findings)}`);
|
|
430
|
+
}
|
|
431
|
+
console.log();
|
|
432
|
+
}
|
|
433
|
+
if (selection.skipped.length > 0) {
|
|
434
|
+
const counts = new Map();
|
|
435
|
+
for (const s2 of selection.skipped) {
|
|
436
|
+
const reason = describeSkipReason(s2.reason);
|
|
437
|
+
counts.set(reason, (counts.get(reason) ?? 0) + 1);
|
|
438
|
+
}
|
|
439
|
+
const summary = [...counts.entries()].map(([r, n]) => `${n} ${r}`).join(", ");
|
|
440
|
+
console.log(info(`Not considered: ${summary}.`));
|
|
441
|
+
console.log();
|
|
442
|
+
}
|
|
443
|
+
if (selection.included.length === 0 && selection.blocked.length === 0) {
|
|
444
|
+
console.log(fail("Nothing to export. Only items marked " +
|
|
445
|
+
c.bold("shareable") +
|
|
446
|
+
" are eligible.\n Promote what the AI has already captured with: " +
|
|
447
|
+
c.bold("memshare mark --tags <tags> --shareable")));
|
|
448
|
+
process.exitCode = 1;
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (opts.preview) {
|
|
452
|
+
console.log(info("Preview only. Re-run without --preview to write the bundle."));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const items = selection.included.map((candidate) => candidate.item);
|
|
456
|
+
// Each blocked item gets its own decision -- that is the consent step.
|
|
457
|
+
for (const candidate of selection.blocked) {
|
|
458
|
+
const decision = await decideBlocked(candidate, opts);
|
|
459
|
+
if (decision === "redacted")
|
|
460
|
+
items.push(candidate.redacted);
|
|
461
|
+
else if (decision === "as-is")
|
|
462
|
+
items.push(candidate.item);
|
|
463
|
+
}
|
|
464
|
+
if (items.length === 0) {
|
|
465
|
+
console.log(fail("Everything was excluded. Nothing written."));
|
|
466
|
+
process.exitCode = 1;
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (!opts.yes && isInteractive()) {
|
|
470
|
+
const go = await confirm({
|
|
471
|
+
message: `Export ${items.length} item(s)${opts.for ? ` for ${opts.for}` : ""}?`,
|
|
472
|
+
default: true,
|
|
473
|
+
});
|
|
474
|
+
if (!go) {
|
|
475
|
+
console.log(info("Cancelled. Nothing written."));
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const bundle = buildExportBundle(items, {
|
|
480
|
+
exportedBy: config.displayName,
|
|
481
|
+
...(opts.note ? { description: opts.note } : {}),
|
|
482
|
+
...(opts.for ? { exportedFor: opts.for } : {}),
|
|
483
|
+
...(opts.expires ? { expiresAt: parseDuration(opts.expires) } : {}),
|
|
484
|
+
});
|
|
485
|
+
const destination = opts.out ? path.resolve(opts.out) : s.bundlesDir;
|
|
486
|
+
const written = await writeBundleFile(bundle, destination);
|
|
487
|
+
console.log();
|
|
488
|
+
console.log(ok(`Wrote ${c.bold(written)}`));
|
|
489
|
+
console.log(info(`${items.length} item(s), hash ${bundle.metadata.contentHash.slice(0, 12)}...`));
|
|
490
|
+
if (bundle.metadata.expiresAt) {
|
|
491
|
+
console.log(info(`Expires ${relativeTime(bundle.metadata.expiresAt)}.`));
|
|
492
|
+
}
|
|
493
|
+
console.log();
|
|
494
|
+
console.log(heading("Send that file however you like. On the other side:"));
|
|
495
|
+
console.log(` memshare preview ${path.basename(written)}`);
|
|
496
|
+
console.log(` memshare import ${path.basename(written)}`);
|
|
497
|
+
console.log();
|
|
498
|
+
});
|
|
499
|
+
async function decideBlocked(candidate, opts) {
|
|
500
|
+
if (opts.redactBlocked)
|
|
501
|
+
return "redacted";
|
|
502
|
+
if (opts.yes || !isInteractive())
|
|
503
|
+
return "skip";
|
|
504
|
+
console.log();
|
|
505
|
+
console.log(`${c.yellow("Blocked:")} ${candidate.item.content}`);
|
|
506
|
+
console.log(` ${c.dim("contains:")} ${summarisePII(candidate.findings)}`);
|
|
507
|
+
console.log(` ${c.dim("redacted:")} ${candidate.redacted.content}`);
|
|
508
|
+
return select({
|
|
509
|
+
message: "What should happen to it?",
|
|
510
|
+
default: "skip",
|
|
511
|
+
choices: [
|
|
512
|
+
{ name: "Skip it (recommended)", value: "skip" },
|
|
513
|
+
{ name: "Include the redacted version", value: "redacted" },
|
|
514
|
+
{ name: "Include it as-is, sensitive parts and all", value: "as-is" },
|
|
515
|
+
],
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
// ---------------------------------------------------------------- preview
|
|
519
|
+
program
|
|
520
|
+
.command("preview")
|
|
521
|
+
.argument("<file>", "bundle file to inspect")
|
|
522
|
+
.description("inspect a bundle without importing it")
|
|
523
|
+
.option("--json", "raw JSON output")
|
|
524
|
+
.action(async (file, opts) => {
|
|
525
|
+
const s = await requireStore();
|
|
526
|
+
const plan = await loadPlan(s, file);
|
|
527
|
+
if (opts.json) {
|
|
528
|
+
console.log(JSON.stringify(plan.bundle, null, 2));
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
printPlan(plan);
|
|
532
|
+
console.log(info(`Nothing imported. Run \`memshare import ${path.basename(file)}\` to choose.`));
|
|
533
|
+
console.log();
|
|
534
|
+
});
|
|
535
|
+
// ---------------------------------------------------------------- import
|
|
536
|
+
program
|
|
537
|
+
.command("import")
|
|
538
|
+
.argument("<file>", "bundle file from someone else")
|
|
539
|
+
.description("import a bundle, choosing item by item")
|
|
540
|
+
.option("-y, --yes", "accept every new item without asking")
|
|
541
|
+
.addOption(new Option("--visibility <visibility>", "how to store accepted items").choices(Visibility.options))
|
|
542
|
+
.option("--tag-sender", "tag accepted items with the sender's name, e.g. from-alice")
|
|
543
|
+
.option("--allow-duplicates", "also offer items you already have")
|
|
544
|
+
.action(async (file, opts) => {
|
|
545
|
+
const s = await requireStore();
|
|
546
|
+
const plan = await loadPlan(s, file);
|
|
547
|
+
printPlan(plan);
|
|
548
|
+
const selectable = plan.entries.filter((e) => e.status === "new" || (e.status === "duplicate" && opts.allowDuplicates));
|
|
549
|
+
if (selectable.length === 0) {
|
|
550
|
+
console.log(info("Nothing new to import."));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
let acceptedIds;
|
|
554
|
+
if (opts.yes || !isInteractive()) {
|
|
555
|
+
if (!opts.yes) {
|
|
556
|
+
throw new UserError("Import needs a terminal to ask you item by item. Re-run with --yes to accept all new items.");
|
|
557
|
+
}
|
|
558
|
+
acceptedIds = selectable.map((e) => e.item.id);
|
|
559
|
+
}
|
|
560
|
+
else {
|
|
561
|
+
acceptedIds = await checkbox({
|
|
562
|
+
message: "Which items do you want? (space to toggle, enter to confirm)",
|
|
563
|
+
pageSize: 15,
|
|
564
|
+
choices: selectable.map((e) => ({
|
|
565
|
+
name: `${truncate(e.item.content, 66)}` +
|
|
566
|
+
(e.item.tags.length > 0 ? ` [${e.item.tags.join(", ")}]` : "") +
|
|
567
|
+
(e.status === "duplicate" ? c.dim(" (you already have this)") : "") +
|
|
568
|
+
(e.findings.length > 0 ? c.yellow(` (${formatPii(e.findings)})`) : ""),
|
|
569
|
+
value: e.item.id,
|
|
570
|
+
checked: e.status === "new" && e.findings.length === 0,
|
|
571
|
+
})),
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
if (acceptedIds.length === 0) {
|
|
575
|
+
console.log(info("Nothing accepted."));
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const result = await applyImport(s, plan, {
|
|
579
|
+
acceptedIds,
|
|
580
|
+
...(opts.visibility ? { visibility: opts.visibility } : {}),
|
|
581
|
+
...(opts.tagSender ? { addTags: [senderTag(plan.bundle.metadata.exportedBy)] } : {}),
|
|
582
|
+
});
|
|
583
|
+
console.log();
|
|
584
|
+
console.log(ok(`Imported ${result.imported.length} item(s) from ${c.bold(plan.bundle.metadata.exportedBy)}.`));
|
|
585
|
+
console.log(info(`Stored as ${c.bold(result.imported[0]?.visibility ?? "private")} with confidence ` +
|
|
586
|
+
`${c.bold("imported")}. Nothing you already had was overwritten.`));
|
|
587
|
+
console.log();
|
|
588
|
+
});
|
|
589
|
+
async function loadPlan(s, file) {
|
|
590
|
+
const result = await readBundleFile(path.resolve(file));
|
|
591
|
+
for (const w of result.warnings)
|
|
592
|
+
console.log(warn(w));
|
|
593
|
+
if (!result.ok || !result.bundle) {
|
|
594
|
+
throw new UserError(`Cannot use this bundle:\n ${result.errors.join("\n ")}`);
|
|
595
|
+
}
|
|
596
|
+
return planImport(s, result.bundle);
|
|
597
|
+
}
|
|
598
|
+
function printPlan(plan) {
|
|
599
|
+
const meta = plan.bundle.metadata;
|
|
600
|
+
console.log();
|
|
601
|
+
console.log(heading(`Bundle from ${c.bold(meta.exportedBy)}`));
|
|
602
|
+
console.log(info(`${plan.counts.total} item(s), exported ${relativeTime(meta.exportedAt)}` +
|
|
603
|
+
(meta.exportedFor ? `, for ${meta.exportedFor}` : "") +
|
|
604
|
+
(meta.expiresAt ? `, expires ${relativeTime(meta.expiresAt)}` : "")));
|
|
605
|
+
console.log(info(`Integrity check passed (${meta.contentHash.slice(0, 12)}...).`));
|
|
606
|
+
if (meta.description)
|
|
607
|
+
console.log(info(`Note: ${meta.description}`));
|
|
608
|
+
console.log();
|
|
609
|
+
for (const entry of plan.entries) {
|
|
610
|
+
const marker = entry.status === "new"
|
|
611
|
+
? c.green("new")
|
|
612
|
+
: entry.status === "duplicate"
|
|
613
|
+
? c.dim("dup")
|
|
614
|
+
: c.yellow("exp");
|
|
615
|
+
console.log(` ${marker} ${entry.item.content}`);
|
|
616
|
+
const bits = [
|
|
617
|
+
entry.item.tags.length > 0 ? c.cyan(entry.item.tags.join(", ")) : c.dim("(no tags)"),
|
|
618
|
+
c.dim(`via ${entry.item.source.tool}`),
|
|
619
|
+
];
|
|
620
|
+
if (entry.status === "duplicate")
|
|
621
|
+
bits.push(c.dim("you already have this"));
|
|
622
|
+
if (entry.findings.length > 0)
|
|
623
|
+
bits.push(c.yellow(`sensitive: ${formatPii(entry.findings)}`));
|
|
624
|
+
console.log(` ${bits.join(c.dim(" | "))}`);
|
|
625
|
+
}
|
|
626
|
+
console.log();
|
|
627
|
+
}
|
|
628
|
+
// ---------------------------------------------------------------- serve
|
|
629
|
+
program
|
|
630
|
+
.command("serve")
|
|
631
|
+
.description("run the MCP server on stdio (this is what Claude connects to)")
|
|
632
|
+
.action(async () => {
|
|
633
|
+
// Imported lazily so the CLI stays fast for everything else.
|
|
634
|
+
const { serve } = await import("../mcp/server.js");
|
|
635
|
+
await serve(store());
|
|
636
|
+
});
|
|
637
|
+
// ---------------------------------------------------------------- config
|
|
638
|
+
program
|
|
639
|
+
.command("config")
|
|
640
|
+
.description("show or change settings")
|
|
641
|
+
.option("--set <key=value>", "e.g. --set mode=auto", collect, [])
|
|
642
|
+
.action(async (opts) => {
|
|
643
|
+
const s = await requireStore();
|
|
644
|
+
let config = await s.readConfig();
|
|
645
|
+
if (opts.set.length > 0) {
|
|
646
|
+
const patch = {};
|
|
647
|
+
for (const pair of opts.set) {
|
|
648
|
+
const at = pair.indexOf("=");
|
|
649
|
+
if (at === -1)
|
|
650
|
+
throw new UserError(`Expected key=value, got "${pair}".`);
|
|
651
|
+
const key = pair.slice(0, at).trim();
|
|
652
|
+
const raw = pair.slice(at + 1).trim();
|
|
653
|
+
if (!(key in config)) {
|
|
654
|
+
throw new UserError(`Unknown setting "${key}". Known: ${Object.keys(config).join(", ")}.`);
|
|
655
|
+
}
|
|
656
|
+
patch[key] = raw === "true" ? true : raw === "false" ? false : raw;
|
|
657
|
+
}
|
|
658
|
+
config = await s.init(patch);
|
|
659
|
+
console.log(ok("Updated."));
|
|
660
|
+
}
|
|
661
|
+
console.log();
|
|
662
|
+
console.log(heading(`Store: ${s.root}`));
|
|
663
|
+
for (const [key, value] of Object.entries(config)) {
|
|
664
|
+
console.log(` ${key.padEnd(20)} ${c.bold(String(value))}`);
|
|
665
|
+
}
|
|
666
|
+
console.log();
|
|
667
|
+
});
|
|
668
|
+
// ---------------------------------------------------------------- run
|
|
669
|
+
function collect(value, previous) {
|
|
670
|
+
return [...previous, value];
|
|
671
|
+
}
|
|
672
|
+
function guessName() {
|
|
673
|
+
const raw = process.env.USER ?? process.env.USERNAME ?? "";
|
|
674
|
+
return raw.trim() === "" ? "anonymous" : raw.trim();
|
|
675
|
+
}
|
|
676
|
+
async function main() {
|
|
677
|
+
try {
|
|
678
|
+
await program.parseAsync(process.argv);
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
// Ctrl-C inside a prompt is a normal way to leave, not a crash.
|
|
682
|
+
if (err instanceof Error && err.name === "ExitPromptError") {
|
|
683
|
+
console.log();
|
|
684
|
+
console.log(info("Cancelled."));
|
|
685
|
+
process.exitCode = 130;
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (err instanceof UserError) {
|
|
689
|
+
console.error();
|
|
690
|
+
console.error(fail(err.message));
|
|
691
|
+
console.error();
|
|
692
|
+
process.exitCode = 1;
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
console.error();
|
|
696
|
+
console.error(fail(err instanceof Error ? err.message : String(err)));
|
|
697
|
+
console.error();
|
|
698
|
+
process.exitCode = 1;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
void main();
|
|
702
|
+
//# sourceMappingURL=index.js.map
|