metame-cli 1.5.18 → 1.5.20
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/index.js +157 -80
- package/package.json +2 -2
- package/scripts/bin/bootstrap-worktree.sh +20 -0
- package/scripts/core/audit.js +190 -0
- package/scripts/core/handoff.js +780 -0
- package/scripts/core/handoff.test.js +1074 -0
- package/scripts/core/memory-model.js +183 -0
- package/scripts/core/memory-model.test.js +486 -0
- package/scripts/core/reactive-paths.js +44 -0
- package/scripts/core/reactive-paths.test.js +35 -0
- package/scripts/core/reactive-prompt.js +51 -0
- package/scripts/core/reactive-prompt.test.js +88 -0
- package/scripts/core/reactive-signal.js +40 -0
- package/scripts/core/reactive-signal.test.js +88 -0
- package/scripts/core/thread-chat-id.js +52 -0
- package/scripts/core/thread-chat-id.test.js +113 -0
- package/scripts/daemon-bridges.js +79 -35
- package/scripts/daemon-claude-engine.js +371 -425
- package/scripts/daemon-command-router.js +80 -6
- package/scripts/daemon-engine-runtime.js +26 -4
- package/scripts/daemon-message-pipeline.js +2 -2
- package/scripts/daemon-reactive-lifecycle.js +134 -33
- package/scripts/daemon-session-commands.js +133 -43
- package/scripts/daemon-session-store.js +300 -82
- package/scripts/daemon-team-dispatch.js +16 -16
- package/scripts/daemon.js +37 -176
- package/scripts/deploy-manifest.js +90 -0
- package/scripts/docs/maintenance-manual.md +14 -11
- package/scripts/docs/pointer-map.md +13 -4
- package/scripts/feishu-adapter.js +31 -27
- package/scripts/hooks/intent-engine.js +6 -3
- package/scripts/hooks/intent-memory-recall.js +1 -0
- package/scripts/hooks/intent-perpetual.js +1 -1
- package/scripts/memory-extract.js +5 -97
- package/scripts/memory-gc.js +35 -90
- package/scripts/memory-migrate-v2.js +304 -0
- package/scripts/memory-nightly-reflect.js +40 -41
- package/scripts/memory.js +340 -859
- package/scripts/migrate-reactive-paths.js +122 -0
- package/scripts/signal-capture.js +4 -0
- package/scripts/sync-plugin.js +56 -0
package/index.js
CHANGED
|
@@ -7,7 +7,8 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
const { spawn, execSync } = require('child_process');
|
|
10
|
-
const { sleepSync, findProcessesByPattern, icon } = require('./scripts/platform');
|
|
10
|
+
const { sleepSync, findProcessesByPattern, killProcessTree, icon } = require('./scripts/platform');
|
|
11
|
+
const { collectDeployGroups, collectSyntaxCheckFiles } = require('./scripts/deploy-manifest');
|
|
11
12
|
|
|
12
13
|
// On Windows, resolve .cmd wrapper → actual Node.js entry and spawn node directly.
|
|
13
14
|
// Completely bypasses cmd.exe, eliminating terminal flash.
|
|
@@ -254,6 +255,16 @@ function readRunningDaemonPid({ pidFile, lockFile }) {
|
|
|
254
255
|
return null;
|
|
255
256
|
}
|
|
256
257
|
|
|
258
|
+
function isPidAlive(pid) {
|
|
259
|
+
if (!pid || Number.isNaN(pid) || pid === process.pid) return false;
|
|
260
|
+
try {
|
|
261
|
+
process.kill(pid, 0);
|
|
262
|
+
return true;
|
|
263
|
+
} catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
257
268
|
// --- macOS launchd integration ---
|
|
258
269
|
const LAUNCHD_LABEL = 'com.metame.npm-daemon';
|
|
259
270
|
const LAUNCHD_PLIST = path.join(HOME_DIR, 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
@@ -318,6 +329,99 @@ function launchdIsRunning() {
|
|
|
318
329
|
} catch { return null; }
|
|
319
330
|
}
|
|
320
331
|
|
|
332
|
+
function cleanDaemonRuntimeState({ pidFile, lockFile }) {
|
|
333
|
+
try {
|
|
334
|
+
if (fs.existsSync(pidFile)) {
|
|
335
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10);
|
|
336
|
+
if (!isPidAlive(pid)) fs.unlinkSync(pidFile);
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
try { fs.unlinkSync(pidFile); } catch { /* ignore */ }
|
|
340
|
+
}
|
|
341
|
+
try {
|
|
342
|
+
if (fs.existsSync(lockFile)) {
|
|
343
|
+
const lock = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
|
|
344
|
+
const pid = parseInt(lock && lock.pid, 10);
|
|
345
|
+
if (!isPidAlive(pid)) fs.unlinkSync(lockFile);
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
try { fs.unlinkSync(lockFile); } catch { /* ignore */ }
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function terminateDaemonProcesses({
|
|
353
|
+
pidFile,
|
|
354
|
+
lockFile,
|
|
355
|
+
preservePid = null,
|
|
356
|
+
}) {
|
|
357
|
+
const targets = new Set();
|
|
358
|
+
for (const pid of findProcessesByPattern('node.*daemon\\.js')) {
|
|
359
|
+
if (pid && pid !== process.pid && pid !== preservePid) targets.add(pid);
|
|
360
|
+
}
|
|
361
|
+
for (const file of [pidFile, lockFile]) {
|
|
362
|
+
if (!fs.existsSync(file)) continue;
|
|
363
|
+
try {
|
|
364
|
+
const raw = fs.readFileSync(file, 'utf8').trim();
|
|
365
|
+
const pid = file === pidFile
|
|
366
|
+
? parseInt(raw, 10)
|
|
367
|
+
: parseInt((JSON.parse(raw || '{}') || {}).pid, 10);
|
|
368
|
+
if (pid && pid !== process.pid && pid !== preservePid && isPidAlive(pid)) targets.add(pid);
|
|
369
|
+
} catch { /* ignore invalid metadata */ }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!targets.size) {
|
|
373
|
+
cleanDaemonRuntimeState({ pidFile, lockFile });
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
for (const pid of targets) killProcessTree(pid, 'SIGTERM');
|
|
378
|
+
sleepSync(1500);
|
|
379
|
+
for (const pid of targets) {
|
|
380
|
+
if (isPidAlive(pid)) killProcessTree(pid, 'SIGKILL');
|
|
381
|
+
}
|
|
382
|
+
sleepSync(500);
|
|
383
|
+
cleanDaemonRuntimeState({ pidFile, lockFile });
|
|
384
|
+
return [...targets];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function ensureLaunchdDaemonRunning({
|
|
388
|
+
daemonScript,
|
|
389
|
+
daemonLog,
|
|
390
|
+
pidFile,
|
|
391
|
+
lockFile,
|
|
392
|
+
restart = false,
|
|
393
|
+
}) {
|
|
394
|
+
ensureLaunchdPlist({ daemonScript, daemonLog });
|
|
395
|
+
let launchdPid = launchdIsRunning();
|
|
396
|
+
|
|
397
|
+
if (launchdPid) {
|
|
398
|
+
terminateDaemonProcesses({ pidFile, lockFile, preservePid: launchdPid });
|
|
399
|
+
} else {
|
|
400
|
+
terminateDaemonProcesses({ pidFile, lockFile });
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
execSync(`launchctl bootstrap gui/$(id -u) ${LAUNCHD_PLIST} 2>/dev/null`);
|
|
405
|
+
} catch { /* already bootstrapped */ }
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
execSync(`launchctl kickstart ${restart ? '-k ' : ''}gui/$(id -u)/${LAUNCHD_LABEL}`);
|
|
409
|
+
} catch {
|
|
410
|
+
// Recover from a stale/removed launchd service entry.
|
|
411
|
+
try { execSync(`launchctl bootout gui/$(id -u)/${LAUNCHD_LABEL} 2>/dev/null`); } catch { /* ignore */ }
|
|
412
|
+
terminateDaemonProcesses({ pidFile, lockFile });
|
|
413
|
+
execSync(`launchctl bootstrap gui/$(id -u) ${LAUNCHD_PLIST}`);
|
|
414
|
+
execSync(`launchctl kickstart gui/$(id -u)/${LAUNCHD_LABEL}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
sleepSync(1500);
|
|
418
|
+
launchdPid = launchdIsRunning();
|
|
419
|
+
if (!launchdPid) {
|
|
420
|
+
launchdPid = readRunningDaemonPid({ pidFile, lockFile });
|
|
421
|
+
}
|
|
422
|
+
return launchdPid || null;
|
|
423
|
+
}
|
|
424
|
+
|
|
321
425
|
function requestDaemonRestart({
|
|
322
426
|
reason = 'manual-restart',
|
|
323
427
|
daemonPidFile = path.join(METAME_DIR, 'daemon.pid'),
|
|
@@ -374,20 +478,11 @@ function requestDaemonRestart({
|
|
|
374
478
|
// Auto-deploy bundled scripts to ~/.metame/
|
|
375
479
|
// IMPORTANT: daemon.yaml is USER CONFIG — never overwrite it. Only daemon-default.yaml (template) is synced.
|
|
376
480
|
const scriptsDir = path.join(__dirname, 'scripts');
|
|
377
|
-
// Auto-detect ALL runtime scripts: daemon-*.js + all other non-test, non-utility .js/.yaml/.sh files.
|
|
378
|
-
// This prevents "missing module" crashes when new files are added without updating a manual list.
|
|
379
481
|
const EXCLUDED_SCRIPTS = new Set(['sync-readme.js', 'test_daemon.js', 'daemon.yaml']);
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (/\.test\.js$/.test(f)) return false;
|
|
385
|
-
return /\.(js|yaml|sh)$/.test(f);
|
|
386
|
-
});
|
|
387
|
-
} catch {
|
|
388
|
-
return [];
|
|
389
|
-
}
|
|
390
|
-
})();
|
|
482
|
+
const SCRIPT_DEPLOY_GROUPS = collectDeployGroups(fs, path, scriptsDir, {
|
|
483
|
+
excludedScripts: EXCLUDED_SCRIPTS,
|
|
484
|
+
includeNestedDirs: ['core'],
|
|
485
|
+
});
|
|
391
486
|
|
|
392
487
|
// Protect daemon.yaml: create backup before any sync operation
|
|
393
488
|
const DAEMON_YAML_BACKUP = path.join(METAME_DIR, 'daemon.yaml.bak');
|
|
@@ -423,15 +518,14 @@ if (_isInWorktree) {
|
|
|
423
518
|
// Catches bad merges and careless agent edits BEFORE they can crash the daemon.
|
|
424
519
|
const { execSync: _execSync } = require('child_process');
|
|
425
520
|
const syntaxErrors = [];
|
|
426
|
-
for (const
|
|
427
|
-
if (!f.endsWith('.js')) continue;
|
|
428
|
-
const fp = path.join(scriptsDir, f);
|
|
521
|
+
for (const fp of collectSyntaxCheckFiles(path, SCRIPT_DEPLOY_GROUPS)) {
|
|
429
522
|
if (!fs.existsSync(fp)) continue;
|
|
430
523
|
try {
|
|
431
524
|
_execSync(`"${process.execPath}" -c "${fp}"`, { timeout: 5000, stdio: 'pipe', windowsHide: true });
|
|
432
525
|
} catch (e) {
|
|
526
|
+
const label = path.relative(scriptsDir, fp);
|
|
433
527
|
const msg = (e.stderr ? e.stderr.toString().trim() : e.message).split('\n')[0];
|
|
434
|
-
syntaxErrors.push(`${
|
|
528
|
+
syntaxErrors.push(`${label}: ${msg}`);
|
|
435
529
|
}
|
|
436
530
|
}
|
|
437
531
|
|
|
@@ -441,7 +535,11 @@ if (syntaxErrors.length > 0) {
|
|
|
441
535
|
for (const err of syntaxErrors) console.error(` ${err}`);
|
|
442
536
|
console.error('Fix the errors before deploying. Daemon continues running with old code.');
|
|
443
537
|
} else {
|
|
444
|
-
scriptsUpdated =
|
|
538
|
+
scriptsUpdated = SCRIPT_DEPLOY_GROUPS.reduce((updated, group) => {
|
|
539
|
+
const destDir = group.destSubdir ? path.join(METAME_DIR, group.destSubdir) : METAME_DIR;
|
|
540
|
+
const changed = syncDirFiles(group.srcDir, destDir, { fileList: group.fileList });
|
|
541
|
+
return updated || changed;
|
|
542
|
+
}, false);
|
|
445
543
|
if (scriptsUpdated) {
|
|
446
544
|
console.log(`${icon("pkg")} Scripts synced to ~/.metame/.`);
|
|
447
545
|
}
|
|
@@ -454,6 +552,21 @@ const binUpdated = syncDirFiles(path.join(__dirname, 'scripts', 'bin'), path.joi
|
|
|
454
552
|
// Hooks: Claude Code event hooks (Stop, PostToolUse, etc.)
|
|
455
553
|
const hooksUpdated = syncDirFiles(path.join(__dirname, 'scripts', 'hooks'), path.join(METAME_DIR, 'hooks'));
|
|
456
554
|
|
|
555
|
+
// Migrate legacy reactive flat paths to per-project directory structure
|
|
556
|
+
try {
|
|
557
|
+
const { migrate } = require('./scripts/migrate-reactive-paths');
|
|
558
|
+
const migrationReport = migrate(METAME_DIR);
|
|
559
|
+
if (migrationReport.migrated.length > 0) {
|
|
560
|
+
console.log(`${icon("pkg")} Migrated ${migrationReport.migrated.length} reactive file(s) to per-project dirs.`);
|
|
561
|
+
}
|
|
562
|
+
if (migrationReport.errors.length > 0) {
|
|
563
|
+
console.log(`${icon("warn")} Reactive migration had ${migrationReport.errors.length} error(s).`);
|
|
564
|
+
}
|
|
565
|
+
} catch (e) {
|
|
566
|
+
// Non-critical: migration failure should not block deploy
|
|
567
|
+
console.log(`${icon("warn")} Reactive path migration skipped: ${e.message}`);
|
|
568
|
+
}
|
|
569
|
+
|
|
457
570
|
const daemonCodeUpdated = scriptsUpdated || binUpdated || hooksUpdated;
|
|
458
571
|
const shouldAutoRestartAfterDeploy = (() => {
|
|
459
572
|
const [cmd] = process.argv.slice(2);
|
|
@@ -2164,39 +2277,12 @@ WantedBy=default.target
|
|
|
2164
2277
|
}
|
|
2165
2278
|
|
|
2166
2279
|
if (process.platform === 'darwin') {
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
if (n !== launchdPid) {
|
|
2174
|
-
try { process.kill(n, 'SIGTERM'); } catch { /* */ }
|
|
2175
|
-
}
|
|
2176
|
-
}
|
|
2177
|
-
} catch { /* ignore */ }
|
|
2178
|
-
// Clean stale lock/pid from orphan processes
|
|
2179
|
-
if (fs.existsSync(DAEMON_LOCK)) {
|
|
2180
|
-
try {
|
|
2181
|
-
const lock = JSON.parse(fs.readFileSync(DAEMON_LOCK, 'utf8'));
|
|
2182
|
-
const pid = parseInt(lock && lock.pid, 10);
|
|
2183
|
-
if (pid) { process.kill(pid, 0); } // throws if dead
|
|
2184
|
-
} catch {
|
|
2185
|
-
// Owner is dead — clean stale files
|
|
2186
|
-
try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
|
|
2187
|
-
try { fs.unlinkSync(DAEMON_LOCK); } catch { /* */ }
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
2190
|
-
ensureLaunchdPlist({ daemonScript: DAEMON_SCRIPT, daemonLog: DAEMON_LOG });
|
|
2191
|
-
try {
|
|
2192
|
-
execSync(`launchctl bootstrap gui/$(id -u) ${LAUNCHD_PLIST} 2>/dev/null`);
|
|
2193
|
-
} catch { /* already bootstrapped */ }
|
|
2194
|
-
// kickstart ensures the process is actually running now
|
|
2195
|
-
try {
|
|
2196
|
-
execSync(`launchctl kickstart gui/$(id -u)/${LAUNCHD_LABEL}`);
|
|
2197
|
-
} catch { /* already running */ }
|
|
2198
|
-
sleepSync(1500);
|
|
2199
|
-
const pid = launchdIsRunning();
|
|
2280
|
+
const pid = ensureLaunchdDaemonRunning({
|
|
2281
|
+
daemonScript: DAEMON_SCRIPT,
|
|
2282
|
+
daemonLog: DAEMON_LOG,
|
|
2283
|
+
pidFile: DAEMON_PID,
|
|
2284
|
+
lockFile: DAEMON_LOCK,
|
|
2285
|
+
});
|
|
2200
2286
|
if (pid) {
|
|
2201
2287
|
console.log(`${icon("ok")} MetaMe daemon started via launchd (PID: ${pid})`);
|
|
2202
2288
|
} else {
|
|
@@ -2241,17 +2327,7 @@ WantedBy=default.target
|
|
|
2241
2327
|
try {
|
|
2242
2328
|
execSync(`launchctl bootout gui/$(id -u)/${LAUNCHD_LABEL} 2>/dev/null`);
|
|
2243
2329
|
} catch { /* not loaded */ }
|
|
2244
|
-
|
|
2245
|
-
try {
|
|
2246
|
-
const pids = findProcessesByPattern('node.*daemon\\.js');
|
|
2247
|
-
for (const n of pids) {
|
|
2248
|
-
try { process.kill(n, 'SIGTERM'); } catch { /* */ }
|
|
2249
|
-
}
|
|
2250
|
-
if (pids.length) sleepSync(2000);
|
|
2251
|
-
for (const n of pids) {
|
|
2252
|
-
try { process.kill(n, 'SIGKILL'); } catch { /* already gone */ }
|
|
2253
|
-
}
|
|
2254
|
-
} catch { /* */ }
|
|
2330
|
+
terminateDaemonProcesses({ pidFile: DAEMON_PID, lockFile: DAEMON_LOCK });
|
|
2255
2331
|
try { fs.unlinkSync(DAEMON_PID); } catch { /* */ }
|
|
2256
2332
|
try { fs.unlinkSync(DAEMON_LOCK); } catch { /* */ }
|
|
2257
2333
|
console.log(`${icon("ok")} Daemon stopped. launchd auto-restart disabled.`);
|
|
@@ -2295,23 +2371,17 @@ WantedBy=default.target
|
|
|
2295
2371
|
}
|
|
2296
2372
|
|
|
2297
2373
|
if (process.platform === 'darwin') {
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
} else {
|
|
2310
|
-
console.log(`${icon("ok")} Daemon restart requested via launchd...`);
|
|
2311
|
-
}
|
|
2312
|
-
} catch (e) {
|
|
2313
|
-
console.error(`${icon("fail")} launchctl kickstart failed: ${e.message}`);
|
|
2314
|
-
process.exit(1);
|
|
2374
|
+
const pid = ensureLaunchdDaemonRunning({
|
|
2375
|
+
daemonScript: DAEMON_SCRIPT,
|
|
2376
|
+
daemonLog: DAEMON_LOG,
|
|
2377
|
+
pidFile: DAEMON_PID,
|
|
2378
|
+
lockFile: DAEMON_LOCK,
|
|
2379
|
+
restart: true,
|
|
2380
|
+
});
|
|
2381
|
+
if (pid) {
|
|
2382
|
+
console.log(`${icon("ok")} Daemon restarted via launchd (PID: ${pid})`);
|
|
2383
|
+
} else {
|
|
2384
|
+
console.log(`${icon("ok")} Daemon restart requested via launchd...`);
|
|
2315
2385
|
}
|
|
2316
2386
|
process.exit(0);
|
|
2317
2387
|
}
|
|
@@ -2369,6 +2439,13 @@ WantedBy=default.target
|
|
|
2369
2439
|
}
|
|
2370
2440
|
} catch { /* lock stale or invalid */ }
|
|
2371
2441
|
}
|
|
2442
|
+
if (!isRunning && process.platform === 'darwin' && fs.existsSync(LAUNCHD_PLIST)) {
|
|
2443
|
+
const pid = launchdIsRunning();
|
|
2444
|
+
if (pid) {
|
|
2445
|
+
isRunning = true;
|
|
2446
|
+
runningPid = pid;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2372
2449
|
|
|
2373
2450
|
console.log(`${icon("bot")} MetaMe Daemon: ${isRunning ? icon("green") + ' Running' : icon("red") + ' Stopped'}`);
|
|
2374
2451
|
if (state.started_at) console.log(` Started: ${state.started_at}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metame-cli",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.20",
|
|
4
4
|
"description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"test:daemon-status": "node --test scripts/daemon-restart-status.test.js",
|
|
22
22
|
"start": "node index.js",
|
|
23
23
|
"push": "bash scripts/bin/push-clean.sh",
|
|
24
|
-
"sync:plugin": "node
|
|
24
|
+
"sync:plugin": "node scripts/sync-plugin.js",
|
|
25
25
|
"sync:readme": "node scripts/sync-readme.js",
|
|
26
26
|
"restart:daemon": "node index.js stop 2>/dev/null; sleep 1; node index.js start 2>/dev/null || echo '鈿狅笍 Daemon not running or restart failed'",
|
|
27
27
|
"prepublishOnly": "node -e \"const fs=require('fs');['scripts/daemon.yaml','plugin/scripts/daemon.yaml'].forEach(f=>{if(fs.existsSync(f)){const c=fs.readFileSync(f,'utf8');if(/bot_token:\\s*[^n]|app_secret:\\s*[^n]|enabled:\\s*true/.test(c)){console.error('ABORT: Real credentials found in '+f);process.exit(1)}}})\"",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
5
|
+
cd "$ROOT"
|
|
6
|
+
|
|
7
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
8
|
+
echo "Node.js is required" >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
if [[ -f package-lock.json ]]; then
|
|
13
|
+
echo "Using npm ci in $(pwd)"
|
|
14
|
+
npm ci --no-audit --no-fund
|
|
15
|
+
else
|
|
16
|
+
echo "package-lock.json missing, using npm install in $(pwd)"
|
|
17
|
+
npm install --no-audit --no-fund
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
node -e "require('js-yaml'); require('./scripts/resolve-yaml'); console.log('bootstrap-ok')"
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createAudit(deps) {
|
|
4
|
+
const {
|
|
5
|
+
fs,
|
|
6
|
+
logFile,
|
|
7
|
+
stateFile,
|
|
8
|
+
stdout = process.stdout,
|
|
9
|
+
stderr = process.stderr,
|
|
10
|
+
usageRetentionDaysDefault = 30,
|
|
11
|
+
} = deps;
|
|
12
|
+
|
|
13
|
+
let logMaxSize = 1048576;
|
|
14
|
+
let cachedState = null;
|
|
15
|
+
|
|
16
|
+
function ensureUsageShape(state) {
|
|
17
|
+
if (!state.usage || typeof state.usage !== 'object') state.usage = {};
|
|
18
|
+
if (!state.usage.categories || typeof state.usage.categories !== 'object') state.usage.categories = {};
|
|
19
|
+
if (!state.usage.daily || typeof state.usage.daily !== 'object') state.usage.daily = {};
|
|
20
|
+
const keepDays = Number(state.usage.retention_days);
|
|
21
|
+
state.usage.retention_days = Number.isFinite(keepDays) && keepDays >= 7
|
|
22
|
+
? Math.floor(keepDays)
|
|
23
|
+
: usageRetentionDaysDefault;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function ensureStateShape(state) {
|
|
27
|
+
if (!state || typeof state !== 'object') return {
|
|
28
|
+
pid: null,
|
|
29
|
+
budget: { date: null, tokens_used: 0 },
|
|
30
|
+
tasks: {},
|
|
31
|
+
sessions: {},
|
|
32
|
+
started_at: null,
|
|
33
|
+
usage: { retention_days: usageRetentionDaysDefault, categories: {}, daily: {} },
|
|
34
|
+
};
|
|
35
|
+
if (!state.budget || typeof state.budget !== 'object') state.budget = { date: null, tokens_used: 0 };
|
|
36
|
+
if (typeof state.budget.tokens_used !== 'number') state.budget.tokens_used = Number(state.budget.tokens_used) || 0;
|
|
37
|
+
if (!Object.prototype.hasOwnProperty.call(state.budget, 'date')) state.budget.date = null;
|
|
38
|
+
if (!state.tasks || typeof state.tasks !== 'object') state.tasks = {};
|
|
39
|
+
if (!state.sessions || typeof state.sessions !== 'object') state.sessions = {};
|
|
40
|
+
ensureUsageShape(state);
|
|
41
|
+
return state;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pruneDailyUsage(usage, todayIso) {
|
|
45
|
+
const keepDays = usage.retention_days || usageRetentionDaysDefault;
|
|
46
|
+
const cutoff = new Date(`${todayIso}T00:00:00.000Z`);
|
|
47
|
+
cutoff.setUTCDate(cutoff.getUTCDate() - (keepDays - 1));
|
|
48
|
+
const cutoffIso = cutoff.toISOString().slice(0, 10);
|
|
49
|
+
for (const day of Object.keys(usage.daily || {})) {
|
|
50
|
+
if (day < cutoffIso) delete usage.daily[day];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readStateFromDisk() {
|
|
55
|
+
try {
|
|
56
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
57
|
+
return ensureStateShape(state);
|
|
58
|
+
} catch {
|
|
59
|
+
return ensureStateShape({
|
|
60
|
+
pid: null,
|
|
61
|
+
budget: { date: null, tokens_used: 0 },
|
|
62
|
+
tasks: {},
|
|
63
|
+
sessions: {},
|
|
64
|
+
started_at: null,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function refreshLogMaxSize(cfg) {
|
|
70
|
+
logMaxSize = (cfg && cfg.daemon && cfg.daemon.log_max_size) || 1048576;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function log(level, msg) {
|
|
74
|
+
const ts = new Date().toISOString();
|
|
75
|
+
const line = `[${ts}] [${level}] ${msg}\n`;
|
|
76
|
+
try {
|
|
77
|
+
if (fs.existsSync(logFile)) {
|
|
78
|
+
const stat = fs.statSync(logFile);
|
|
79
|
+
if (stat.size > logMaxSize) {
|
|
80
|
+
const bakFile = logFile + '.bak';
|
|
81
|
+
if (fs.existsSync(bakFile)) fs.unlinkSync(bakFile);
|
|
82
|
+
fs.renameSync(logFile, bakFile);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
fs.appendFileSync(logFile, line, 'utf8');
|
|
86
|
+
} catch {
|
|
87
|
+
stderr.write(line);
|
|
88
|
+
}
|
|
89
|
+
if (stdout && !stdout.isTTY && typeof stdout.write === 'function') {
|
|
90
|
+
stdout.write(line);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function loadState() {
|
|
95
|
+
if (!cachedState) cachedState = readStateFromDisk();
|
|
96
|
+
return cachedState;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function saveState(state) {
|
|
100
|
+
const next = ensureStateShape(state);
|
|
101
|
+
if (cachedState && cachedState !== next) {
|
|
102
|
+
const current = ensureStateShape(cachedState);
|
|
103
|
+
|
|
104
|
+
const currentBudgetDate = String(current.budget.date || '');
|
|
105
|
+
const nextBudgetDate = String(next.budget.date || '');
|
|
106
|
+
const currentBudgetTokens = Math.max(0, Math.floor(Number(current.budget.tokens_used) || 0));
|
|
107
|
+
const nextBudgetTokens = Math.max(0, Math.floor(Number(next.budget.tokens_used) || 0));
|
|
108
|
+
if (currentBudgetDate && (!nextBudgetDate || currentBudgetDate > nextBudgetDate)) {
|
|
109
|
+
next.budget.date = currentBudgetDate;
|
|
110
|
+
next.budget.tokens_used = currentBudgetTokens;
|
|
111
|
+
} else if (currentBudgetDate && currentBudgetDate === nextBudgetDate) {
|
|
112
|
+
next.budget.tokens_used = Math.max(currentBudgetTokens, nextBudgetTokens);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const currentKeepDays = Number(current.usage.retention_days) || usageRetentionDaysDefault;
|
|
116
|
+
const nextKeepDays = Number(next.usage.retention_days) || usageRetentionDaysDefault;
|
|
117
|
+
next.usage.retention_days = Math.max(currentKeepDays, nextKeepDays);
|
|
118
|
+
|
|
119
|
+
for (const [category, curMeta] of Object.entries(current.usage.categories || {})) {
|
|
120
|
+
if (!next.usage.categories[category] || typeof next.usage.categories[category] !== 'object') {
|
|
121
|
+
next.usage.categories[category] = {};
|
|
122
|
+
}
|
|
123
|
+
const curTotal = Math.max(0, Math.floor(Number(curMeta && curMeta.total) || 0));
|
|
124
|
+
const nextTotal = Math.max(0, Math.floor(Number(next.usage.categories[category].total) || 0));
|
|
125
|
+
if (curTotal > nextTotal) next.usage.categories[category].total = curTotal;
|
|
126
|
+
|
|
127
|
+
const curUpdated = String(curMeta && curMeta.updated_at || '');
|
|
128
|
+
const nextUpdated = String(next.usage.categories[category].updated_at || '');
|
|
129
|
+
if (curUpdated && curUpdated > nextUpdated) next.usage.categories[category].updated_at = curUpdated;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const [day, curDayUsageRaw] of Object.entries(current.usage.daily || {})) {
|
|
133
|
+
const curDayUsage = (curDayUsageRaw && typeof curDayUsageRaw === 'object') ? curDayUsageRaw : {};
|
|
134
|
+
if (!next.usage.daily[day] || typeof next.usage.daily[day] !== 'object') {
|
|
135
|
+
next.usage.daily[day] = {};
|
|
136
|
+
}
|
|
137
|
+
const nextDayUsage = next.usage.daily[day];
|
|
138
|
+
for (const [key, curValue] of Object.entries(curDayUsage)) {
|
|
139
|
+
const curNum = Math.max(0, Math.floor(Number(curValue) || 0));
|
|
140
|
+
const nextNum = Math.max(0, Math.floor(Number(nextDayUsage[key]) || 0));
|
|
141
|
+
if (curNum > nextNum) nextDayUsage[key] = curNum;
|
|
142
|
+
}
|
|
143
|
+
const categorySum = Object.entries(nextDayUsage)
|
|
144
|
+
.filter(([key]) => key !== 'total')
|
|
145
|
+
.reduce((sum, [, value]) => sum + Math.max(0, Math.floor(Number(value) || 0)), 0);
|
|
146
|
+
nextDayUsage.total = Math.max(Math.max(0, Math.floor(Number(nextDayUsage.total) || 0)), categorySum);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const currentUsageUpdated = String(current.usage.updated_at || '');
|
|
150
|
+
const nextUsageUpdated = String(next.usage.updated_at || '');
|
|
151
|
+
if (currentUsageUpdated && currentUsageUpdated > nextUsageUpdated) {
|
|
152
|
+
next.usage.updated_at = currentUsageUpdated;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (current.sessions && typeof current.sessions === 'object') {
|
|
156
|
+
if (!next.sessions || typeof next.sessions !== 'object') next.sessions = {};
|
|
157
|
+
for (const [key, curSession] of Object.entries(current.sessions)) {
|
|
158
|
+
if (!next.sessions[key]) {
|
|
159
|
+
next.sessions[key] = curSession;
|
|
160
|
+
} else {
|
|
161
|
+
const curActive = Number(curSession && curSession.last_active) || 0;
|
|
162
|
+
const nextActive = Number(next.sessions[key] && next.sessions[key].last_active) || 0;
|
|
163
|
+
if (curActive > nextActive) next.sessions[key] = curSession;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
cachedState = next;
|
|
170
|
+
try {
|
|
171
|
+
fs.writeFileSync(stateFile, JSON.stringify(next, null, 2), 'utf8');
|
|
172
|
+
} catch (e) {
|
|
173
|
+
log('ERROR', `Failed to save state: ${e.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
refreshLogMaxSize,
|
|
179
|
+
log,
|
|
180
|
+
loadState,
|
|
181
|
+
saveState,
|
|
182
|
+
ensureUsageShape,
|
|
183
|
+
ensureStateShape,
|
|
184
|
+
pruneDailyUsage,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
createAudit,
|
|
190
|
+
};
|