@kisev/memomatic 1.0.0-dev.100.g21366c4ad740

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 (51) hide show
  1. package/README.md +97 -0
  2. package/README.ru.md +95 -0
  3. package/assets/systemd/memomatic-dream.service +9 -0
  4. package/assets/systemd/memomatic-dream.timer +10 -0
  5. package/assets/systemd/memomatic-sessions.service +9 -0
  6. package/assets/systemd/memomatic-sessions.timer +10 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +309 -0
  9. package/dist/corpus.d.ts +12 -0
  10. package/dist/corpus.js +138 -0
  11. package/dist/dream.d.ts +31 -0
  12. package/dist/dream.js +234 -0
  13. package/dist/entries.d.ts +31 -0
  14. package/dist/entries.js +119 -0
  15. package/dist/executor.d.ts +45 -0
  16. package/dist/executor.js +267 -0
  17. package/dist/gates.d.ts +27 -0
  18. package/dist/gates.js +69 -0
  19. package/dist/generated/cli.d.ts +28 -0
  20. package/dist/generated/cli.js +181 -0
  21. package/dist/inbox.d.ts +34 -0
  22. package/dist/inbox.js +224 -0
  23. package/dist/index.d.ts +7 -0
  24. package/dist/index.js +6 -0
  25. package/dist/ingest.d.ts +1 -0
  26. package/dist/ingest.js +15 -0
  27. package/dist/ingestion.d.ts +30 -0
  28. package/dist/ingestion.js +179 -0
  29. package/dist/mcp.d.ts +15 -0
  30. package/dist/mcp.js +205 -0
  31. package/dist/opencode-config.d.ts +1 -0
  32. package/dist/opencode-config.js +84 -0
  33. package/dist/operations.d.ts +7 -0
  34. package/dist/operations.js +3 -0
  35. package/dist/paths.d.ts +18 -0
  36. package/dist/paths.js +55 -0
  37. package/dist/rules.d.ts +13 -0
  38. package/dist/rules.js +39 -0
  39. package/dist/search.d.ts +28 -0
  40. package/dist/search.js +191 -0
  41. package/dist/service.d.ts +63 -0
  42. package/dist/service.js +163 -0
  43. package/dist/sessions.d.ts +36 -0
  44. package/dist/sessions.js +238 -0
  45. package/dist/settings.d.ts +48 -0
  46. package/dist/settings.js +189 -0
  47. package/dist/store.d.ts +55 -0
  48. package/dist/store.js +286 -0
  49. package/dist/visibility.d.ts +7 -0
  50. package/dist/visibility.js +13 -0
  51. package/package.json +48 -0
@@ -0,0 +1,267 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { randomBytes } from "node:crypto";
6
+ import { isolatedProviderConfig } from "./opencode-config.js";
7
+ export function extractJson(response) {
8
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(response);
9
+ for (const candidate of [fenced?.[1], response]) {
10
+ if (!candidate)
11
+ continue;
12
+ const start = candidate.indexOf("{");
13
+ const end = candidate.lastIndexOf("}");
14
+ if (start < 0 || end <= start)
15
+ continue;
16
+ try {
17
+ return JSON.parse(candidate.slice(start, end + 1));
18
+ }
19
+ catch {
20
+ continue;
21
+ }
22
+ }
23
+ throw new Error("model response contains no JSON object");
24
+ }
25
+ export class OpenCodeExecutor {
26
+ options;
27
+ name = "opencode";
28
+ usage = {
29
+ calls: 0,
30
+ retries: 0,
31
+ inputTokens: 0,
32
+ outputTokens: 0,
33
+ cacheReadTokens: 0,
34
+ cacheWriteTokens: 0,
35
+ usageAvailable: false,
36
+ charactersSent: 0,
37
+ };
38
+ child;
39
+ directory;
40
+ url;
41
+ authorization;
42
+ constructor(options) {
43
+ this.options = options;
44
+ }
45
+ async initialize() {
46
+ if (this.url)
47
+ return;
48
+ if (this.options.url) {
49
+ const url = new URL(this.options.url);
50
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password)
51
+ throw new Error("invalid OpenCode URL");
52
+ this.url = url.href.replace(/\/$/, "");
53
+ const password = process.env.OPENCODE_PASSWORD ?? process.env.OPENCODE_SERVER_PASSWORD;
54
+ if (password)
55
+ this.authorization = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
56
+ return;
57
+ }
58
+ const inherited = await isolatedProviderConfig();
59
+ this.directory = await mkdtemp(join(tmpdir(), "memomatic-opencode-"));
60
+ const configFile = join(this.directory, "opencode.json");
61
+ await writeFile(configFile, JSON.stringify({
62
+ ...inherited,
63
+ plugins: ["-opencode.skill", "-opencode.config.skill", "-opencode.tool.skill"],
64
+ permissions: [{ action: "*", resource: "*", effect: "deny" }],
65
+ agents: {
66
+ memomatic: {
67
+ mode: "primary",
68
+ description: "Bounded memory extraction",
69
+ system: "Return only the requested JSON. Do not use tools.",
70
+ permissions: [{ action: "*", resource: "*", effect: "deny" }],
71
+ },
72
+ },
73
+ }), { mode: 0o600 });
74
+ const password = randomBytes(24).toString("hex");
75
+ this.authorization = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
76
+ this.options.observe?.({
77
+ phase: "opencode.start",
78
+ message: "Starting one isolated OpenCode server for this run",
79
+ });
80
+ const child = spawn(this.options.binary ?? "opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0"], {
81
+ cwd: this.directory,
82
+ detached: process.platform !== "win32",
83
+ stdio: ["ignore", "pipe", "pipe"],
84
+ env: {
85
+ ...process.env,
86
+ OPENCODE_SERVER_USERNAME: "opencode",
87
+ OPENCODE_SERVER_PASSWORD: password,
88
+ OPENCODE_CONFIG_PROJECT_DISABLE: "1",
89
+ OPENCODE_CONFIG_DIR: this.directory,
90
+ OPENCODE_CONFIG: configFile,
91
+ OPENCODE_CONFIG_CONTENT: "{}",
92
+ OPENCODE_PASSWORD: password,
93
+ },
94
+ });
95
+ this.child = child;
96
+ this.url = await new Promise((resolve, reject) => {
97
+ let output = "";
98
+ const timeout = setTimeout(() => done(new Error("OpenCode startup timed out")), 30000);
99
+ const abort = () => done(this.options.signal?.reason instanceof Error
100
+ ? this.options.signal.reason
101
+ : new Error("interrupted"));
102
+ const failed = (error) => done(error);
103
+ const closed = () => done(new Error("OpenCode exited before becoming ready"));
104
+ const data = (chunk) => {
105
+ output = (output + chunk.toString()).slice(-8192);
106
+ const match = /https?:\/\/127\.0\.0\.1:\d+/.exec(output);
107
+ if (match)
108
+ done(undefined, match[0]);
109
+ };
110
+ const done = (error, url) => {
111
+ clearTimeout(timeout);
112
+ child.stdout?.off("data", data);
113
+ child.stderr?.off("data", data);
114
+ child.off("error", failed);
115
+ child.off("exit", closed);
116
+ this.options.signal?.removeEventListener("abort", abort);
117
+ if (error)
118
+ reject(error);
119
+ else
120
+ resolve(url);
121
+ };
122
+ child.stdout?.on("data", data);
123
+ child.stderr?.on("data", data);
124
+ child.once("error", failed);
125
+ child.once("exit", closed);
126
+ this.options.signal?.addEventListener("abort", abort, { once: true });
127
+ if (this.options.signal?.aborted)
128
+ abort();
129
+ });
130
+ // Drain logs without retaining provider output, credentials or prompts.
131
+ child.stdout?.resume();
132
+ child.stderr?.resume();
133
+ this.options.observe?.({ phase: "opencode.ready", message: "OpenCode server ready" });
134
+ }
135
+ async request(path, body, signal, method = "POST") {
136
+ const response = await fetch(`${this.url}${path}`, {
137
+ method,
138
+ headers: {
139
+ "content-type": "application/json",
140
+ ...(this.authorization ? { authorization: this.authorization } : {}),
141
+ },
142
+ body: JSON.stringify(body),
143
+ signal,
144
+ });
145
+ if (!response.ok)
146
+ throw new Error(`OpenCode ${path.includes("generate") ? "model request" : "session request"} failed: HTTP ${response.status}`);
147
+ return response.status === 204 ? {} : (await response.json());
148
+ }
149
+ async complete(request) {
150
+ this.options.signal?.throwIfAborted();
151
+ await this.initialize();
152
+ for (let attempt = 0; attempt <= (this.options.retries ?? 1); attempt++) {
153
+ this.options.signal?.throwIfAborted();
154
+ const signal = AbortSignal.any([
155
+ AbortSignal.timeout(this.options.timeoutMs ?? 180000),
156
+ ...(this.options.signal ? [this.options.signal] : []),
157
+ ]);
158
+ let id;
159
+ const started = Date.now();
160
+ const heartbeat = setInterval(() => this.options.observe?.({
161
+ phase: "model.wait",
162
+ message: "Waiting for OpenCode/model response",
163
+ elapsedMs: Date.now() - started,
164
+ attempt: attempt + 1,
165
+ }), 15000);
166
+ heartbeat.unref();
167
+ try {
168
+ const slash = this.options.model?.indexOf("/") ?? -1;
169
+ if (this.options.model && slash < 1)
170
+ throw new Error("model must use provider/model format");
171
+ const session = await this.request("/api/session", {
172
+ title: "[memomatic-internal] memory extraction",
173
+ agent: this.options.url ? "build" : "memomatic",
174
+ ...(this.directory ? { location: { directory: this.directory } } : {}),
175
+ permissions: [{ action: "*", resource: "*", effect: "deny" }],
176
+ ...(this.options.model
177
+ ? {
178
+ model: {
179
+ providerID: this.options.model.slice(0, slash),
180
+ id: this.options.model.slice(slash + 1),
181
+ ...(this.options.variant ? { variant: this.options.variant } : {}),
182
+ },
183
+ }
184
+ : {}),
185
+ }, signal);
186
+ const data = session.data;
187
+ if (typeof data?.id !== "string")
188
+ throw new Error("OpenCode session response is malformed");
189
+ id = data.id;
190
+ await this.request(`/api/experimental/session/${encodeURIComponent(id)}/instructions/entries/memomatic`, { value: request.system }, signal, "PUT");
191
+ this.usage.calls++;
192
+ this.usage.charactersSent += request.system.length + request.prompt.length;
193
+ this.options.observe?.({
194
+ phase: "model.request",
195
+ message: "Sending prepared fragment to OpenCode",
196
+ characters: request.prompt.length,
197
+ attempt: attempt + 1,
198
+ });
199
+ const response = await this.request(`/api/session/${encodeURIComponent(id)}/generate`, { prompt: `[memomatic-internal] ${request.prompt}` }, signal);
200
+ const generated = response.data;
201
+ if (typeof generated?.text !== "string")
202
+ throw new Error("OpenCode model response is malformed");
203
+ // V2 transient generation returns text only, not exact token usage.
204
+ const parsed = extractJson(generated.text);
205
+ this.options.observe?.({
206
+ phase: "model.done",
207
+ message: "Model response received",
208
+ elapsedMs: Date.now() - started,
209
+ ...this.usage,
210
+ });
211
+ return JSON.stringify(parsed);
212
+ }
213
+ catch (error) {
214
+ if (id)
215
+ await this.request(`/api/session/${encodeURIComponent(id)}/interrupt?resume=false`, {}, AbortSignal.timeout(3000)).catch(() => undefined);
216
+ if (this.options.signal?.aborted)
217
+ throw this.options.signal.reason;
218
+ if (attempt === (this.options.retries ?? 1))
219
+ throw new Error(`OpenCode request failed after ${attempt + 1} attempt(s): ${error.message}`);
220
+ this.usage.retries++;
221
+ this.options.observe?.({
222
+ phase: "model.retry",
223
+ message: "Retrying failed model request",
224
+ level: "warn",
225
+ attempt: attempt + 2,
226
+ reason: error.message,
227
+ });
228
+ }
229
+ finally {
230
+ clearInterval(heartbeat);
231
+ }
232
+ }
233
+ throw new Error("unreachable");
234
+ }
235
+ async close() {
236
+ const child = this.child;
237
+ if (child?.pid && child.exitCode === null && child.signalCode === null) {
238
+ await new Promise((resolve) => {
239
+ const kill = (signal) => {
240
+ try {
241
+ if (process.platform !== "win32" && child.pid)
242
+ process.kill(-child.pid, signal);
243
+ else
244
+ child.kill(signal);
245
+ }
246
+ catch { }
247
+ };
248
+ const timer = setTimeout(() => kill("SIGKILL"), 1000);
249
+ timer.unref();
250
+ child.once("close", () => {
251
+ clearTimeout(timer);
252
+ resolve();
253
+ });
254
+ kill("SIGTERM");
255
+ });
256
+ }
257
+ if (child?.pid && process.platform !== "win32") {
258
+ try {
259
+ process.kill(-child.pid, "SIGKILL");
260
+ }
261
+ catch { }
262
+ }
263
+ this.child = undefined;
264
+ if (this.directory)
265
+ await rm(this.directory, { recursive: true, force: true });
266
+ }
267
+ }
@@ -0,0 +1,27 @@
1
+ import type { MemomaticSettings } from "./settings.js";
2
+ import type { IndexedEntry, MemoryStore } from "./store.js";
3
+ export type PromotionCandidate = {
4
+ entry: IndexedEntry;
5
+ score: number;
6
+ signals: {
7
+ relevance: number;
8
+ frequency: number;
9
+ diversity: number;
10
+ recency: number;
11
+ consolidation: number;
12
+ richness: number;
13
+ };
14
+ eligible: boolean;
15
+ reason: string | null;
16
+ };
17
+ export declare function scoreCandidate(entry: IndexedEntry, usage: {
18
+ useful: number;
19
+ queries: string[];
20
+ days: string[];
21
+ }, settings: MemomaticSettings, relevance: number, now?: number): PromotionCandidate;
22
+ export declare function promotionCandidates(store: MemoryStore, settings: MemomaticSettings, relevanceByStableId: Map<string, number>, now?: number): PromotionCandidate[];
23
+ /**
24
+ * Self-relevance of episodic entries that already passed the useful-signal
25
+ * gate: an entry matching its own text through FTS is consolidation-relevant.
26
+ */
27
+ export declare function relevanceByFts(store: MemoryStore, settings: MemomaticSettings): Map<string, number>;
package/dist/gates.js ADDED
@@ -0,0 +1,69 @@
1
+ export function scoreCandidate(entry, usage, settings, relevance, now = Date.now()) {
2
+ const signals = {
3
+ relevance: Math.max(0, Math.min(1, relevance)),
4
+ frequency: Math.min(1, usage.useful / 10),
5
+ diversity: Math.min(1, usage.queries.length / 5),
6
+ recency: entry.observedAt
7
+ ? Math.pow(0.5, Math.max(0, now - entry.observedAt) / 86_400_000 / settings.search.halfLifeDays)
8
+ : 0.5,
9
+ consolidation: Math.min(1, usage.useful / Math.max(1, settings.dream.minUseful)),
10
+ richness: Math.min(1, entry.text.length / 400),
11
+ };
12
+ const score = 0.3 * signals.relevance +
13
+ 0.24 * signals.frequency +
14
+ 0.15 * signals.diversity +
15
+ 0.15 * signals.recency +
16
+ 0.1 * signals.consolidation +
17
+ 0.06 * signals.richness;
18
+ let eligible = true;
19
+ let reason = null;
20
+ if (entry.kind !== "episodic") {
21
+ eligible = false;
22
+ reason = "not episodic";
23
+ }
24
+ else if (entry.origin === null) {
25
+ eligible = false;
26
+ reason = "unknown origin";
27
+ }
28
+ else if (usage.useful < settings.dream.minUseful) {
29
+ eligible = false;
30
+ reason = `useful ${usage.useful} < ${settings.dream.minUseful}`;
31
+ }
32
+ else if (usage.queries.length < settings.dream.minUniqueQueries) {
33
+ eligible = false;
34
+ reason = `queries ${usage.queries.length} < ${settings.dream.minUniqueQueries}`;
35
+ }
36
+ else if (score < settings.dream.minScore) {
37
+ eligible = false;
38
+ reason = `score ${score.toFixed(3)} < ${settings.dream.minScore}`;
39
+ }
40
+ return { entry, score, signals, eligible, reason };
41
+ }
42
+ export function promotionCandidates(store, settings, relevanceByStableId, now = Date.now()) {
43
+ const candidates = [];
44
+ for (const entry of store.allEntries()) {
45
+ const usage = store.usageFor(entry.stableId);
46
+ candidates.push(scoreCandidate(entry, usage, settings, relevanceByStableId.get(entry.stableId) ?? 0, now));
47
+ }
48
+ return candidates
49
+ .filter((candidate) => candidate.eligible)
50
+ .sort((left, right) => right.score - left.score)
51
+ .slice(0, settings.dream.maxCandidates);
52
+ }
53
+ /**
54
+ * Self-relevance of episodic entries that already passed the useful-signal
55
+ * gate: an entry matching its own text through FTS is consolidation-relevant.
56
+ */
57
+ export function relevanceByFts(store, settings) {
58
+ const relevance = new Map();
59
+ for (const entry of store.allEntries()) {
60
+ if (entry.kind !== "episodic")
61
+ continue;
62
+ if (store.usageFor(entry.stableId).useful < settings.dream.minUseful)
63
+ continue;
64
+ const match = store.ftsSearch(entry.text, 10).get(entry.stableId);
65
+ if (match)
66
+ relevance.set(entry.stableId, match);
67
+ }
68
+ return relevance;
69
+ }
@@ -0,0 +1,28 @@
1
+ import { Command, Option, CommanderError, InvalidArgumentError } from "commander";
2
+ export { Command, Option, CommanderError, InvalidArgumentError };
3
+ export declare function integer(value: string): number;
4
+ export declare function duration(value: string): number;
5
+ export declare function program(name: string, description: string, version: string): Command;
6
+ export declare function common(command: Command, prefix: string): Command;
7
+ export declare function configFile(path: string | undefined): Promise<Record<string, unknown>>;
8
+ export declare function configFileSync(path: string | undefined): Record<string, unknown>;
9
+ export declare function applyConfig(command: Command, config: Record<string, unknown>): void;
10
+ export type Event = {
11
+ phase: string;
12
+ message: string;
13
+ level?: "debug" | "info" | "warn" | "error";
14
+ completed?: number;
15
+ total?: number;
16
+ elapsedMs?: number;
17
+ [key: string]: unknown;
18
+ };
19
+ export type Observe = (event: Event) => void;
20
+ export declare function reporter(options?: Record<string, unknown>): {
21
+ emit: Observe;
22
+ close: () => void;
23
+ };
24
+ export declare function cancellation(): {
25
+ signal: AbortSignal;
26
+ dispose: () => void;
27
+ };
28
+ export declare function fail(name: string, error: unknown, json?: boolean): void;
@@ -0,0 +1,181 @@
1
+ import { Command, Option, CommanderError, InvalidArgumentError } from "commander";
2
+ import { readFile } from "node:fs/promises";
3
+ import { readFileSync } from "node:fs";
4
+ export { Command, Option, CommanderError, InvalidArgumentError };
5
+ export function integer(value) {
6
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
7
+ throw new InvalidArgumentError("expected a non-negative integer");
8
+ return Number(value);
9
+ }
10
+ export function duration(value) {
11
+ const match = /^(\d+)(ms|s|m|h)?$/.exec(value);
12
+ if (!match)
13
+ throw new InvalidArgumentError("expected a duration such as 500ms, 30s or 5m");
14
+ const result = Number(match[1]) * ({ ms: 1, s: 1000, m: 60000, h: 3600000 }[match[2] ?? "ms"] ?? 1);
15
+ if (!Number.isSafeInteger(result) || result < 0)
16
+ throw new InvalidArgumentError("duration is out of range");
17
+ return result;
18
+ }
19
+ export function program(name, description, version) {
20
+ return new Command(name)
21
+ .description(description)
22
+ .version(version)
23
+ .exitOverride()
24
+ .configureHelp({ showGlobalOptions: true })
25
+ .showHelpAfterError()
26
+ .configureOutput({ outputError: () => { } });
27
+ }
28
+ export function common(command, prefix) {
29
+ return command
30
+ .addOption(new Option("--config <file>", "JSON configuration file (CLI > env > file > defaults)").env(`${prefix}_CONFIG`))
31
+ .addOption(new Option("--json", "machine-readable result on stdout").env(`${prefix}_JSON`))
32
+ .addOption(new Option("--log-level <level>", "diagnostic verbosity on stderr")
33
+ .choices(["debug", "info", "warn", "error", "silent"])
34
+ .env(`${prefix}_LOG_LEVEL`))
35
+ .addOption(new Option("--log-format <format>", "diagnostic format on stderr")
36
+ .choices(["text", "json"])
37
+ .env(`${prefix}_LOG_FORMAT`))
38
+ .addOption(new Option("--progress <mode>", "terminal progress; never written to stdout")
39
+ .choices(["auto", "always", "never"])
40
+ .env(`${prefix}_PROGRESS`))
41
+ .addOption(new Option("--color <mode>", "color policy (NO_COLOR disables color)")
42
+ .choices(["auto", "always", "never"])
43
+ .env(`${prefix}_COLOR`));
44
+ }
45
+ export async function configFile(path) {
46
+ if (!path)
47
+ return {};
48
+ const value = JSON.parse(await readFile(path, "utf8"));
49
+ if (!value || typeof value !== "object" || Array.isArray(value))
50
+ throw new InvalidArgumentError("configuration must be a JSON object");
51
+ return value;
52
+ }
53
+ export function configFileSync(path) {
54
+ if (!path)
55
+ return {};
56
+ const value = JSON.parse(readFileSync(path, "utf8"));
57
+ if (!value || typeof value !== "object" || Array.isArray(value))
58
+ throw new InvalidArgumentError("configuration must be a JSON object");
59
+ return value;
60
+ }
61
+ export function applyConfig(command, config) {
62
+ for (const option of command.options) {
63
+ const key = option.attributeName();
64
+ if (!(key in config) || ["cli", "env"].includes(command.getOptionValueSource(key) ?? ""))
65
+ continue;
66
+ if (["yes", "config"].includes(key))
67
+ continue;
68
+ const value = config[key];
69
+ if (option.required || option.optional) {
70
+ const raw = String(value);
71
+ if (option.argChoices && !option.argChoices.includes(raw))
72
+ throw new InvalidArgumentError(`invalid configuration value for ${key}`);
73
+ command.setOptionValueWithSource(key, option.parseArg ? option.parseArg(raw, undefined) : raw, "config");
74
+ }
75
+ else {
76
+ if (typeof value !== "boolean")
77
+ throw new InvalidArgumentError(`${key} must be boolean`);
78
+ command.setOptionValueWithSource(key, value, "config");
79
+ }
80
+ }
81
+ }
82
+ export function reporter(options = {}) {
83
+ const started = Date.now();
84
+ const level = String(options.logLevel ?? "info");
85
+ const levels = ["debug", "info", "warn", "error", "silent"];
86
+ const json = options.logFormat === "json";
87
+ const interactive = !json &&
88
+ options.progress !== "never" &&
89
+ (options.progress === "always" || process.stderr.isTTY);
90
+ let current;
91
+ let painted = false;
92
+ let frame = 0;
93
+ let meter;
94
+ const bar = () => {
95
+ if (!meter || meter.total <= 0)
96
+ return "";
97
+ const ratio = Math.min(1, Math.max(0, meter.completed / meter.total));
98
+ const filled = Math.floor(ratio * 16);
99
+ return ` | ${meter.phase} [${"█".repeat(filled)}${"░".repeat(16 - filled)}] ${Math.floor(ratio * 100)}%`;
100
+ };
101
+ const clean = (text) => Array.from(text, (char) => {
102
+ const code = char.codePointAt(0);
103
+ return code < 32 || (code >= 127 && code <= 159) ? " " : char;
104
+ }).join("");
105
+ const line = (event) => `${event.phase}: ${event.message}${event.total !== undefined ? ` [${event.completed ?? 0}/${event.total}]` : ""} (${Math.floor((Date.now() - started) / 1000)}s)`;
106
+ const clear = () => {
107
+ if (painted) {
108
+ process.stderr.write("\r\x1b[2K");
109
+ painted = false;
110
+ }
111
+ };
112
+ const timer = interactive && level !== "silent"
113
+ ? setInterval(() => {
114
+ if (current) {
115
+ const color = process.env.NO_COLOR === undefined &&
116
+ (options.color === "always" || (options.color !== "never" && process.stderr.isTTY));
117
+ process.stderr.write(`\r\x1b[2K${color ? "\x1b[36m" : ""}${["⠋", "⠙", "⠹", "⠸"][frame++ % 4]} ${clean(line(current))}${bar()}${color ? "\x1b[0m" : ""}`);
118
+ painted = true;
119
+ }
120
+ }, 120)
121
+ : undefined;
122
+ timer?.unref();
123
+ const emit = (event) => {
124
+ const severity = event.level ?? "info";
125
+ if (levels.indexOf(severity) < levels.indexOf(level))
126
+ return;
127
+ current = event;
128
+ if (event.total !== undefined)
129
+ meter =
130
+ event.total > 0
131
+ ? {
132
+ phase: event.phase.split(".")[0],
133
+ completed: event.completed ?? 0,
134
+ total: event.total,
135
+ }
136
+ : undefined;
137
+ else if (meter && meter.completed >= meter.total)
138
+ meter = undefined;
139
+ clear();
140
+ process.stderr.write(json
141
+ ? `${JSON.stringify({ at: new Date().toISOString(), elapsedMs: Date.now() - started, ...event })}\n`
142
+ : `${clean(line(event))}\n`);
143
+ };
144
+ return {
145
+ emit,
146
+ close: () => {
147
+ if (timer)
148
+ clearInterval(timer);
149
+ clear();
150
+ },
151
+ };
152
+ }
153
+ export function cancellation() {
154
+ const controller = new AbortController();
155
+ const interrupt = () => controller.abort(new Error("interrupted"));
156
+ process.on("SIGINT", interrupt);
157
+ process.on("SIGTERM", interrupt);
158
+ process.on("SIGHUP", interrupt);
159
+ return {
160
+ signal: controller.signal,
161
+ dispose: () => {
162
+ process.off("SIGINT", interrupt);
163
+ process.off("SIGTERM", interrupt);
164
+ process.off("SIGHUP", interrupt);
165
+ },
166
+ };
167
+ }
168
+ export function fail(name, error, json = false) {
169
+ if (error instanceof CommanderError && error.exitCode === 0)
170
+ return;
171
+ const message = error instanceof Error ? error.message : String(error);
172
+ const interrupted = /interrupted|aborted/i.test(message);
173
+ process.stderr.write(json ? `${JSON.stringify({ status: "error", message })}\n` : `${name}: ${message}\n`);
174
+ process.exitCode = /timeout|timed out|deadline/i.test(message)
175
+ ? 124
176
+ : interrupted
177
+ ? 130
178
+ : error instanceof CommanderError
179
+ ? 2
180
+ : 1;
181
+ }
@@ -0,0 +1,34 @@
1
+ import { type OperationOptions } from "./operations.js";
2
+ import type { MemomaticContext } from "./service.js";
3
+ import type { MemomaticPaths } from "./paths.js";
4
+ export type InboxReport = {
5
+ filesProcessed: number;
6
+ filesRejected: number;
7
+ entriesAppended: number;
8
+ entriesSuperseded: number;
9
+ entriesDuplicated: number;
10
+ linesForbidden: number;
11
+ linesInvalid: number;
12
+ dryRun: boolean;
13
+ };
14
+ export declare function inboxFileName(source: string, now?: Date): string;
15
+ /**
16
+ * Append one inbox drop file. Producers write Markdown entry lines that the
17
+ * deterministic `process` pass later validates, gates, and moves into the
18
+ * corpus. Nothing here touches corpus files.
19
+ */
20
+ export declare function dropToInbox(paths: MemomaticPaths, lines: string[], source: string): Promise<string>;
21
+ /**
22
+ * Deterministic inbox pass: validate entry lines, enforce never-save rules,
23
+ * deduplicate exact texts, supersede entries sharing a key, route user-origin
24
+ * targets, then rebuild the index with embeddings. No model turns.
25
+ */
26
+ export declare function processInbox(context: MemomaticContext, options?: OperationOptions & {
27
+ dryRun?: boolean;
28
+ deferIndex?: boolean;
29
+ }): Promise<InboxReport>;
30
+ /**
31
+ * Serialize whole runs (dream or process) with a stale-tolerant lock file so a
32
+ * nightly sweep and a manual CLI invocation never mutate the corpus together.
33
+ */
34
+ export declare function withRunLock<T>(paths: MemomaticPaths, work: () => Promise<T>): Promise<T>;