@kisev/memomatic 1.0.0-dev.69.gd644a007a7ee → 1.0.0-dev.71.gee4e37a5bb61
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/README.md +10 -1
- package/README.ru.md +10 -1
- package/dist/cli.js +201 -92
- package/dist/dream.d.ts +17 -3
- package/dist/dream.js +197 -40
- package/dist/executor.d.ts +29 -6
- package/dist/executor.js +256 -45
- package/dist/generated/cli.d.ts +28 -0
- package/dist/generated/cli.js +181 -0
- package/dist/inbox.d.ts +3 -1
- package/dist/inbox.js +27 -6
- package/dist/ingest.d.ts +0 -15
- package/dist/ingest.js +1 -100
- package/dist/ingestion.d.ts +30 -0
- package/dist/ingestion.js +209 -0
- package/dist/operations.d.ts +7 -0
- package/dist/operations.js +3 -0
- package/dist/paths.js +7 -2
- package/dist/search.d.ts +2 -1
- package/dist/search.js +52 -2
- package/dist/service.d.ts +4 -2
- package/dist/service.js +11 -8
- package/dist/settings.d.ts +8 -1
- package/dist/settings.js +31 -3
- package/dist/store.js +2 -0
- package/package.json +4 -3
package/dist/executor.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
2
6
|
export function extractJson(response) {
|
|
3
7
|
const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(response);
|
|
4
|
-
const
|
|
5
|
-
|
|
8
|
+
for (const candidate of [fenced?.[1], response]) {
|
|
9
|
+
if (!candidate)
|
|
10
|
+
continue;
|
|
6
11
|
const start = candidate.indexOf("{");
|
|
7
12
|
const end = candidate.lastIndexOf("}");
|
|
8
|
-
if (start
|
|
13
|
+
if (start < 0 || end <= start)
|
|
9
14
|
continue;
|
|
10
15
|
try {
|
|
11
16
|
return JSON.parse(candidate.slice(start, end + 1));
|
|
@@ -19,60 +24,266 @@ export function extractJson(response) {
|
|
|
19
24
|
export class OpenCodeExecutor {
|
|
20
25
|
options;
|
|
21
26
|
name = "opencode";
|
|
27
|
+
usage = {
|
|
28
|
+
calls: 0,
|
|
29
|
+
retries: 0,
|
|
30
|
+
inputTokens: 0,
|
|
31
|
+
outputTokens: 0,
|
|
32
|
+
cacheReadTokens: 0,
|
|
33
|
+
cacheWriteTokens: 0,
|
|
34
|
+
usageAvailable: false,
|
|
35
|
+
charactersSent: 0,
|
|
36
|
+
};
|
|
37
|
+
child;
|
|
38
|
+
directory;
|
|
39
|
+
url;
|
|
40
|
+
authorization;
|
|
22
41
|
constructor(options) {
|
|
23
42
|
this.options = options;
|
|
24
43
|
}
|
|
44
|
+
async initialize() {
|
|
45
|
+
if (this.url)
|
|
46
|
+
return;
|
|
47
|
+
if (this.options.url) {
|
|
48
|
+
const url = new URL(this.options.url);
|
|
49
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password)
|
|
50
|
+
throw new Error("invalid OpenCode URL");
|
|
51
|
+
this.url = url.href.replace(/\/$/, "");
|
|
52
|
+
if (process.env.OPENCODE_SERVER_PASSWORD)
|
|
53
|
+
this.authorization = `Basic ${Buffer.from(`${process.env.OPENCODE_SERVER_USERNAME ?? "opencode"}:${process.env.OPENCODE_SERVER_PASSWORD}`).toString("base64")}`;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let inherited = {};
|
|
57
|
+
if (process.env.OPENCODE_CONFIG_CONTENT) {
|
|
58
|
+
try {
|
|
59
|
+
const value = JSON.parse(process.env.OPENCODE_CONFIG_CONTENT);
|
|
60
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
61
|
+
throw new Error();
|
|
62
|
+
inherited = value;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
throw new Error("OpenCode inline configuration must be a JSON object");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
this.directory = await mkdtemp(join(tmpdir(), "memomatic-opencode-"));
|
|
69
|
+
const password = randomBytes(24).toString("hex");
|
|
70
|
+
this.authorization = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
|
|
71
|
+
this.options.observe?.({
|
|
72
|
+
phase: "opencode.start",
|
|
73
|
+
message: "Starting one isolated OpenCode server for this run",
|
|
74
|
+
});
|
|
75
|
+
const child = spawn(this.options.binary ?? "opencode", ["serve", "--hostname", "127.0.0.1", "--port", "0", "--pure"], {
|
|
76
|
+
cwd: this.directory,
|
|
77
|
+
detached: process.platform !== "win32",
|
|
78
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
79
|
+
env: {
|
|
80
|
+
...process.env,
|
|
81
|
+
OPENCODE_SERVER_USERNAME: "opencode",
|
|
82
|
+
OPENCODE_SERVER_PASSWORD: password,
|
|
83
|
+
OPENCODE_DISABLE_PROJECT_CONFIG: "1",
|
|
84
|
+
OPENCODE_DISABLE_EXTERNAL_SKILLS: "1",
|
|
85
|
+
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS: "1",
|
|
86
|
+
OPENCODE_DISABLE_DEFAULT_PLUGINS: "1",
|
|
87
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
88
|
+
...inherited,
|
|
89
|
+
permission: "deny",
|
|
90
|
+
tools: { "*": false },
|
|
91
|
+
agent: {
|
|
92
|
+
...inherited.agent,
|
|
93
|
+
memomatic: {
|
|
94
|
+
mode: "primary",
|
|
95
|
+
description: "Bounded memory extraction",
|
|
96
|
+
prompt: "Return only the requested JSON. Do not use tools.",
|
|
97
|
+
permission: "deny",
|
|
98
|
+
tools: { "*": false },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
this.child = child;
|
|
105
|
+
this.url = await new Promise((resolve, reject) => {
|
|
106
|
+
let output = "";
|
|
107
|
+
const timeout = setTimeout(() => done(new Error("OpenCode startup timed out")), 30000);
|
|
108
|
+
const abort = () => done(this.options.signal?.reason instanceof Error
|
|
109
|
+
? this.options.signal.reason
|
|
110
|
+
: new Error("interrupted"));
|
|
111
|
+
const failed = (error) => done(error);
|
|
112
|
+
const closed = () => done(new Error("OpenCode exited before becoming ready"));
|
|
113
|
+
const data = (chunk) => {
|
|
114
|
+
output = (output + chunk.toString()).slice(-8192);
|
|
115
|
+
const match = /https?:\/\/127\.0\.0\.1:\d+/.exec(output);
|
|
116
|
+
if (match)
|
|
117
|
+
done(undefined, match[0]);
|
|
118
|
+
};
|
|
119
|
+
const done = (error, url) => {
|
|
120
|
+
clearTimeout(timeout);
|
|
121
|
+
child.stdout?.off("data", data);
|
|
122
|
+
child.stderr?.off("data", data);
|
|
123
|
+
child.off("error", failed);
|
|
124
|
+
child.off("exit", closed);
|
|
125
|
+
this.options.signal?.removeEventListener("abort", abort);
|
|
126
|
+
if (error)
|
|
127
|
+
reject(error);
|
|
128
|
+
else
|
|
129
|
+
resolve(url);
|
|
130
|
+
};
|
|
131
|
+
child.stdout?.on("data", data);
|
|
132
|
+
child.stderr?.on("data", data);
|
|
133
|
+
child.once("error", failed);
|
|
134
|
+
child.once("exit", closed);
|
|
135
|
+
this.options.signal?.addEventListener("abort", abort, { once: true });
|
|
136
|
+
if (this.options.signal?.aborted)
|
|
137
|
+
abort();
|
|
138
|
+
});
|
|
139
|
+
// Drain logs without retaining provider output, credentials or prompts.
|
|
140
|
+
child.stdout?.resume();
|
|
141
|
+
child.stderr?.resume();
|
|
142
|
+
this.options.observe?.({ phase: "opencode.ready", message: "OpenCode server ready" });
|
|
143
|
+
}
|
|
144
|
+
async request(path, body, signal) {
|
|
145
|
+
const response = await fetch(`${this.url}${path}`, {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: {
|
|
148
|
+
"content-type": "application/json",
|
|
149
|
+
...(this.authorization ? { authorization: this.authorization } : {}),
|
|
150
|
+
},
|
|
151
|
+
body: JSON.stringify(body),
|
|
152
|
+
signal,
|
|
153
|
+
});
|
|
154
|
+
if (!response.ok)
|
|
155
|
+
throw new Error(`OpenCode ${path.includes("message") ? "model request" : "session request"} failed: HTTP ${response.status}`);
|
|
156
|
+
return (await response.json());
|
|
157
|
+
}
|
|
25
158
|
async complete(request) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
159
|
+
this.options.signal?.throwIfAborted();
|
|
160
|
+
await this.initialize();
|
|
161
|
+
for (let attempt = 0; attempt <= (this.options.retries ?? 1); attempt++) {
|
|
162
|
+
this.options.signal?.throwIfAborted();
|
|
163
|
+
const signal = AbortSignal.any([
|
|
164
|
+
AbortSignal.timeout(this.options.timeoutMs ?? 180000),
|
|
165
|
+
...(this.options.signal ? [this.options.signal] : []),
|
|
166
|
+
]);
|
|
167
|
+
let id;
|
|
168
|
+
const started = Date.now();
|
|
169
|
+
const heartbeat = setInterval(() => this.options.observe?.({
|
|
170
|
+
phase: "model.wait",
|
|
171
|
+
message: "Waiting for OpenCode/model response",
|
|
172
|
+
elapsedMs: Date.now() - started,
|
|
173
|
+
attempt: attempt + 1,
|
|
174
|
+
}), 15000);
|
|
175
|
+
heartbeat.unref();
|
|
34
176
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
177
|
+
const session = await this.request("/session", {
|
|
178
|
+
title: "[memomatic-internal] memory extraction",
|
|
179
|
+
permission: [{ permission: "*", pattern: "*", action: "deny" }],
|
|
180
|
+
}, signal);
|
|
181
|
+
if (typeof session.id !== "string")
|
|
182
|
+
throw new Error("OpenCode session response is malformed");
|
|
183
|
+
id = session.id;
|
|
184
|
+
const slash = this.options.model?.indexOf("/") ?? -1;
|
|
185
|
+
if (this.options.model && slash < 1)
|
|
186
|
+
throw new Error("model must use provider/model format");
|
|
187
|
+
this.usage.calls++;
|
|
188
|
+
this.usage.charactersSent += request.system.length + request.prompt.length;
|
|
189
|
+
this.options.observe?.({
|
|
190
|
+
phase: "model.request",
|
|
191
|
+
message: "Sending prepared fragment to OpenCode",
|
|
192
|
+
characters: request.prompt.length,
|
|
193
|
+
attempt: attempt + 1,
|
|
194
|
+
});
|
|
195
|
+
const response = await this.request(`/session/${encodeURIComponent(id)}/message`, {
|
|
196
|
+
agent: this.options.url ? "build" : "memomatic",
|
|
197
|
+
system: request.system,
|
|
198
|
+
tools: { "*": false },
|
|
199
|
+
...(this.options.model
|
|
200
|
+
? {
|
|
201
|
+
model: {
|
|
202
|
+
providerID: this.options.model.slice(0, slash),
|
|
203
|
+
modelID: this.options.model.slice(slash + 1),
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
: {}),
|
|
207
|
+
...(this.options.variant ? { variant: this.options.variant } : {}),
|
|
208
|
+
parts: [{ type: "text", text: `[memomatic-internal] ${request.prompt}` }],
|
|
209
|
+
}, signal);
|
|
210
|
+
const info = response.info;
|
|
211
|
+
if (info?.error)
|
|
212
|
+
throw new Error("OpenCode model returned an error; inspect the configured provider");
|
|
213
|
+
if (info?.tokens) {
|
|
214
|
+
this.usage.usageAvailable = true;
|
|
215
|
+
this.usage.inputTokens += info.tokens.input ?? 0;
|
|
216
|
+
this.usage.outputTokens += info.tokens.output ?? 0;
|
|
217
|
+
this.usage.cacheReadTokens += info.tokens.cache?.read ?? 0;
|
|
218
|
+
this.usage.cacheWriteTokens += info.tokens.cache?.write ?? 0;
|
|
44
219
|
}
|
|
45
|
-
const
|
|
220
|
+
const parts = response.parts;
|
|
221
|
+
if (!Array.isArray(parts))
|
|
222
|
+
throw new Error("OpenCode model response is malformed");
|
|
223
|
+
const parsed = extractJson(parts
|
|
224
|
+
.filter((part) => part.type === "text")
|
|
225
|
+
.map((part) => part.text ?? "")
|
|
226
|
+
.join("\n"));
|
|
227
|
+
this.options.observe?.({
|
|
228
|
+
phase: "model.done",
|
|
229
|
+
message: "Model response received",
|
|
230
|
+
elapsedMs: Date.now() - started,
|
|
231
|
+
...this.usage,
|
|
232
|
+
});
|
|
46
233
|
return JSON.stringify(parsed);
|
|
47
234
|
}
|
|
48
|
-
catch {
|
|
49
|
-
if (
|
|
50
|
-
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (id)
|
|
237
|
+
await this.request(`/session/${encodeURIComponent(id)}/abort`, {}, AbortSignal.timeout(3000)).catch(() => undefined);
|
|
238
|
+
if (this.options.signal?.aborted)
|
|
239
|
+
throw this.options.signal.reason;
|
|
240
|
+
if (attempt === (this.options.retries ?? 1))
|
|
241
|
+
throw new Error(`OpenCode request failed after ${attempt + 1} attempt(s): ${error.message}`);
|
|
242
|
+
this.usage.retries++;
|
|
243
|
+
this.options.observe?.({
|
|
244
|
+
phase: "model.retry",
|
|
245
|
+
message: "Retrying failed model request",
|
|
246
|
+
level: "warn",
|
|
247
|
+
attempt: attempt + 2,
|
|
248
|
+
reason: error.message,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
clearInterval(heartbeat);
|
|
51
253
|
}
|
|
52
254
|
}
|
|
53
255
|
throw new Error("unreachable");
|
|
54
256
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
resolve(
|
|
73
|
-
|
|
74
|
-
|
|
257
|
+
async close() {
|
|
258
|
+
const child = this.child;
|
|
259
|
+
if (child?.pid && child.exitCode === null && child.signalCode === null) {
|
|
260
|
+
await new Promise((resolve) => {
|
|
261
|
+
const kill = (signal) => {
|
|
262
|
+
try {
|
|
263
|
+
if (process.platform !== "win32" && child.pid)
|
|
264
|
+
process.kill(-child.pid, signal);
|
|
265
|
+
else
|
|
266
|
+
child.kill(signal);
|
|
267
|
+
}
|
|
268
|
+
catch { }
|
|
269
|
+
};
|
|
270
|
+
const timer = setTimeout(() => kill("SIGKILL"), 1000);
|
|
271
|
+
timer.unref();
|
|
272
|
+
child.once("close", () => {
|
|
273
|
+
clearTimeout(timer);
|
|
274
|
+
resolve();
|
|
275
|
+
});
|
|
276
|
+
kill("SIGTERM");
|
|
75
277
|
});
|
|
76
|
-
}
|
|
278
|
+
}
|
|
279
|
+
if (child?.pid && process.platform !== "win32") {
|
|
280
|
+
try {
|
|
281
|
+
process.kill(-child.pid, "SIGKILL");
|
|
282
|
+
}
|
|
283
|
+
catch { }
|
|
284
|
+
}
|
|
285
|
+
this.child = undefined;
|
|
286
|
+
if (this.directory)
|
|
287
|
+
await rm(this.directory, { recursive: true, force: true });
|
|
77
288
|
}
|
|
78
289
|
}
|
|
@@ -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
|
+
}
|
package/dist/inbox.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type OperationOptions } from "./operations.js";
|
|
1
2
|
import type { MemomaticContext } from "./service.js";
|
|
2
3
|
import type { MemomaticPaths } from "./paths.js";
|
|
3
4
|
export type InboxReport = {
|
|
@@ -22,8 +23,9 @@ export declare function dropToInbox(paths: MemomaticPaths, lines: string[], sour
|
|
|
22
23
|
* deduplicate exact texts, supersede entries sharing a key, route user-origin
|
|
23
24
|
* targets, then rebuild the index with embeddings. No model turns.
|
|
24
25
|
*/
|
|
25
|
-
export declare function processInbox(context: MemomaticContext, options?: {
|
|
26
|
+
export declare function processInbox(context: MemomaticContext, options?: OperationOptions & {
|
|
26
27
|
dryRun?: boolean;
|
|
28
|
+
deferIndex?: boolean;
|
|
27
29
|
}): Promise<InboxReport>;
|
|
28
30
|
/**
|
|
29
31
|
* Serialize whole runs (dream or process) with a stale-tolerant lock file so a
|
package/dist/inbox.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, open, readdir, rename, rm, unlink } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { check } from "./operations.js";
|
|
4
5
|
import { writeAtomic } from "@kisev/safe-fs";
|
|
5
6
|
import { appendDailyEntry, readTextIfExists, replaceEntryLine, writeCorpusFile } from "./corpus.js";
|
|
6
7
|
import { entryLine, parseEntryLine } from "./entries.js";
|
|
@@ -87,6 +88,13 @@ export async function processInbox(context, options = {}) {
|
|
|
87
88
|
return report;
|
|
88
89
|
const corpus = await scanCorpus(context.paths);
|
|
89
90
|
for (const name of names) {
|
|
91
|
+
check(options);
|
|
92
|
+
options.observe?.({
|
|
93
|
+
phase: "inbox",
|
|
94
|
+
message: "Processing inbox file",
|
|
95
|
+
completed: report.filesProcessed + report.filesRejected,
|
|
96
|
+
total: names.length,
|
|
97
|
+
});
|
|
90
98
|
const markdown = (await readTextIfExists(join(context.paths.inboxDir, name))) ?? "";
|
|
91
99
|
const rawLines = markdown.split("\n");
|
|
92
100
|
const parsedLines = rawLines.filter((line) => line.trim()).map((line) => parseEntryLine(line));
|
|
@@ -108,6 +116,7 @@ export async function processInbox(context, options = {}) {
|
|
|
108
116
|
}
|
|
109
117
|
let accepted = false;
|
|
110
118
|
for (const parsed of allowed) {
|
|
119
|
+
check(options);
|
|
111
120
|
if (corpus.texts.has(parsed.text)) {
|
|
112
121
|
report.entriesDuplicated += 1;
|
|
113
122
|
continue;
|
|
@@ -151,8 +160,8 @@ export async function processInbox(context, options = {}) {
|
|
|
151
160
|
report.filesRejected += 1;
|
|
152
161
|
}
|
|
153
162
|
}
|
|
154
|
-
if (!dryRun)
|
|
155
|
-
await reindex(context.paths, context.settings, context.store);
|
|
163
|
+
if (!dryRun && !options.deferIndex)
|
|
164
|
+
await reindex(context.paths, context.settings, context.store, options);
|
|
156
165
|
return report;
|
|
157
166
|
}
|
|
158
167
|
/**
|
|
@@ -160,11 +169,12 @@ export async function processInbox(context, options = {}) {
|
|
|
160
169
|
* nightly sweep and a manual CLI invocation never mutate the corpus together.
|
|
161
170
|
*/
|
|
162
171
|
export async function withRunLock(paths, work) {
|
|
172
|
+
const owner = `${process.pid} ${new Date().toISOString()} ${randomBytes(12).toString("hex")}\n`;
|
|
163
173
|
const acquire = async () => {
|
|
164
174
|
try {
|
|
165
175
|
const handle = await open(paths.runLockFile, "wx", 0o600);
|
|
166
176
|
try {
|
|
167
|
-
await handle.writeFile(
|
|
177
|
+
await handle.writeFile(owner);
|
|
168
178
|
}
|
|
169
179
|
finally {
|
|
170
180
|
await handle.close();
|
|
@@ -178,14 +188,24 @@ export async function withRunLock(paths, work) {
|
|
|
178
188
|
const handle = await open(paths.runLockFile, "r");
|
|
179
189
|
try {
|
|
180
190
|
const info = await handle.stat();
|
|
181
|
-
|
|
191
|
+
const owner = Number.parseInt((await handle.readFile("utf8")).split(" ")[0], 10);
|
|
192
|
+
if (Number.isSafeInteger(owner) && owner > 0) {
|
|
193
|
+
try {
|
|
194
|
+
process.kill(owner, 0);
|
|
195
|
+
}
|
|
196
|
+
catch (cause) {
|
|
197
|
+
stale = cause.code === "ESRCH";
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else
|
|
201
|
+
stale = Date.now() - info.mtimeMs > LOCK_STALE_MS;
|
|
182
202
|
}
|
|
183
203
|
finally {
|
|
184
204
|
await handle.close();
|
|
185
205
|
}
|
|
186
206
|
}
|
|
187
207
|
catch {
|
|
188
|
-
return;
|
|
208
|
+
return acquire();
|
|
189
209
|
}
|
|
190
210
|
if (!stale)
|
|
191
211
|
throw new Error("another memomatic run is active");
|
|
@@ -198,6 +218,7 @@ export async function withRunLock(paths, work) {
|
|
|
198
218
|
return await work();
|
|
199
219
|
}
|
|
200
220
|
finally {
|
|
201
|
-
await
|
|
221
|
+
if ((await readTextIfExists(paths.runLockFile)) === owner)
|
|
222
|
+
await rm(paths.runLockFile, { force: true }).catch(() => undefined);
|
|
202
223
|
}
|
|
203
224
|
}
|
package/dist/ingest.d.ts
CHANGED
|
@@ -1,16 +1 @@
|
|
|
1
|
-
export type IngestSession = {
|
|
2
|
-
id: string;
|
|
3
|
-
title: string;
|
|
4
|
-
directory: string;
|
|
5
|
-
timeCreated: number;
|
|
6
|
-
messages: Array<{
|
|
7
|
-
role: string;
|
|
8
|
-
text: string;
|
|
9
|
-
}>;
|
|
10
|
-
};
|
|
11
1
|
export declare function opencodeDatabasePath(): string;
|
|
12
|
-
export declare function loadRecentSessions(databaseFile: string, afterTimeCreated: number, options?: {
|
|
13
|
-
maxSessions: number;
|
|
14
|
-
maxCharsPerSession: number;
|
|
15
|
-
before?: number;
|
|
16
|
-
}): IngestSession[];
|