@memrosetta/cli 0.4.7 → 0.5.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.
Files changed (43) hide show
  1. package/dist/chunk-2MO65JLK.js +139 -0
  2. package/dist/chunk-5PH2RDAS.js +750 -0
  3. package/dist/chunk-IKIJVRHU.js +16 -0
  4. package/dist/chunk-PMQKXYS6.js +752 -0
  5. package/dist/chunk-SYPVELIW.js +64 -0
  6. package/dist/chunk-TSA67QME.js +56 -0
  7. package/dist/chunk-UKGD7QXV.js +81 -0
  8. package/dist/chunk-UOT33X3Q.js +750 -0
  9. package/dist/chunk-VR3TRSC7.js +148 -0
  10. package/dist/clear-HNVVALWL.js +39 -0
  11. package/dist/compress-VZJHSVJK.js +33 -0
  12. package/dist/count-WRCFIWWS.js +24 -0
  13. package/dist/enforce-PHO4L7C2.js +381 -0
  14. package/dist/feedback-JXSKOFRS.js +51 -0
  15. package/dist/feedback-O6NKG46O.js +51 -0
  16. package/dist/hooks/enforce-claude-code.js +119 -0
  17. package/dist/hooks/on-prompt.js +9 -5
  18. package/dist/hooks/on-stop.js +8 -4
  19. package/dist/index.js +22 -16
  20. package/dist/ingest-JWLBIFEI.js +97 -0
  21. package/dist/init-ARU2CIOH.js +205 -0
  22. package/dist/init-C2MCDVWL.js +205 -0
  23. package/dist/init-LIWL7Y3H.js +205 -0
  24. package/dist/init-VUJX3RRW.js +205 -0
  25. package/dist/invalidate-QPOV2E5U.js +40 -0
  26. package/dist/invalidate-VUHHNZUX.js +40 -0
  27. package/dist/maintain-UUZ76QET.js +37 -0
  28. package/dist/relate-MIOQYWTI.js +57 -0
  29. package/dist/relate-TEZ3GIIY.js +57 -0
  30. package/dist/reset-HQFOMYVP.js +129 -0
  31. package/dist/reset-OVU4A575.js +129 -0
  32. package/dist/reset-T6DPQCWI.js +129 -0
  33. package/dist/reset-WMDSMCAR.js +129 -0
  34. package/dist/search-IXV43G2C.js +48 -0
  35. package/dist/status-BLHKHB34.js +184 -0
  36. package/dist/status-CNGR7YDV.js +184 -0
  37. package/dist/status-IHYARQXU.js +184 -0
  38. package/dist/store-DTN3PTFQ.js +101 -0
  39. package/dist/store-V7I5RJ6Y.js +101 -0
  40. package/dist/sync-EHYMBZHE.js +542 -0
  41. package/dist/sync-UJBD7575.js +542 -0
  42. package/dist/working-memory-CH524PJA.js +53 -0
  43. package/package.json +6 -5
@@ -0,0 +1,148 @@
1
+ // src/hooks/memory-extractor.ts
2
+ import { userInfo } from "os";
3
+ var KEYWORD_PATTERNS = {
4
+ typescript: "TypeScript",
5
+ sqlite: "SQLite",
6
+ api: "API",
7
+ benchmark: "benchmark",
8
+ docker: "Docker",
9
+ git: "Git",
10
+ react: "React",
11
+ hono: "Hono",
12
+ test: "test",
13
+ deploy: "deploy",
14
+ refactor: "refactor",
15
+ bug: "bug",
16
+ memrosetta: "MemRosetta",
17
+ vector: "vector",
18
+ embedding: "embedding",
19
+ fts5: "FTS5",
20
+ search: "search",
21
+ memory: "memory",
22
+ database: "database",
23
+ postgresql: "PostgreSQL",
24
+ nextjs: "Next.js",
25
+ "next.js": "Next.js",
26
+ node: "Node.js",
27
+ python: "Python",
28
+ rust: "Rust",
29
+ kubernetes: "Kubernetes",
30
+ ci: "CI/CD",
31
+ security: "security",
32
+ auth: "authentication",
33
+ cache: "cache",
34
+ performance: "performance"
35
+ };
36
+ function classifyTurn(turn) {
37
+ const lower = turn.content.toLowerCase();
38
+ if (turn.role === "user") {
39
+ if (lower.includes("decide") || lower.includes("go with") || lower.includes("let's do") || lower.includes("proceed") || lower.includes("approved")) {
40
+ return "decision";
41
+ }
42
+ if (lower.includes("prefer") || lower.includes("i like") || lower.includes("i want") || lower.includes("i need")) {
43
+ return "preference";
44
+ }
45
+ return "event";
46
+ }
47
+ return "fact";
48
+ }
49
+ function extractKeywords(text) {
50
+ const keywords = /* @__PURE__ */ new Set();
51
+ const lower = text.toLowerCase();
52
+ for (const [pattern, keyword] of Object.entries(KEYWORD_PATTERNS)) {
53
+ if (lower.includes(pattern)) {
54
+ keywords.add(keyword);
55
+ }
56
+ }
57
+ return [...keywords];
58
+ }
59
+ function resolveUserId(_cwd) {
60
+ return userInfo().username;
61
+ }
62
+ function extractFirstSentence(text, maxLen = 200) {
63
+ const lines = text.split("\n");
64
+ for (const line of lines) {
65
+ const trimmed = line.trim();
66
+ if (!trimmed) continue;
67
+ if (trimmed.startsWith("```")) continue;
68
+ if (trimmed.startsWith("|")) continue;
69
+ if (trimmed.startsWith("- [")) continue;
70
+ if (trimmed.startsWith("> ")) continue;
71
+ if (trimmed.startsWith("import ") || trimmed.startsWith("export ") || trimmed.startsWith("const ") || trimmed.startsWith("function ")) continue;
72
+ if (trimmed.startsWith("{") || trimmed.startsWith("}")) continue;
73
+ if (/^[\s\-=*#]+$/.test(trimmed)) continue;
74
+ if (trimmed.startsWith("#")) {
75
+ const headerText = trimmed.replace(/^#+\s*/, "");
76
+ if (headerText.length > 10) {
77
+ return headerText.length > maxLen ? headerText.slice(0, maxLen - 3) + "..." : headerText;
78
+ }
79
+ continue;
80
+ }
81
+ if (trimmed.startsWith("/") || trimmed.startsWith("./") || trimmed.startsWith("$")) continue;
82
+ if (trimmed.length < 15) continue;
83
+ if (!/[a-zA-Z\uAC00-\uD7AF]{3,}/.test(trimmed)) continue;
84
+ return trimmed.length > maxLen ? trimmed.slice(0, maxLen - 3) + "..." : trimmed;
85
+ }
86
+ const clean = text.replace(/\s+/g, " ").trim();
87
+ return clean.length > maxLen ? clean.slice(0, maxLen - 3) + "..." : clean;
88
+ }
89
+ function isWorthStoring(turn) {
90
+ const content = turn.content;
91
+ if (content.length < 30) return false;
92
+ if (turn.role === "user") {
93
+ const lower = content.toLowerCase().trim();
94
+ if (lower.length < 20) return false;
95
+ if (lower.startsWith("/")) return false;
96
+ if (/^(y|n|yes|no|ok|ㅇㅇ|ㅋㅋ|ㄱㄱ|네|아니|진행|계속|좋아|해봐)$/i.test(lower)) return false;
97
+ return true;
98
+ }
99
+ const codeBlockCount = (content.match(/```/g) || []).length / 2;
100
+ if (codeBlockCount >= 2) return false;
101
+ const toolPatterns = [
102
+ /^(reading|writing|creating|editing|deleting|running|checking|searching|looking)/i,
103
+ /^(file|directory|folder) (created|modified|deleted|found)/i,
104
+ /^(let me|i'll|i will) (read|check|look|search|find|create|write|edit)/i,
105
+ /^(here('s| is| are) (the|a|an))/i,
106
+ /^\d+ (files?|tests?|errors?|warnings?) (found|passed|failed|created)/i,
107
+ /^(done|completed|finished|success|failed|error)/i,
108
+ /^(installing|building|compiling|running tests)/i
109
+ ];
110
+ for (const pattern of toolPatterns) {
111
+ if (pattern.test(content.trim())) return false;
112
+ }
113
+ if (content.length > 2e3) return false;
114
+ const wordCount = content.split(/\s+/).length;
115
+ const codeChars = (content.match(/[{}();=<>]/g) || []).length;
116
+ if (codeChars > wordCount * 0.3) return false;
117
+ return true;
118
+ }
119
+ function extractMemories(data, userId) {
120
+ const memories = [];
121
+ const sessionShort = data.sessionId ? data.sessionId.slice(0, 8) : "unknown";
122
+ const now = (/* @__PURE__ */ new Date()).toISOString();
123
+ const seen = /* @__PURE__ */ new Set();
124
+ for (let i = 0; i < data.turns.length; i++) {
125
+ const turn = data.turns[i];
126
+ if (!isWorthStoring(turn)) continue;
127
+ const content = extractFirstSentence(turn.content);
128
+ if (seen.has(content)) continue;
129
+ seen.add(content);
130
+ memories.push({
131
+ userId,
132
+ namespace: `session-${sessionShort}`,
133
+ memoryType: classifyTurn(turn),
134
+ content,
135
+ documentDate: now,
136
+ sourceId: `cc-${sessionShort}-${i}`,
137
+ confidence: turn.role === "user" ? 0.9 : 0.8,
138
+ keywords: extractKeywords(content)
139
+ });
140
+ }
141
+ return memories;
142
+ }
143
+
144
+ export {
145
+ classifyTurn,
146
+ resolveUserId,
147
+ extractMemories
148
+ };
@@ -0,0 +1,39 @@
1
+ import {
2
+ hasFlag,
3
+ optionalOption
4
+ } from "./chunk-SYPVELIW.js";
5
+ import {
6
+ getEngine
7
+ } from "./chunk-VAVUPQZA.js";
8
+ import {
9
+ output,
10
+ outputError
11
+ } from "./chunk-ET6TNQOJ.js";
12
+ import {
13
+ getDefaultUserId
14
+ } from "./chunk-SEPYQK3J.js";
15
+
16
+ // src/commands/clear.ts
17
+ async function run(options) {
18
+ const { args, format, db, noEmbeddings } = options;
19
+ const userId = optionalOption(args, "--user") ?? getDefaultUserId();
20
+ const confirm = hasFlag(args, "--confirm");
21
+ if (!confirm) {
22
+ outputError(
23
+ "This will delete all memories for the user. Use --confirm to proceed.",
24
+ format
25
+ );
26
+ process.exitCode = 1;
27
+ return;
28
+ }
29
+ const engine = await getEngine({ db, noEmbeddings });
30
+ const countBefore = await engine.count(userId);
31
+ await engine.clear(userId);
32
+ output(
33
+ { userId, cleared: countBefore, message: `Cleared ${countBefore} memories` },
34
+ format
35
+ );
36
+ }
37
+ export {
38
+ run
39
+ };
@@ -0,0 +1,33 @@
1
+ import {
2
+ optionalOption
3
+ } from "./chunk-SYPVELIW.js";
4
+ import {
5
+ getEngine
6
+ } from "./chunk-VAVUPQZA.js";
7
+ import {
8
+ output
9
+ } from "./chunk-ET6TNQOJ.js";
10
+ import {
11
+ getDefaultUserId
12
+ } from "./chunk-SEPYQK3J.js";
13
+
14
+ // src/commands/compress.ts
15
+ async function run(options) {
16
+ const { args, format, db, noEmbeddings } = options;
17
+ const userId = optionalOption(args, "--user") ?? getDefaultUserId();
18
+ const engine = await getEngine({ db, noEmbeddings });
19
+ const result = await engine.compress(userId);
20
+ if (format === "text") {
21
+ process.stdout.write(`Compression completed for user: ${userId}
22
+ `);
23
+ process.stdout.write(` Groups compressed: ${result.compressed}
24
+ `);
25
+ process.stdout.write(` Memories archived: ${result.removed}
26
+ `);
27
+ return;
28
+ }
29
+ output({ userId, ...result }, format);
30
+ }
31
+ export {
32
+ run
33
+ };
@@ -0,0 +1,24 @@
1
+ import {
2
+ optionalOption
3
+ } from "./chunk-SYPVELIW.js";
4
+ import {
5
+ getEngine
6
+ } from "./chunk-VAVUPQZA.js";
7
+ import {
8
+ output
9
+ } from "./chunk-ET6TNQOJ.js";
10
+ import {
11
+ getDefaultUserId
12
+ } from "./chunk-SEPYQK3J.js";
13
+
14
+ // src/commands/count.ts
15
+ async function run(options) {
16
+ const { args, format, db, noEmbeddings } = options;
17
+ const userId = optionalOption(args, "--user") ?? getDefaultUserId();
18
+ const engine = await getEngine({ db, noEmbeddings });
19
+ const count = await engine.count(userId);
20
+ output({ userId, count }, format);
21
+ }
22
+ export {
23
+ run
24
+ };
@@ -0,0 +1,381 @@
1
+ import {
2
+ resolveUserId
3
+ } from "./chunk-VR3TRSC7.js";
4
+ import {
5
+ hasFlag,
6
+ optionalOption,
7
+ requireOption
8
+ } from "./chunk-SYPVELIW.js";
9
+ import {
10
+ getEngine
11
+ } from "./chunk-VAVUPQZA.js";
12
+ import {
13
+ output,
14
+ outputError
15
+ } from "./chunk-ET6TNQOJ.js";
16
+ import "./chunk-SEPYQK3J.js";
17
+
18
+ // src/commands/enforce.ts
19
+ import { existsSync, readFileSync } from "fs";
20
+
21
+ // src/hooks/llm-extractor.ts
22
+ var SYSTEM_PROMPT = `You extract atomic long-term memories from a single
23
+ assistant turn in a coding assistant conversation.
24
+
25
+ Return a JSON object: { "memories": [...] }. Each memory has:
26
+ - "content": one self-contained, full-sentence fact, decision, preference,
27
+ or event. Keep proper nouns. Resolve pronouns. Korean stays Korean,
28
+ English stays English.
29
+ - "memoryType": one of "decision", "fact", "preference", "event".
30
+ - "keywords": 2-5 short keywords for search.
31
+ - "confidence": 0.0 to 1.0.
32
+
33
+ Only emit memories the user would still care about NEXT WEEK. Ignore:
34
+ - acknowledgements, greetings, status updates, confirmations
35
+ - code snippets and diffs (those belong in git)
36
+ - debugging steps and intermediate reasoning
37
+ - questions you asked the user
38
+
39
+ If nothing is worth storing, return { "memories": [] }.`;
40
+ var VALID_TYPES = /* @__PURE__ */ new Set([
41
+ "decision",
42
+ "fact",
43
+ "preference",
44
+ "event"
45
+ ]);
46
+ function safeParseMemories(raw) {
47
+ const stripped = raw.replace(/^```(?:json)?\s*|\s*```$/g, "").trim();
48
+ let parsed;
49
+ try {
50
+ parsed = JSON.parse(stripped);
51
+ } catch {
52
+ return [];
53
+ }
54
+ if (!parsed || typeof parsed !== "object") return [];
55
+ const obj = parsed;
56
+ if (!Array.isArray(obj.memories)) return [];
57
+ const out = [];
58
+ for (const item of obj.memories) {
59
+ if (!item || typeof item !== "object") continue;
60
+ const m = item;
61
+ if (typeof m.content !== "string" || m.content.trim().length === 0) continue;
62
+ if (!m.memoryType || !VALID_TYPES.has(m.memoryType)) continue;
63
+ out.push({
64
+ content: m.content.trim(),
65
+ memoryType: m.memoryType,
66
+ keywords: Array.isArray(m.keywords) && m.keywords.every((k) => typeof k === "string") ? m.keywords : void 0,
67
+ confidence: typeof m.confidence === "number" && m.confidence >= 0 && m.confidence <= 1 ? m.confidence : void 0
68
+ });
69
+ }
70
+ return out;
71
+ }
72
+ function buildUserPrompt(params) {
73
+ const ctx = params.userPrompt ? `User just asked:
74
+ ${params.userPrompt.slice(0, 800)}
75
+
76
+ ` : "";
77
+ return `${ctx}Assistant turn (client=${params.client}):
78
+ ${params.text}`;
79
+ }
80
+ async function extractAnthropic(params) {
81
+ const apiKey = process.env.ANTHROPIC_API_KEY;
82
+ if (!apiKey) return null;
83
+ try {
84
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
85
+ method: "POST",
86
+ headers: {
87
+ "content-type": "application/json",
88
+ "x-api-key": apiKey,
89
+ "anthropic-version": "2023-06-01"
90
+ },
91
+ body: JSON.stringify({
92
+ model: "claude-haiku-4-5-20251001",
93
+ max_tokens: 1024,
94
+ system: SYSTEM_PROMPT,
95
+ messages: [{ role: "user", content: buildUserPrompt(params) }]
96
+ })
97
+ });
98
+ if (!res.ok) {
99
+ process.stderr.write(
100
+ `[enforce] anthropic returned ${res.status} ${res.statusText}
101
+ `
102
+ );
103
+ return null;
104
+ }
105
+ const body = await res.json();
106
+ const text = body.content?.find((c) => c.type === "text")?.text?.trim() ?? "";
107
+ if (!text) return [];
108
+ return safeParseMemories(text);
109
+ } catch (err) {
110
+ process.stderr.write(
111
+ `[enforce] anthropic call failed: ${err instanceof Error ? err.message : String(err)}
112
+ `
113
+ );
114
+ return null;
115
+ }
116
+ }
117
+ async function extractOpenAI(params) {
118
+ const apiKey = process.env.OPENAI_API_KEY;
119
+ if (!apiKey) return null;
120
+ try {
121
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
122
+ method: "POST",
123
+ headers: {
124
+ "content-type": "application/json",
125
+ authorization: `Bearer ${apiKey}`
126
+ },
127
+ body: JSON.stringify({
128
+ model: "gpt-4o-mini",
129
+ max_tokens: 1024,
130
+ response_format: { type: "json_object" },
131
+ messages: [
132
+ { role: "system", content: SYSTEM_PROMPT },
133
+ { role: "user", content: buildUserPrompt(params) }
134
+ ]
135
+ })
136
+ });
137
+ if (!res.ok) {
138
+ process.stderr.write(
139
+ `[enforce] openai returned ${res.status} ${res.statusText}
140
+ `
141
+ );
142
+ return null;
143
+ }
144
+ const body = await res.json();
145
+ const text = body.choices?.[0]?.message?.content?.trim() ?? "";
146
+ if (!text) return [];
147
+ return safeParseMemories(text);
148
+ } catch (err) {
149
+ process.stderr.write(
150
+ `[enforce] openai call failed: ${err instanceof Error ? err.message : String(err)}
151
+ `
152
+ );
153
+ return null;
154
+ }
155
+ }
156
+ async function extractPropositionizer(params) {
157
+ try {
158
+ const importer = new Function("m", "return import(m)");
159
+ const mod = await importer("@memrosetta/extractor").catch(() => null);
160
+ if (!mod?.PropositionizerDecomposer) return null;
161
+ const decomposer = new mod.PropositionizerDecomposer();
162
+ const facts = await decomposer.decompose(params.text);
163
+ if (!Array.isArray(facts) || facts.length === 0) return [];
164
+ return facts.map((content) => ({
165
+ content,
166
+ memoryType: "fact",
167
+ confidence: 0.6
168
+ }));
169
+ } catch (err) {
170
+ process.stderr.write(
171
+ `[enforce] propositionizer fallback failed: ${err instanceof Error ? err.message : String(err)}
172
+ `
173
+ );
174
+ return null;
175
+ }
176
+ }
177
+ async function extractWithLLM(params) {
178
+ const anthropic = await extractAnthropic(params);
179
+ if (anthropic !== null) {
180
+ return { memories: anthropic, source: "anthropic", attempted: true };
181
+ }
182
+ const openai = await extractOpenAI(params);
183
+ if (openai !== null) {
184
+ return { memories: openai, source: "openai", attempted: true };
185
+ }
186
+ const local = await extractPropositionizer(params);
187
+ if (local !== null) {
188
+ return { memories: local, source: "propositionizer", attempted: true };
189
+ }
190
+ return { memories: [], source: "none", attempted: false };
191
+ }
192
+
193
+ // src/commands/enforce.ts
194
+ var MAX_ATTEMPTS = 2;
195
+ var HEURISTIC_KEYWORDS = [
196
+ // English
197
+ "decided",
198
+ "choose",
199
+ "chose",
200
+ "use ",
201
+ "switch to",
202
+ "instead of",
203
+ "conclusion",
204
+ "agreed",
205
+ "fixed",
206
+ "fix:",
207
+ "released",
208
+ "deployed",
209
+ "discovered",
210
+ "found",
211
+ "turns out",
212
+ // Korean
213
+ "\uACB0\uC815",
214
+ "\uACB0\uB860",
215
+ "\uD569\uC758",
216
+ "\uD655\uC815",
217
+ "\uC218\uC815",
218
+ "\uD574\uACB0",
219
+ "\uBC1C\uACAC",
220
+ "\uBC30\uD3EC",
221
+ "\uBC14\uAFE8",
222
+ "\uBC14\uAFB8\uC790",
223
+ "\uBCC0\uACBD",
224
+ "\uAD50\uCCB4"
225
+ ];
226
+ function loadEvent(path) {
227
+ if (!existsSync(path)) {
228
+ throw new Error(`event-json file not found: ${path}`);
229
+ }
230
+ const raw = readFileSync(path, "utf-8");
231
+ let parsed;
232
+ try {
233
+ parsed = JSON.parse(raw);
234
+ } catch {
235
+ throw new Error(`event-json is not valid JSON: ${path}`);
236
+ }
237
+ const e = parsed;
238
+ if (!e.client || typeof e.assistantMessage !== "string") {
239
+ throw new Error(
240
+ "event-json must include at least { client, assistantMessage }"
241
+ );
242
+ }
243
+ return {
244
+ client: e.client,
245
+ turnId: e.turnId,
246
+ assistantMessage: e.assistantMessage,
247
+ userPrompt: e.userPrompt,
248
+ cwd: e.cwd,
249
+ transcriptPath: e.transcriptPath,
250
+ attempt: e.attempt ?? 1
251
+ };
252
+ }
253
+ function looksStorable(text) {
254
+ const lower = text.toLowerCase();
255
+ return HEURISTIC_KEYWORDS.some((k) => lower.includes(k.toLowerCase()));
256
+ }
257
+ function buildFooter(result) {
258
+ if (result.status === "noop") {
259
+ return "STORED: none (noop)";
260
+ }
261
+ if (result.status === "needs-continuation") {
262
+ return "STORED: pending (needs-continuation)";
263
+ }
264
+ if (result.memories.length === 0) {
265
+ return "STORED: failed";
266
+ }
267
+ const items = result.memories.map((m) => `${m.type}(${m.memoryId})`).join(", ");
268
+ return `STORED: ${items}`;
269
+ }
270
+ async function run(options) {
271
+ const { args, format, db, noEmbeddings } = options;
272
+ const sub = args[0];
273
+ if (sub !== "stop") {
274
+ outputError(
275
+ "Usage: memrosetta enforce stop --client <id> --event-json <path>",
276
+ format
277
+ );
278
+ process.exitCode = 1;
279
+ return;
280
+ }
281
+ const sliced = args.slice(1);
282
+ let client;
283
+ let eventPath;
284
+ try {
285
+ client = requireOption(sliced, "--client", "client identifier");
286
+ eventPath = requireOption(sliced, "--event-json", "event JSON path");
287
+ } catch (err) {
288
+ outputError(err instanceof Error ? err.message : String(err), format);
289
+ process.exitCode = 1;
290
+ return;
291
+ }
292
+ const explicitAttempt = optionalOption(sliced, "--attempt");
293
+ const dryRun = hasFlag(sliced, "--dry-run");
294
+ let event;
295
+ try {
296
+ event = loadEvent(eventPath);
297
+ } catch (err) {
298
+ outputError(err instanceof Error ? err.message : String(err), format);
299
+ process.exitCode = 1;
300
+ return;
301
+ }
302
+ const attempt = explicitAttempt ? Math.max(1, parseInt(explicitAttempt, 10)) : event.attempt ?? 1;
303
+ if (!event.assistantMessage.trim()) {
304
+ const result2 = {
305
+ status: "noop",
306
+ structuredCount: 0,
307
+ extractedCount: 0,
308
+ memories: [],
309
+ footer: "STORED: none (noop)",
310
+ attempt,
311
+ maxAttempts: MAX_ATTEMPTS,
312
+ reason: "empty assistant message"
313
+ };
314
+ output(result2, format);
315
+ return;
316
+ }
317
+ const extractor = await extractWithLLM({
318
+ text: event.assistantMessage,
319
+ userPrompt: event.userPrompt,
320
+ client
321
+ });
322
+ const stored = [];
323
+ if (!dryRun && extractor.memories.length > 0) {
324
+ try {
325
+ const engine = await getEngine({ db, noEmbeddings });
326
+ const userId = resolveUserId(event.cwd ?? process.cwd());
327
+ for (const m of extractor.memories) {
328
+ const input = {
329
+ userId,
330
+ content: m.content,
331
+ memoryType: m.memoryType,
332
+ keywords: m.keywords,
333
+ confidence: m.confidence
334
+ };
335
+ const memory = await engine.store(input);
336
+ stored.push({ type: memory.memoryType, memoryId: memory.memoryId });
337
+ }
338
+ } catch (err) {
339
+ const reason2 = err instanceof Error ? err.message : String(err);
340
+ const result2 = {
341
+ status: "noop",
342
+ structuredCount: 0,
343
+ extractedCount: extractor.memories.length,
344
+ memories: [],
345
+ footer: "STORED: failed",
346
+ attempt,
347
+ maxAttempts: MAX_ATTEMPTS,
348
+ reason: `engine.store failed: ${reason2}`
349
+ };
350
+ output(result2, format);
351
+ return;
352
+ }
353
+ }
354
+ const extractedCount = stored.length;
355
+ const structuredCount = 0;
356
+ let status;
357
+ let reason;
358
+ if (extractedCount > 0) {
359
+ status = "stored";
360
+ } else if (looksStorable(event.assistantMessage) && attempt < MAX_ATTEMPTS && extractor.attempted) {
361
+ status = "needs-continuation";
362
+ reason = "turn looked storable (decision/conclusion keywords present) but extractor returned 0 memories";
363
+ } else {
364
+ status = "noop";
365
+ reason = extractor.attempted ? "extractor returned 0 memories and turn does not look storable" : "no LLM extractor available (set ANTHROPIC_API_KEY / OPENAI_API_KEY or install propositionizer model)";
366
+ }
367
+ const partial = {
368
+ status,
369
+ structuredCount,
370
+ extractedCount,
371
+ memories: stored,
372
+ attempt,
373
+ maxAttempts: MAX_ATTEMPTS,
374
+ reason
375
+ };
376
+ const result = { ...partial, footer: buildFooter(partial) };
377
+ output(result, format);
378
+ }
379
+ export {
380
+ run
381
+ };
@@ -0,0 +1,51 @@
1
+ import {
2
+ buildFeedbackGivenOp,
3
+ openCliSyncContext
4
+ } from "./chunk-2MO65JLK.js";
5
+ import {
6
+ hasFlag
7
+ } from "./chunk-NU5ZJJXP.js";
8
+ import {
9
+ getEngine,
10
+ resolveDbPath
11
+ } from "./chunk-VAVUPQZA.js";
12
+ import {
13
+ output,
14
+ outputError
15
+ } from "./chunk-ET6TNQOJ.js";
16
+ import "./chunk-SEPYQK3J.js";
17
+
18
+ // src/commands/feedback.ts
19
+ async function run(options) {
20
+ const { args, format, db, noEmbeddings } = options;
21
+ const memoryId = args.find((a) => !a.startsWith("-"));
22
+ if (!memoryId) {
23
+ outputError("Usage: memrosetta feedback <memoryId> --helpful | --not-helpful", format);
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+ const helpful = hasFlag(args, "--helpful");
28
+ const notHelpful = hasFlag(args, "--not-helpful");
29
+ if (!helpful && !notHelpful) {
30
+ outputError("Specify --helpful or --not-helpful", format);
31
+ process.exitCode = 1;
32
+ return;
33
+ }
34
+ if (helpful && notHelpful) {
35
+ outputError("Specify either --helpful or --not-helpful, not both", format);
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+ const engine = await getEngine({ db, noEmbeddings });
40
+ const now = (/* @__PURE__ */ new Date()).toISOString();
41
+ await engine.feedback(memoryId, helpful);
42
+ const sync = await openCliSyncContext(resolveDbPath(db));
43
+ if (sync.enabled) {
44
+ sync.enqueue(buildFeedbackGivenOp(sync, memoryId, helpful, now));
45
+ sync.close();
46
+ }
47
+ output({ memoryId, helpful }, format);
48
+ }
49
+ export {
50
+ run
51
+ };