@nordbyte/nordrelay 0.2.1 → 0.3.1
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/.env.example +22 -0
- package/CHANGELOG.md +26 -0
- package/README.md +147 -19
- package/dist/access-control.js +6 -0
- package/dist/agent-adapter.js +60 -0
- package/dist/audit-log.js +54 -0
- package/dist/bot-preferences.js +13 -9
- package/dist/bot-ui.js +6 -0
- package/dist/bot.js +526 -26
- package/dist/channel-adapter.js +58 -0
- package/dist/codex-session.js +3 -1
- package/dist/config.js +47 -0
- package/dist/context-key.js +23 -0
- package/dist/index.js +47 -2
- package/dist/logger.js +24 -1
- package/dist/operations.js +340 -15
- package/dist/prompt-store.js +33 -11
- package/dist/relay-runtime.js +908 -0
- package/dist/session-locks.js +81 -0
- package/dist/session-registry.js +11 -7
- package/dist/settings-service.js +253 -0
- package/dist/state-backend.js +83 -0
- package/dist/web-dashboard.js +890 -0
- package/dist/web-state.js +131 -0
- package/docker-compose.yml +1 -1
- package/package.json +4 -1
- package/plugins/nordrelay/.codex-plugin/plugin.json +1 -1
- package/plugins/nordrelay/scripts/nordrelay.mjs +235 -13
package/dist/operations.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { closeSync, existsSync, openSync } from "node:fs";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { describeCodexCli, resolveCodexCli } from "./codex-cli.js";
|
|
7
7
|
import { findLatestDatabase } from "./codex-state.js";
|
|
8
8
|
import { describePiCli, resolvePiCli } from "./pi-cli.js";
|
|
9
9
|
const APP_NAME = "nordrelay";
|
|
10
|
-
const
|
|
10
|
+
const PACKAGE_NAME = "@nordbyte/nordrelay";
|
|
11
|
+
const CODEX_PACKAGE_NAME = "@openai/codex";
|
|
12
|
+
const PI_PACKAGE_NAME = "@mariozechner/pi-coding-agent";
|
|
13
|
+
const DEFAULT_HOME = path.join(os.homedir(), ".nordrelay");
|
|
11
14
|
const SECRET_RE = /(bot|token|api[_-]?key|authorization|bearer|password|secret)(["'=: ]+)([^\s"',]+)/gi;
|
|
15
|
+
const DEFAULT_VERSION_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
12
16
|
export function getConnectorHome() {
|
|
13
17
|
return process.env.NORDRELAY_HOME || DEFAULT_HOME;
|
|
14
18
|
}
|
|
@@ -39,6 +43,30 @@ export async function readLogTail(lines = 80, filePath = getConnectorLogPath())
|
|
|
39
43
|
return `Cannot read log: ${error instanceof Error ? error.message : String(error)}`;
|
|
40
44
|
}
|
|
41
45
|
}
|
|
46
|
+
export async function readFormattedLogTail(lines = 80, filePath = getConnectorLogPath()) {
|
|
47
|
+
const boundedLines = Math.min(Math.max(lines, 1), 300);
|
|
48
|
+
try {
|
|
49
|
+
const [contents, stats] = await Promise.all([readFile(filePath, "utf8"), stat(filePath)]);
|
|
50
|
+
const rawLines = contents.split(/\r?\n/).filter((line) => line.trim().length > 0).slice(-boundedLines);
|
|
51
|
+
const formatted = rawLines.map(formatLogLine).join("\n");
|
|
52
|
+
return {
|
|
53
|
+
filePath,
|
|
54
|
+
requestedLines: boundedLines,
|
|
55
|
+
lineCount: rawLines.length,
|
|
56
|
+
updatedAt: stats.mtime,
|
|
57
|
+
plain: redactSecrets(formatted),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
filePath,
|
|
63
|
+
requestedLines: boundedLines,
|
|
64
|
+
lineCount: 0,
|
|
65
|
+
updatedAt: null,
|
|
66
|
+
plain: `Cannot read log: ${error instanceof Error ? error.message : String(error)}`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
42
70
|
export async function getPackageVersion() {
|
|
43
71
|
try {
|
|
44
72
|
const pkg = JSON.parse(await readFile(path.join(getSourceRoot(), "package.json"), "utf8"));
|
|
@@ -48,18 +76,55 @@ export async function getPackageVersion() {
|
|
|
48
76
|
return "unknown";
|
|
49
77
|
}
|
|
50
78
|
}
|
|
79
|
+
export async function getVersionChecks(options = {}) {
|
|
80
|
+
const nordrelayVersion = await getPackageVersion();
|
|
81
|
+
const codexCli = resolveCodexCli();
|
|
82
|
+
const piCli = resolvePiCli(process.env, options.piCliPath);
|
|
83
|
+
const codexVersionLabel = codexCli.path
|
|
84
|
+
? detectCliVersion(codexCli.path)
|
|
85
|
+
: readInstalledPackageVersion(CODEX_PACKAGE_NAME) ?? "not installed";
|
|
86
|
+
const piVersionLabel = piCli.path ? detectCliVersion(piCli.path) : "not installed";
|
|
87
|
+
return {
|
|
88
|
+
nordrelay: buildVersionCheck({
|
|
89
|
+
label: "NordRelay",
|
|
90
|
+
packageName: PACKAGE_NAME,
|
|
91
|
+
installedLabel: nordrelayVersion,
|
|
92
|
+
installedVersion: extractVersion(nordrelayVersion),
|
|
93
|
+
}),
|
|
94
|
+
codex: buildVersionCheck({
|
|
95
|
+
label: "Codex",
|
|
96
|
+
packageName: CODEX_PACKAGE_NAME,
|
|
97
|
+
installedLabel: codexVersionLabel,
|
|
98
|
+
installedVersion: extractVersion(codexVersionLabel),
|
|
99
|
+
notInstalled: codexVersionLabel === "not installed",
|
|
100
|
+
}),
|
|
101
|
+
pi: buildVersionCheck({
|
|
102
|
+
label: "Pi",
|
|
103
|
+
packageName: PI_PACKAGE_NAME,
|
|
104
|
+
installedLabel: piVersionLabel,
|
|
105
|
+
installedVersion: extractVersion(piVersionLabel),
|
|
106
|
+
notInstalled: piVersionLabel === "not installed",
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
51
110
|
export async function getConnectorHealth() {
|
|
52
111
|
const state = await readConnectorState();
|
|
53
112
|
const version = await getPackageVersion();
|
|
54
113
|
const pidRunning = isProcessRunning(state.pid);
|
|
55
114
|
const appPidRunning = isProcessRunning(state.appPid);
|
|
115
|
+
const codexCli = resolveCodexCli();
|
|
116
|
+
const piCli = resolvePiCli();
|
|
56
117
|
return {
|
|
57
118
|
version,
|
|
58
119
|
state,
|
|
59
120
|
pidRunning,
|
|
60
121
|
appPidRunning,
|
|
61
|
-
codexCli: describeCodexCli(
|
|
62
|
-
|
|
122
|
+
codexCli: describeCodexCli(codexCli),
|
|
123
|
+
codexCliPath: codexCli.path ?? null,
|
|
124
|
+
codexCliVersion: detectCliVersion(codexCli.path),
|
|
125
|
+
piCli: describePiCli(piCli),
|
|
126
|
+
piCliPath: piCli.path ?? null,
|
|
127
|
+
piCliVersion: detectCliVersion(piCli.path),
|
|
63
128
|
stateFile: getConnectorStatePath(),
|
|
64
129
|
logFile: getConnectorLogPath(),
|
|
65
130
|
databasePath: findLatestDatabase(),
|
|
@@ -80,17 +145,15 @@ export function spawnSelfUpdate() {
|
|
|
80
145
|
const sourceRoot = getSourceRoot();
|
|
81
146
|
const script = getWrapperScriptPath();
|
|
82
147
|
const updateLog = getUpdateLogPath();
|
|
148
|
+
const method = detectSelfUpdateMethod(sourceRoot);
|
|
149
|
+
const commands = method === "npm"
|
|
150
|
+
? buildNpmSelfUpdateCommands()
|
|
151
|
+
: buildGitSelfUpdateCommands(script);
|
|
83
152
|
const logFd = openSync(updateLog, "a");
|
|
84
153
|
const command = [
|
|
85
154
|
"set -e",
|
|
86
|
-
`printf '\\n[%s] Starting connector self-update\\n' "$(date -Is)"`,
|
|
87
|
-
|
|
88
|
-
"npm install",
|
|
89
|
-
"npm run check",
|
|
90
|
-
"npm test",
|
|
91
|
-
"npm run build",
|
|
92
|
-
`printf '[%s] Checks passed; restarting connector\\n' "$(date -Is)"`,
|
|
93
|
-
`${shellQuote(process.execPath)} ${shellQuote(script)} restart --keep-pending-updates`,
|
|
155
|
+
`printf '\\n[%s] Starting ${method} connector self-update\\n' "$(date -Is)"`,
|
|
156
|
+
...commands,
|
|
94
157
|
].join(" && ");
|
|
95
158
|
const child = spawn("sh", ["-lc", command], {
|
|
96
159
|
cwd: sourceRoot,
|
|
@@ -100,11 +163,25 @@ export function spawnSelfUpdate() {
|
|
|
100
163
|
});
|
|
101
164
|
child.unref();
|
|
102
165
|
closeSync(logFd);
|
|
103
|
-
return
|
|
166
|
+
return {
|
|
167
|
+
logPath: updateLog,
|
|
168
|
+
method,
|
|
169
|
+
sourceRoot,
|
|
170
|
+
summary: method === "npm"
|
|
171
|
+
? `Install latest ${PACKAGE_NAME} with npm, verify the CLI, and restart.`
|
|
172
|
+
: "Pull origin/main, install dependencies, run check, tests, build, and restart.",
|
|
173
|
+
};
|
|
104
174
|
}
|
|
105
175
|
export function getSourceRoot() {
|
|
106
176
|
return process.env.NORDRELAY_SOURCE_ROOT || process.cwd();
|
|
107
177
|
}
|
|
178
|
+
export function detectSelfUpdateMethod(sourceRoot = getSourceRoot()) {
|
|
179
|
+
const override = process.env.NORDRELAY_UPDATE_METHOD?.trim().toLowerCase();
|
|
180
|
+
if (override === "npm" || override === "git") {
|
|
181
|
+
return override;
|
|
182
|
+
}
|
|
183
|
+
return existsSync(path.join(sourceRoot, ".git")) ? "git" : "npm";
|
|
184
|
+
}
|
|
108
185
|
function getWrapperScriptPath() {
|
|
109
186
|
const sourceRoot = getSourceRoot();
|
|
110
187
|
const script = path.join(sourceRoot, "plugins", APP_NAME, "scripts", `${APP_NAME}.mjs`);
|
|
@@ -128,6 +205,254 @@ function isProcessRunning(pid) {
|
|
|
128
205
|
function redactSecrets(text) {
|
|
129
206
|
return text.replace(SECRET_RE, "$1$2[redacted]");
|
|
130
207
|
}
|
|
208
|
+
function buildGitSelfUpdateCommands(script) {
|
|
209
|
+
return [
|
|
210
|
+
"git pull --ff-only origin main",
|
|
211
|
+
"npm install",
|
|
212
|
+
"npm run check",
|
|
213
|
+
"npm test",
|
|
214
|
+
"npm run build",
|
|
215
|
+
`printf '[%s] Checks passed; restarting connector\\n' "$(date -Is)"`,
|
|
216
|
+
`${shellQuote(process.execPath)} ${shellQuote(script)} restart --keep-pending-updates`,
|
|
217
|
+
];
|
|
218
|
+
}
|
|
219
|
+
function buildNpmSelfUpdateCommands() {
|
|
220
|
+
return [
|
|
221
|
+
`${resolveNpmCommand()} install -g ${PACKAGE_NAME}@latest`,
|
|
222
|
+
"nordrelay version",
|
|
223
|
+
`printf '[%s] npm update finished; restarting connector\\n' "$(date -Is)"`,
|
|
224
|
+
"nordrelay restart --keep-pending-updates",
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
function resolveNpmCommand() {
|
|
228
|
+
const npmExecPath = process.env.npm_execpath;
|
|
229
|
+
if (npmExecPath && existsSync(npmExecPath)) {
|
|
230
|
+
return `${shellQuote(process.execPath)} ${shellQuote(npmExecPath)}`;
|
|
231
|
+
}
|
|
232
|
+
return "npm";
|
|
233
|
+
}
|
|
234
|
+
function detectCliVersion(commandPath) {
|
|
235
|
+
if (!commandPath) {
|
|
236
|
+
return "not installed";
|
|
237
|
+
}
|
|
238
|
+
const result = spawnSync(commandPath, ["--version"], {
|
|
239
|
+
encoding: "utf8",
|
|
240
|
+
timeout: 3000,
|
|
241
|
+
windowsHide: true,
|
|
242
|
+
});
|
|
243
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
244
|
+
if (result.error) {
|
|
245
|
+
return `unavailable (${result.error.message})`;
|
|
246
|
+
}
|
|
247
|
+
if (result.status !== 0) {
|
|
248
|
+
return output ? `unavailable (${output})` : `unavailable (exit ${result.status ?? "unknown"})`;
|
|
249
|
+
}
|
|
250
|
+
return output || "unknown";
|
|
251
|
+
}
|
|
252
|
+
function buildVersionCheck(options) {
|
|
253
|
+
if (options.notInstalled) {
|
|
254
|
+
return {
|
|
255
|
+
label: options.label,
|
|
256
|
+
packageName: options.packageName,
|
|
257
|
+
installedLabel: "not installed",
|
|
258
|
+
installedVersion: null,
|
|
259
|
+
latestVersion: null,
|
|
260
|
+
status: "not-installed",
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const latest = detectLatestNpmVersion(options.packageName);
|
|
264
|
+
if (!options.installedVersion || !latest.version) {
|
|
265
|
+
return {
|
|
266
|
+
label: options.label,
|
|
267
|
+
packageName: options.packageName,
|
|
268
|
+
installedLabel: options.installedLabel,
|
|
269
|
+
installedVersion: options.installedVersion,
|
|
270
|
+
latestVersion: latest.version,
|
|
271
|
+
status: "unknown",
|
|
272
|
+
detail: latest.error ?? "Could not parse installed version",
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
label: options.label,
|
|
277
|
+
packageName: options.packageName,
|
|
278
|
+
installedLabel: options.installedLabel,
|
|
279
|
+
installedVersion: options.installedVersion,
|
|
280
|
+
latestVersion: latest.version,
|
|
281
|
+
status: compareVersions(options.installedVersion, latest.version) < 0 ? "outdated" : "current",
|
|
282
|
+
detail: latest.error,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function detectLatestNpmVersion(packageName) {
|
|
286
|
+
const cached = readVersionCache(packageName);
|
|
287
|
+
if (cached) {
|
|
288
|
+
return cached;
|
|
289
|
+
}
|
|
290
|
+
const result = spawnSync("npm", ["view", packageName, "version", "--registry=https://registry.npmjs.org"], {
|
|
291
|
+
encoding: "utf8",
|
|
292
|
+
timeout: 5000,
|
|
293
|
+
windowsHide: true,
|
|
294
|
+
});
|
|
295
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
296
|
+
if (result.error) {
|
|
297
|
+
return { version: null, error: result.error.message };
|
|
298
|
+
}
|
|
299
|
+
if (result.status !== 0) {
|
|
300
|
+
return { version: null, error: output || `npm exited ${result.status ?? "unknown"}` };
|
|
301
|
+
}
|
|
302
|
+
const resolved = { version: output.split(/\r?\n/).at(-1)?.trim() || null };
|
|
303
|
+
writeVersionCache(packageName, resolved.version);
|
|
304
|
+
return resolved;
|
|
305
|
+
}
|
|
306
|
+
function readVersionCache(packageName) {
|
|
307
|
+
const ttlMs = parseVersionCacheTtlMs();
|
|
308
|
+
if (ttlMs <= 0) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
const payload = JSON.parse(readFileSync(getVersionCachePath(), "utf8"));
|
|
313
|
+
const entry = payload.packages?.[packageName];
|
|
314
|
+
if (!entry || typeof entry.version !== "string" || typeof entry.checkedAt !== "number") {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
if (Date.now() - entry.checkedAt > ttlMs) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
return { version: entry.version };
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function writeVersionCache(packageName, version) {
|
|
327
|
+
if (!version || parseVersionCacheTtlMs() <= 0) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const filePath = getVersionCachePath();
|
|
331
|
+
try {
|
|
332
|
+
const existing = existsSync(filePath)
|
|
333
|
+
? JSON.parse(readFileSync(filePath, "utf8"))
|
|
334
|
+
: {};
|
|
335
|
+
const packages = existing.packages ?? {};
|
|
336
|
+
packages[packageName] = { version, checkedAt: Date.now() };
|
|
337
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
338
|
+
writeFileSync(filePath, `${JSON.stringify({ packages }, null, 2)}\n`, "utf8");
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
// Best-effort cache only.
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function getVersionCachePath() {
|
|
345
|
+
return path.join(getConnectorHome(), "version-cache.json");
|
|
346
|
+
}
|
|
347
|
+
function parseVersionCacheTtlMs() {
|
|
348
|
+
const raw = process.env.NORDRELAY_VERSION_CACHE_TTL_MS;
|
|
349
|
+
if (!raw) {
|
|
350
|
+
return DEFAULT_VERSION_CACHE_TTL_MS;
|
|
351
|
+
}
|
|
352
|
+
const parsed = Number(raw);
|
|
353
|
+
return Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : DEFAULT_VERSION_CACHE_TTL_MS;
|
|
354
|
+
}
|
|
355
|
+
function readInstalledPackageVersion(packageName) {
|
|
356
|
+
try {
|
|
357
|
+
const packagePath = path.join(getSourceRoot(), "node_modules", ...packageName.split("/"), "package.json");
|
|
358
|
+
const pkg = JSON.parse(readFileSyncUtf8(packagePath));
|
|
359
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function readFileSyncUtf8(filePath) {
|
|
366
|
+
return readFileSync(filePath, "utf8");
|
|
367
|
+
}
|
|
368
|
+
function extractVersion(value) {
|
|
369
|
+
const match = value.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
370
|
+
return match?.[0] ?? null;
|
|
371
|
+
}
|
|
372
|
+
function compareVersions(left, right) {
|
|
373
|
+
const leftParts = parseVersionParts(left);
|
|
374
|
+
const rightParts = parseVersionParts(right);
|
|
375
|
+
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
|
|
376
|
+
const diff = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);
|
|
377
|
+
if (diff !== 0) {
|
|
378
|
+
return diff;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
function parseVersionParts(value) {
|
|
384
|
+
return value.split(/[.-]/).slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
|
|
385
|
+
}
|
|
131
386
|
function shellQuote(value) {
|
|
132
387
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
133
388
|
}
|
|
389
|
+
function formatLogLine(line) {
|
|
390
|
+
const trimmed = line.trim();
|
|
391
|
+
if (!trimmed) {
|
|
392
|
+
return "";
|
|
393
|
+
}
|
|
394
|
+
const parsedJson = parseJsonLogLine(trimmed);
|
|
395
|
+
if (parsedJson) {
|
|
396
|
+
return parsedJson;
|
|
397
|
+
}
|
|
398
|
+
const textRecord = trimmed.match(/^\[(?<timestamp>[^\]]+)\]\s+(?<level>INFO|WARN|ERROR)\s+(?<message>.*)$/i);
|
|
399
|
+
if (textRecord?.groups) {
|
|
400
|
+
return [
|
|
401
|
+
formatLogTimestamp(textRecord.groups.timestamp),
|
|
402
|
+
textRecord.groups.level.toUpperCase().padEnd(5),
|
|
403
|
+
textRecord.groups.message,
|
|
404
|
+
].join(" ");
|
|
405
|
+
}
|
|
406
|
+
const timestampedShellLine = trimmed.match(/^\[(?<timestamp>[^\]]+)\]\s+(?<message>.*)$/);
|
|
407
|
+
if (timestampedShellLine?.groups) {
|
|
408
|
+
return [
|
|
409
|
+
formatLogTimestamp(timestampedShellLine.groups.timestamp),
|
|
410
|
+
"INFO ".padEnd(5),
|
|
411
|
+
timestampedShellLine.groups.message,
|
|
412
|
+
].join(" ");
|
|
413
|
+
}
|
|
414
|
+
return trimmed;
|
|
415
|
+
}
|
|
416
|
+
function parseJsonLogLine(line) {
|
|
417
|
+
if (!line.startsWith("{")) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
try {
|
|
421
|
+
const parsed = JSON.parse(line);
|
|
422
|
+
const timestamp = typeof parsed.ts === "string"
|
|
423
|
+
? parsed.ts
|
|
424
|
+
: typeof parsed.timestamp === "string"
|
|
425
|
+
? parsed.timestamp
|
|
426
|
+
: null;
|
|
427
|
+
const level = typeof parsed.level === "string" ? parsed.level.toUpperCase() : "INFO";
|
|
428
|
+
const message = typeof parsed.message === "string" ? parsed.message : JSON.stringify(parsed);
|
|
429
|
+
const event = typeof parsed.event === "string" && parsed.event !== "console" ? ` ${parsed.event}` : "";
|
|
430
|
+
return [
|
|
431
|
+
formatLogTimestamp(timestamp),
|
|
432
|
+
`${level}${event}`.slice(0, 12).padEnd(12),
|
|
433
|
+
message,
|
|
434
|
+
].join(" ");
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function formatLogTimestamp(value) {
|
|
441
|
+
if (!value) {
|
|
442
|
+
return "unknown time".padEnd(25);
|
|
443
|
+
}
|
|
444
|
+
const parsed = new Date(value);
|
|
445
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
446
|
+
return value.padEnd(25).slice(0, 25);
|
|
447
|
+
}
|
|
448
|
+
return formatLocalTimestamp(parsed);
|
|
449
|
+
}
|
|
450
|
+
function formatLocalTimestamp(date) {
|
|
451
|
+
return [
|
|
452
|
+
`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`,
|
|
453
|
+
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`,
|
|
454
|
+
].join(" ");
|
|
455
|
+
}
|
|
456
|
+
function pad(value) {
|
|
457
|
+
return String(value).padStart(2, "0");
|
|
458
|
+
}
|
package/dist/prompt-store.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { readJsonFileWithBackup, writeJsonFileAtomic } from "./persistence.js";
|
|
2
|
+
import { createDocumentStore } from "./state-backend.js";
|
|
5
3
|
export class PromptStore {
|
|
6
|
-
|
|
4
|
+
store;
|
|
7
5
|
lastPrompts = new Map();
|
|
8
6
|
queues = new Map();
|
|
9
7
|
pausedContexts = new Set();
|
|
10
|
-
constructor(workspace) {
|
|
11
|
-
this.
|
|
8
|
+
constructor(workspace, backend = "json") {
|
|
9
|
+
this.store = createDocumentStore({
|
|
10
|
+
workspace,
|
|
11
|
+
fileName: "prompts.json",
|
|
12
|
+
sqliteKey: "prompts",
|
|
13
|
+
backend,
|
|
14
|
+
});
|
|
12
15
|
this.load();
|
|
13
16
|
}
|
|
14
17
|
setLastPrompt(contextKey, prompt) {
|
|
@@ -18,12 +21,13 @@ export class PromptStore {
|
|
|
18
21
|
getLastPrompt(contextKey) {
|
|
19
22
|
return this.lastPrompts.get(contextKey);
|
|
20
23
|
}
|
|
21
|
-
enqueue(contextKey, prompt) {
|
|
24
|
+
enqueue(contextKey, prompt, options = {}) {
|
|
22
25
|
const item = {
|
|
23
26
|
...prompt,
|
|
24
27
|
id: createQueueId(),
|
|
25
28
|
contextKey,
|
|
26
29
|
createdAt: Date.now(),
|
|
30
|
+
notBefore: options.notBefore,
|
|
27
31
|
};
|
|
28
32
|
const queue = this.queues.get(contextKey) ?? [];
|
|
29
33
|
queue.push(item);
|
|
@@ -39,7 +43,15 @@ export class PromptStore {
|
|
|
39
43
|
}
|
|
40
44
|
dequeue(contextKey) {
|
|
41
45
|
const queue = this.queues.get(contextKey);
|
|
42
|
-
|
|
46
|
+
if (!queue || queue.length === 0) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const index = queue.findIndex((queued) => !queued.notBefore || queued.notBefore <= now);
|
|
51
|
+
if (index === -1) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const [item] = queue.splice(index, 1);
|
|
43
55
|
if (!queue || queue.length === 0) {
|
|
44
56
|
this.queues.delete(contextKey);
|
|
45
57
|
}
|
|
@@ -53,6 +65,16 @@ export class PromptStore {
|
|
|
53
65
|
list(contextKey) {
|
|
54
66
|
return [...(this.queues.get(contextKey) ?? [])];
|
|
55
67
|
}
|
|
68
|
+
get(contextKey, id) {
|
|
69
|
+
return this.queues.get(contextKey)?.find((item) => item.id === id);
|
|
70
|
+
}
|
|
71
|
+
nextRunnableAt(contextKey) {
|
|
72
|
+
const timestamps = (this.queues.get(contextKey) ?? [])
|
|
73
|
+
.map((item) => item.notBefore)
|
|
74
|
+
.filter((value) => typeof value === "number")
|
|
75
|
+
.sort((left, right) => left - right);
|
|
76
|
+
return timestamps[0] ?? null;
|
|
77
|
+
}
|
|
56
78
|
listContextKeys() {
|
|
57
79
|
return [...new Set([...this.queues.keys(), ...this.pausedContexts])];
|
|
58
80
|
}
|
|
@@ -143,13 +165,12 @@ export class PromptStore {
|
|
|
143
165
|
}
|
|
144
166
|
persist() {
|
|
145
167
|
try {
|
|
146
|
-
mkdirSync(path.dirname(this.persistPath), { recursive: true });
|
|
147
168
|
const payload = {
|
|
148
169
|
lastPrompts: Object.fromEntries(this.lastPrompts.entries()),
|
|
149
170
|
queues: Object.fromEntries(this.queues.entries()),
|
|
150
171
|
pausedContexts: [...this.pausedContexts],
|
|
151
172
|
};
|
|
152
|
-
|
|
173
|
+
this.store.write(payload);
|
|
153
174
|
}
|
|
154
175
|
catch (error) {
|
|
155
176
|
console.warn("Failed to persist prompt store:", error instanceof Error ? error.message : String(error));
|
|
@@ -157,7 +178,7 @@ export class PromptStore {
|
|
|
157
178
|
}
|
|
158
179
|
load() {
|
|
159
180
|
try {
|
|
160
|
-
const payload =
|
|
181
|
+
const payload = this.store.read();
|
|
161
182
|
if (!payload) {
|
|
162
183
|
return;
|
|
163
184
|
}
|
|
@@ -218,6 +239,7 @@ function isQueuedPrompt(value) {
|
|
|
218
239
|
typeof value.id === "string" &&
|
|
219
240
|
typeof value.contextKey === "string" &&
|
|
220
241
|
typeof value.createdAt === "number" &&
|
|
242
|
+
(value.notBefore === undefined || typeof value.notBefore === "number") &&
|
|
221
243
|
(value.updatedAt === undefined || typeof value.updatedAt === "number") &&
|
|
222
244
|
(value.attempts === undefined || typeof value.attempts === "number") &&
|
|
223
245
|
(value.lastError === undefined || typeof value.lastError === "string");
|