dual-brain 0.2.30 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dual-brain/docs/claude-code-extension-points.md +32 -0
- package/.dual-brain/docs/data-tools-capabilities.md +181 -0
- package/.dual-brain/docs/ecosystem-tools.md +91 -0
- package/.dual-brain/docs/panel-handoff.md +124 -0
- package/.dual-brain/docs/ruflo-analysis.md +48 -0
- package/bin/dual-brain.mjs +56 -56
- package/dist/mcp-server/index.d.ts +27 -0
- package/dist/mcp-server/index.js +359 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/agent-protocol.d.ts +163 -0
- package/dist/src/agent-protocol.js +368 -0
- package/dist/src/agent-protocol.js.map +1 -0
- package/dist/src/agents/registry.d.ts +52 -0
- package/dist/src/agents/registry.js +393 -0
- package/dist/src/agents/registry.js.map +1 -0
- package/dist/src/awareness.d.ts +93 -0
- package/dist/src/awareness.js +406 -0
- package/dist/src/awareness.js.map +1 -0
- package/dist/src/brief.d.ts +48 -0
- package/dist/src/brief.js +179 -0
- package/dist/src/brief.js.map +1 -0
- package/dist/src/calibration.d.ts +32 -0
- package/dist/src/calibration.js +133 -0
- package/dist/src/calibration.js.map +1 -0
- package/dist/src/checkpoint.d.ts +33 -0
- package/dist/src/checkpoint.js +99 -0
- package/dist/src/checkpoint.js.map +1 -0
- package/dist/src/ci-triage.d.ts +33 -0
- package/dist/src/ci-triage.js +193 -0
- package/dist/src/ci-triage.js.map +1 -0
- package/dist/src/cognitive-loop.d.ts +56 -0
- package/dist/src/cognitive-loop.js +495 -0
- package/dist/src/cognitive-loop.js.map +1 -0
- package/dist/src/collaboration.d.ts +147 -0
- package/dist/src/collaboration.js +438 -0
- package/dist/src/collaboration.js.map +1 -0
- package/dist/src/context-intel.d.ts +47 -0
- package/dist/src/context-intel.js +156 -0
- package/dist/src/context-intel.js.map +1 -0
- package/dist/src/context.d.ts +53 -0
- package/dist/src/context.js +332 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/continuity.d.ts +89 -0
- package/dist/src/continuity.js +230 -0
- package/dist/src/continuity.js.map +1 -0
- package/dist/src/cost-tracker.d.ts +47 -0
- package/dist/src/cost-tracker.js +170 -0
- package/dist/src/cost-tracker.js.map +1 -0
- package/dist/src/debrief.d.ts +53 -0
- package/dist/src/debrief.js +222 -0
- package/dist/src/debrief.js.map +1 -0
- package/dist/src/decide.d.ts +96 -0
- package/dist/src/decide.js +744 -0
- package/dist/src/decide.js.map +1 -0
- package/dist/src/decompose.d.ts +39 -0
- package/dist/src/decompose.js +218 -0
- package/dist/src/decompose.js.map +1 -0
- package/dist/src/detect.d.ts +91 -0
- package/dist/src/detect.js +544 -0
- package/dist/src/detect.js.map +1 -0
- package/dist/src/dispatch.d.ts +154 -0
- package/dist/src/dispatch.js +1306 -0
- package/dist/src/dispatch.js.map +1 -0
- package/dist/src/doctor.d.ts +421 -0
- package/dist/src/doctor.js +1689 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/engine.d.ts +70 -0
- package/dist/src/engine.js +155 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/envelope.d.ts +36 -0
- package/dist/src/envelope.js +80 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/failure-memory.d.ts +55 -0
- package/dist/src/failure-memory.js +175 -0
- package/dist/src/failure-memory.js.map +1 -0
- package/dist/src/fx.d.ts +87 -0
- package/dist/src/fx.js +272 -0
- package/dist/src/fx.js.map +1 -0
- package/dist/src/governance.d.ts +93 -0
- package/dist/src/governance.js +261 -0
- package/dist/src/governance.js.map +1 -0
- package/dist/src/handoff.d.ts +11 -0
- package/dist/src/handoff.js +90 -0
- package/dist/src/handoff.js.map +1 -0
- package/dist/src/head-protocol.d.ts +76 -0
- package/dist/src/head-protocol.js +109 -0
- package/dist/src/head-protocol.js.map +1 -0
- package/dist/src/head.d.ts +222 -0
- package/dist/src/head.js +765 -0
- package/dist/src/head.js.map +1 -0
- package/dist/src/health.d.ts +132 -0
- package/dist/src/health.js +435 -0
- package/dist/src/health.js.map +1 -0
- package/dist/src/inbox.d.ts +70 -0
- package/dist/src/inbox.js +218 -0
- package/dist/src/inbox.js.map +1 -0
- package/dist/src/index.d.ts +33 -0
- package/dist/src/index.js +38 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/install-hooks.d.ts +13 -0
- package/dist/src/install-hooks.js +88 -0
- package/dist/src/install-hooks.js.map +1 -0
- package/dist/src/integrity.d.ts +59 -0
- package/dist/src/integrity.js +206 -0
- package/dist/src/integrity.js.map +1 -0
- package/dist/src/intelligence.d.ts +104 -0
- package/dist/src/intelligence.js +391 -0
- package/dist/src/intelligence.js.map +1 -0
- package/dist/src/ledger.d.ts +54 -0
- package/dist/src/ledger.js +179 -0
- package/dist/src/ledger.js.map +1 -0
- package/dist/src/living-docs.d.ts +14 -0
- package/dist/src/living-docs.js +197 -0
- package/dist/src/living-docs.js.map +1 -0
- package/dist/src/memory-tiers.d.ts +37 -0
- package/dist/src/memory-tiers.js +160 -0
- package/dist/src/memory-tiers.js.map +1 -0
- package/dist/src/model-profiles.d.ts +65 -0
- package/dist/src/model-profiles.js +568 -0
- package/dist/src/model-profiles.js.map +1 -0
- package/dist/src/models.d.ts +58 -0
- package/dist/src/models.js +327 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/narrative.d.ts +54 -0
- package/dist/src/narrative.js +163 -0
- package/dist/src/narrative.js.map +1 -0
- package/dist/src/nextstep.d.ts +16 -0
- package/dist/src/nextstep.js +103 -0
- package/dist/src/nextstep.js.map +1 -0
- package/dist/src/observer.d.ts +18 -0
- package/dist/src/observer.js +251 -0
- package/dist/src/observer.js.map +1 -0
- package/dist/src/outcome.d.ts +110 -0
- package/dist/src/outcome.js +377 -0
- package/dist/src/outcome.js.map +1 -0
- package/dist/src/pipeline.d.ts +167 -0
- package/dist/src/pipeline.js +1503 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/playbook.d.ts +59 -0
- package/dist/src/playbook.js +238 -0
- package/dist/src/playbook.js.map +1 -0
- package/dist/src/pr-agent.d.ts +97 -0
- package/dist/src/pr-agent.js +195 -0
- package/dist/src/pr-agent.js.map +1 -0
- package/dist/src/predictive.d.ts +57 -0
- package/dist/src/predictive.js +230 -0
- package/dist/src/predictive.js.map +1 -0
- package/dist/src/profile.d.ts +294 -0
- package/dist/src/profile.js +1347 -0
- package/dist/src/profile.js.map +1 -0
- package/dist/src/prompt-audit.d.ts +22 -0
- package/dist/src/prompt-audit.js +194 -0
- package/dist/src/prompt-audit.js.map +1 -0
- package/dist/src/prompt-intel.d.ts +12 -0
- package/dist/src/prompt-intel.js +321 -0
- package/dist/src/prompt-intel.js.map +1 -0
- package/dist/src/provider-context.d.ts +121 -0
- package/dist/src/provider-context.js +222 -0
- package/dist/src/provider-context.js.map +1 -0
- package/dist/src/provider-manager.d.ts +92 -0
- package/dist/src/provider-manager.js +428 -0
- package/dist/src/provider-manager.js.map +1 -0
- package/dist/src/receipt.d.ts +87 -0
- package/dist/src/receipt.js +326 -0
- package/dist/src/receipt.js.map +1 -0
- package/dist/src/recommendations.d.ts +13 -0
- package/dist/src/recommendations.js +291 -0
- package/dist/src/recommendations.js.map +1 -0
- package/dist/src/redact.d.ts +15 -0
- package/dist/src/redact.js +129 -0
- package/dist/src/redact.js.map +1 -0
- package/dist/src/replit.d.ts +397 -0
- package/dist/src/replit.js +1160 -0
- package/dist/src/replit.js.map +1 -0
- package/dist/src/repo.d.ts +149 -0
- package/dist/src/repo.js +416 -0
- package/dist/src/repo.js.map +1 -0
- package/dist/src/revert.d.ts +30 -0
- package/dist/src/revert.js +166 -0
- package/dist/src/revert.js.map +1 -0
- package/dist/src/room.d.ts +102 -0
- package/dist/src/room.js +212 -0
- package/dist/src/room.js.map +1 -0
- package/dist/src/routing-advisor.d.ts +57 -0
- package/dist/src/routing-advisor.js +221 -0
- package/dist/src/routing-advisor.js.map +1 -0
- package/dist/src/self-correct.d.ts +40 -0
- package/dist/src/self-correct.js +137 -0
- package/dist/src/self-correct.js.map +1 -0
- package/dist/src/session-lock.d.ts +35 -0
- package/dist/src/session-lock.js +134 -0
- package/dist/src/session-lock.js.map +1 -0
- package/dist/src/session.d.ts +267 -0
- package/dist/src/session.js +1660 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/settings-tui.d.ts +5 -0
- package/dist/src/settings-tui.js +422 -0
- package/dist/src/settings-tui.js.map +1 -0
- package/dist/src/setup-flow.d.ts +63 -0
- package/dist/src/setup-flow.js +233 -0
- package/dist/src/setup-flow.js.map +1 -0
- package/dist/src/signal.d.ts +19 -0
- package/dist/src/signal.js +122 -0
- package/dist/src/signal.js.map +1 -0
- package/dist/src/simmer.d.ts +85 -0
- package/dist/src/simmer.js +224 -0
- package/dist/src/simmer.js.map +1 -0
- package/dist/src/state-export.d.ts +129 -0
- package/dist/src/state-export.js +233 -0
- package/dist/src/state-export.js.map +1 -0
- package/dist/src/strategy.d.ts +54 -0
- package/dist/src/strategy.js +95 -0
- package/dist/src/strategy.js.map +1 -0
- package/dist/src/subscription.d.ts +40 -0
- package/dist/src/subscription.js +189 -0
- package/dist/src/subscription.js.map +1 -0
- package/dist/src/templates.d.ts +208 -0
- package/dist/src/templates.js +238 -0
- package/dist/src/templates.js.map +1 -0
- package/dist/src/test.d.ts +9 -0
- package/dist/src/test.js +1173 -0
- package/dist/src/test.js.map +1 -0
- package/dist/src/think-engine.d.ts +67 -0
- package/dist/src/think-engine.js +412 -0
- package/dist/src/think-engine.js.map +1 -0
- package/dist/src/tui.d.ts +71 -0
- package/dist/src/tui.js +242 -0
- package/dist/src/tui.js.map +1 -0
- package/dist/src/types.d.ts +177 -0
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/update-check.d.ts +7 -0
- package/dist/src/update-check.js +36 -0
- package/dist/src/update-check.js.map +1 -0
- package/dist/src/wave-planner.d.ts +30 -0
- package/dist/src/wave-planner.js +281 -0
- package/dist/src/wave-planner.js.map +1 -0
- package/hooks/head-guard.sh +41 -0
- package/hooks/task-classifier.mjs +328 -0
- package/hooks/vibe-router.mjs +387 -0
- package/package.json +29 -153
- package/src/agents/registry.mjs +0 -405
- package/src/awareness.mjs +0 -425
- package/src/brief.mjs +0 -266
- package/src/calibration.mjs +0 -148
- package/src/checkpoint.mjs +0 -109
- package/src/ci-triage.mjs +0 -191
- package/src/cognitive-loop.mjs +0 -562
- package/src/collaboration.mjs +0 -545
- package/src/context-intel.mjs +0 -158
- package/src/context.mjs +0 -389
- package/src/continuity.mjs +0 -298
- package/src/cost-tracker.mjs +0 -184
- package/src/debrief.mjs +0 -228
- package/src/decide.mjs +0 -1099
- package/src/decompose.mjs +0 -331
- package/src/detect.mjs +0 -702
- package/src/dispatch.mjs +0 -1447
- package/src/doctor.mjs +0 -1607
- package/src/envelope.mjs +0 -139
- package/src/failure-memory.mjs +0 -178
- package/src/fx.mjs +0 -276
- package/src/governance.mjs +0 -279
- package/src/handoff.mjs +0 -87
- package/src/head-protocol.mjs +0 -128
- package/src/head.mjs +0 -952
- package/src/health.mjs +0 -528
- package/src/inbox.mjs +0 -195
- package/src/index.mjs +0 -44
- package/src/install-hooks.mjs +0 -100
- package/src/integrity.mjs +0 -245
- package/src/intelligence.mjs +0 -447
- package/src/ledger.mjs +0 -196
- package/src/living-docs.mjs +0 -210
- package/src/memory-tiers.mjs +0 -193
- package/src/models.mjs +0 -363
- package/src/narrative.mjs +0 -169
- package/src/nextstep.mjs +0 -100
- package/src/observer.mjs +0 -241
- package/src/outcome.mjs +0 -400
- package/src/pipeline.mjs +0 -1711
- package/src/playbook.mjs +0 -257
- package/src/pr-agent.mjs +0 -214
- package/src/predictive.mjs +0 -250
- package/src/profile.mjs +0 -1411
- package/src/prompt-audit.mjs +0 -231
- package/src/prompt-intel.mjs +0 -325
- package/src/provider-context.mjs +0 -257
- package/src/receipt.mjs +0 -344
- package/src/recommendations.mjs +0 -296
- package/src/redact.mjs +0 -192
- package/src/replit.mjs +0 -1210
- package/src/repo.mjs +0 -445
- package/src/revert.mjs +0 -149
- package/src/routing-advisor.mjs +0 -204
- package/src/self-correct.mjs +0 -147
- package/src/session-lock.mjs +0 -160
- package/src/session.mjs +0 -1655
- package/src/settings-tui.mjs +0 -373
- package/src/setup-flow.mjs +0 -223
- package/src/signal.mjs +0 -115
- package/src/simmer.mjs +0 -241
- package/src/strategy.mjs +0 -235
- package/src/subscription.mjs +0 -212
- package/src/templates.mjs +0 -260
- package/src/think-engine.mjs +0 -428
- package/src/tui.mjs +0 -276
- package/src/update-check.mjs +0 -35
- package/src/wave-planner.mjs +0 -294
package/src/inbox.mjs
DELETED
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* inbox.mjs — Cross-session signal system for dual-brain orchestrator
|
|
3
|
-
*
|
|
4
|
-
* Agents write messages to .dualbrain/inbox/. HEAD and the cognitive loop
|
|
5
|
-
* check the inbox at entry points. Messages have TTL and recipient types.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
9
|
-
import { join } from 'node:path';
|
|
10
|
-
import { randomUUID } from 'node:crypto';
|
|
11
|
-
|
|
12
|
-
const INBOX_DIR = join(process.cwd(), '.dualbrain', 'inbox');
|
|
13
|
-
const INDEX_PATH = join(INBOX_DIR, '_index.json');
|
|
14
|
-
const DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24h
|
|
15
|
-
const MAX_ACTIVE = 50;
|
|
16
|
-
const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
17
|
-
|
|
18
|
-
function ensureDir() {
|
|
19
|
-
if (!existsSync(INBOX_DIR)) mkdirSync(INBOX_DIR, { recursive: true });
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function readIndex() {
|
|
23
|
-
try {
|
|
24
|
-
if (existsSync(INDEX_PATH)) return JSON.parse(readFileSync(INDEX_PATH, 'utf8'));
|
|
25
|
-
} catch { /* rebuild */ }
|
|
26
|
-
return rebuildIndex();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function rebuildIndex() {
|
|
30
|
-
ensureDir();
|
|
31
|
-
const entries = [];
|
|
32
|
-
for (const f of readdirSync(INBOX_DIR)) {
|
|
33
|
-
if (f === '_index.json' || !f.endsWith('.json')) continue;
|
|
34
|
-
try {
|
|
35
|
-
const msg = JSON.parse(readFileSync(join(INBOX_DIR, f), 'utf8'));
|
|
36
|
-
entries.push({ id: msg.id, to: msg.to, type: msg.type, priority: msg.priority, createdAt: msg.createdAt, expired: Date.now() > msg.createdAt + msg.ttl });
|
|
37
|
-
} catch { /* skip corrupt */ }
|
|
38
|
-
}
|
|
39
|
-
writeFileSync(INDEX_PATH, JSON.stringify(entries, null, 2));
|
|
40
|
-
return entries;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function writeIndex(entries) {
|
|
44
|
-
ensureDir();
|
|
45
|
-
writeFileSync(INDEX_PATH, JSON.stringify(entries, null, 2));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function readMessage(id) {
|
|
49
|
-
try {
|
|
50
|
-
return JSON.parse(readFileSync(join(INBOX_DIR, `${id}.json`), 'utf8'));
|
|
51
|
-
} catch { return null; }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function matchesRecipient(msgTo, recipient) {
|
|
55
|
-
if (msgTo === 'all' || msgTo === recipient) return true;
|
|
56
|
-
if (msgTo === 'worker:*' && recipient.startsWith('worker:')) return true;
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Write a message to the inbox. */
|
|
61
|
-
export function send(partial) {
|
|
62
|
-
if (!partial.to || !partial.type || !partial.subject || !partial.body) {
|
|
63
|
-
throw new Error('inbox.send requires: to, type, subject, body');
|
|
64
|
-
}
|
|
65
|
-
ensureDir();
|
|
66
|
-
const msg = {
|
|
67
|
-
id: randomUUID(),
|
|
68
|
-
from: partial.from || 'system',
|
|
69
|
-
to: partial.to,
|
|
70
|
-
type: partial.type,
|
|
71
|
-
priority: partial.priority || 'medium',
|
|
72
|
-
subject: partial.subject,
|
|
73
|
-
body: partial.body,
|
|
74
|
-
ttl: partial.ttl ?? DEFAULT_TTL,
|
|
75
|
-
createdAt: Date.now(),
|
|
76
|
-
readBy: partial.readBy || [],
|
|
77
|
-
relatedFiles: partial.relatedFiles || [],
|
|
78
|
-
tags: partial.tags || [],
|
|
79
|
-
};
|
|
80
|
-
// Enforce cap — purge oldest if over limit
|
|
81
|
-
const index = readIndex();
|
|
82
|
-
const active = index.filter(e => !e.expired);
|
|
83
|
-
if (active.length >= MAX_ACTIVE) {
|
|
84
|
-
const sorted = [...active].sort((a, b) => a.createdAt - b.createdAt);
|
|
85
|
-
const toRemove = sorted.slice(0, active.length - MAX_ACTIVE + 1);
|
|
86
|
-
for (const e of toRemove) {
|
|
87
|
-
try { unlinkSync(join(INBOX_DIR, `${e.id}.json`)); } catch { /* ok */ }
|
|
88
|
-
}
|
|
89
|
-
const removeIds = new Set(toRemove.map(e => e.id));
|
|
90
|
-
const trimmed = index.filter(e => !removeIds.has(e.id));
|
|
91
|
-
trimmed.push({ id: msg.id, to: msg.to, type: msg.type, priority: msg.priority, createdAt: msg.createdAt, expired: false });
|
|
92
|
-
writeIndex(trimmed);
|
|
93
|
-
} else {
|
|
94
|
-
index.push({ id: msg.id, to: msg.to, type: msg.type, priority: msg.priority, createdAt: msg.createdAt, expired: false });
|
|
95
|
-
writeIndex(index);
|
|
96
|
-
}
|
|
97
|
-
writeFileSync(join(INBOX_DIR, `${msg.id}.json`), JSON.stringify(msg, null, 2));
|
|
98
|
-
return msg;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Read messages for a recipient. */
|
|
102
|
-
export function check(recipient, options = {}) {
|
|
103
|
-
const { unreadOnly = false, types, minPriority, limit } = options;
|
|
104
|
-
const index = readIndex();
|
|
105
|
-
const now = Date.now();
|
|
106
|
-
const minP = minPriority ? PRIORITY_ORDER[minPriority] ?? 3 : 3;
|
|
107
|
-
let results = [];
|
|
108
|
-
for (const entry of index) {
|
|
109
|
-
if (now > entry.createdAt + DEFAULT_TTL) continue; // rough TTL check
|
|
110
|
-
if (!matchesRecipient(entry.to, recipient)) continue;
|
|
111
|
-
if (types && !types.includes(entry.type)) continue;
|
|
112
|
-
if ((PRIORITY_ORDER[entry.priority] ?? 3) > minP) continue;
|
|
113
|
-
const msg = readMessage(entry.id);
|
|
114
|
-
if (!msg) continue;
|
|
115
|
-
if (now > msg.createdAt + msg.ttl) continue; // precise TTL
|
|
116
|
-
if (unreadOnly && msg.readBy.includes(recipient)) continue;
|
|
117
|
-
results.push(msg);
|
|
118
|
-
}
|
|
119
|
-
results.sort((a, b) => (PRIORITY_ORDER[a.priority] ?? 3) - (PRIORITY_ORDER[b.priority] ?? 3) || b.createdAt - a.createdAt);
|
|
120
|
-
if (limit) results = results.slice(0, limit);
|
|
121
|
-
return results;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Mark a message as read by a specific reader. */
|
|
125
|
-
export function markRead(messageId, reader) {
|
|
126
|
-
const path = join(INBOX_DIR, `${messageId}.json`);
|
|
127
|
-
const msg = readMessage(messageId);
|
|
128
|
-
if (!msg) return;
|
|
129
|
-
if (!msg.readBy.includes(reader)) {
|
|
130
|
-
msg.readBy.push(reader);
|
|
131
|
-
writeFileSync(path, JSON.stringify(msg, null, 2));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Clean up expired and fully-read messages. */
|
|
136
|
-
export function purge(options = {}) {
|
|
137
|
-
const { purgeRead = false } = options;
|
|
138
|
-
const index = readIndex();
|
|
139
|
-
const now = Date.now();
|
|
140
|
-
let expired = 0, read = 0;
|
|
141
|
-
const keep = [];
|
|
142
|
-
for (const entry of index) {
|
|
143
|
-
const msg = readMessage(entry.id);
|
|
144
|
-
if (!msg) continue;
|
|
145
|
-
if (now > msg.createdAt + msg.ttl) {
|
|
146
|
-
try { unlinkSync(join(INBOX_DIR, `${entry.id}.json`)); } catch { /* ok */ }
|
|
147
|
-
expired++;
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
if (purgeRead && msg.readBy.length > 0 && msg.to !== 'all') {
|
|
151
|
-
try { unlinkSync(join(INBOX_DIR, `${entry.id}.json`)); } catch { /* ok */ }
|
|
152
|
-
read++;
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
keep.push(entry);
|
|
156
|
-
}
|
|
157
|
-
writeIndex(keep);
|
|
158
|
-
return { expired, read };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/** Produce a concise text summary for prompt injection. */
|
|
162
|
-
export function generateInboxBrief(recipient) {
|
|
163
|
-
const msgs = check(recipient, { unreadOnly: true, limit: 5 });
|
|
164
|
-
if (!msgs.length) return '';
|
|
165
|
-
const lines = [`\u{1F4EC} Inbox (${msgs.length} unread):`];
|
|
166
|
-
let len = lines[0].length;
|
|
167
|
-
for (const m of msgs) {
|
|
168
|
-
const line = `• [${m.priority}] ${m.from !== 'system' ? `From ${m.from}: ` : ''}${m.subject}`;
|
|
169
|
-
if (len + line.length > 480) { lines.push('• ...'); break; }
|
|
170
|
-
lines.push(line);
|
|
171
|
-
len += line.length;
|
|
172
|
-
}
|
|
173
|
-
return lines.join('\n');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Convenience: send a continuation message when session ends. */
|
|
177
|
-
export function sendContinuation(context) {
|
|
178
|
-
return send({
|
|
179
|
-
from: context.from || 'head',
|
|
180
|
-
to: 'session:next',
|
|
181
|
-
type: 'continuation',
|
|
182
|
-
priority: 'high',
|
|
183
|
-
subject: context.subject || 'Session continuation state',
|
|
184
|
-
body: typeof context.body === 'string' ? context.body : JSON.stringify(context.state || context, null, 2),
|
|
185
|
-
relatedFiles: context.relatedFiles || [],
|
|
186
|
-
tags: ['continuation', ...(context.tags || [])],
|
|
187
|
-
ttl: context.ttl ?? DEFAULT_TTL * 3, // 72h for continuations
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** Convenience: check for the most recent unread continuation. */
|
|
192
|
-
export function checkContinuation(reader = 'head') {
|
|
193
|
-
const msgs = check('session:next', { unreadOnly: true, types: ['continuation'], limit: 1 });
|
|
194
|
-
return msgs.length ? msgs[0] : null;
|
|
195
|
-
}
|
package/src/index.mjs
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* index.mjs — Main entry point for the dual-brain package.
|
|
4
|
-
*
|
|
5
|
-
* Re-exports all public APIs from the four core modules, plus a top-level
|
|
6
|
-
* orchestrate() convenience function for programmatic use.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export { loadProfile, saveProfile, ensureProfile, runOnboarding, rememberPreference, forgetPreference, getActivePreferences, getAvailableProviders, isSoloBrain, getHeadModel, detectAuth, detectEnvironment, saveSubscription, listSubscriptions, autoRefreshToken } from './profile.mjs';
|
|
10
|
-
export { detectTask, classifyIntent, classifyRisk, estimateComplexity, inferTier, extractPaths } from './detect.mjs';
|
|
11
|
-
export { decideRoute, getModelCapabilities, getAvailableModels, shouldDualBrain, explainDecision } from './decide.mjs';
|
|
12
|
-
export { dispatch, buildCommand, detectRuntime, compressResult, dispatchDualBrain } from './dispatch.mjs';
|
|
13
|
-
export { loadPlaybook, listPlaybooks, executePlaybook, createRunArtifact } from './playbook.mjs';
|
|
14
|
-
export { getHealth, markHot, markDegraded, markHealthy, checkCooldown, getProviderScore, recordDispatch, getSessionStats, resetHealth, remainingCooldownMinutes } from './health.mjs';
|
|
15
|
-
export { detectRepo, loadRepoCache, getTestCommand, getLintCommand } from './repo.mjs';
|
|
16
|
-
export { loadSession, saveSession, updateSession, clearSession, formatSessionCard, importReplitSessions, renameSession, pinSession, unpinSession, categorizeSession, getSessionMeta, autoLabel, enrichSessions, ensurePersistence, syncSessionMirror, buildSessionIndex, searchSessions, getSessionContext, extractSessionMeta, getRoutingContext } from './session.mjs';
|
|
17
|
-
export { decompose, isSimpleTask, taskGraphToWaves } from './decompose.mjs';
|
|
18
|
-
export { generateBrief, compressPriorResults, listRoles } from './brief.mjs';
|
|
19
|
-
export { redact, redactFiles, isSecretFile } from './redact.mjs';
|
|
20
|
-
export { isInsideClaude, buildNativeDispatch, normalizeResult } from './dispatch.mjs';
|
|
21
|
-
export { box, bar, badge, menu, separator } from './tui.mjs';
|
|
22
|
-
|
|
23
|
-
// Top-level convenience function
|
|
24
|
-
export async function orchestrate({ prompt, files, cwd, dryRun }) {
|
|
25
|
-
// Import dynamically to avoid circular issues
|
|
26
|
-
const { ensureProfile } = await import('./profile.mjs');
|
|
27
|
-
const { detectTask } = await import('./detect.mjs');
|
|
28
|
-
const { decideRoute } = await import('./decide.mjs');
|
|
29
|
-
const { dispatch: run, dispatchDualBrain } = await import('./dispatch.mjs');
|
|
30
|
-
|
|
31
|
-
const profile = await ensureProfile(cwd || process.cwd(), { interactive: false });
|
|
32
|
-
const detection = detectTask({ prompt, files });
|
|
33
|
-
const decision = decideRoute({ profile, detection, cwd: cwd || process.cwd() });
|
|
34
|
-
|
|
35
|
-
if (dryRun) {
|
|
36
|
-
return { profile, detection, decision, result: null };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const result = decision.dualBrain
|
|
40
|
-
? await dispatchDualBrain({ decision, prompt, files, cwd: cwd || process.cwd() })
|
|
41
|
-
: await run({ decision, prompt, files, cwd: cwd || process.cwd() });
|
|
42
|
-
|
|
43
|
-
return { profile, detection, decision, result };
|
|
44
|
-
}
|
package/src/install-hooks.mjs
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* install-hooks.mjs — Merge dual-brain PreToolUse hooks into .claude/settings.json.
|
|
3
|
-
*
|
|
4
|
-
* Exported function: installHooks(cwd)
|
|
5
|
-
* Returns: { installed: string[], skipped: string[] }
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
-
import { join, dirname } from 'node:path';
|
|
10
|
-
import { fileURLToPath } from 'node:url';
|
|
11
|
-
|
|
12
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
-
const __dirname = dirname(__filename);
|
|
14
|
-
const PKG_ROOT = join(__dirname, '..');
|
|
15
|
-
|
|
16
|
-
// The hook commands we want present in .claude/settings.json PreToolUse
|
|
17
|
-
const HEAD_GUARD_CMD = 'node .claude/hooks/head-guard.mjs';
|
|
18
|
-
const ENFORCE_TIER_CMD = 'node .claude/hooks/enforce-tier.mjs';
|
|
19
|
-
|
|
20
|
-
const DESIRED_HOOKS = [
|
|
21
|
-
{ matcher: 'Edit', command: HEAD_GUARD_CMD },
|
|
22
|
-
{ matcher: 'Write', command: HEAD_GUARD_CMD },
|
|
23
|
-
{ matcher: 'NotebookEdit', command: HEAD_GUARD_CMD },
|
|
24
|
-
{ matcher: 'Bash', command: HEAD_GUARD_CMD },
|
|
25
|
-
{ matcher: 'Agent', command: ENFORCE_TIER_CMD },
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Install dual-brain enforcement hooks into a project's .claude/settings.json.
|
|
30
|
-
*
|
|
31
|
-
* @param {string} cwd - Project root directory (where .claude/ should live)
|
|
32
|
-
* @returns {{ installed: string[], skipped: string[] }}
|
|
33
|
-
*/
|
|
34
|
-
export function installHooks(cwd) {
|
|
35
|
-
const claudeDir = join(cwd, '.claude');
|
|
36
|
-
const hooksDir = join(claudeDir, 'hooks');
|
|
37
|
-
const settingsPath = join(claudeDir, 'settings.json');
|
|
38
|
-
|
|
39
|
-
const installed = [];
|
|
40
|
-
const skipped = [];
|
|
41
|
-
|
|
42
|
-
// Ensure directories exist
|
|
43
|
-
mkdirSync(hooksDir, { recursive: true });
|
|
44
|
-
|
|
45
|
-
// Copy hook files from package into project's .claude/hooks/
|
|
46
|
-
const filesToCopy = [
|
|
47
|
-
{ name: 'head-guard.mjs', exec: true },
|
|
48
|
-
{ name: 'enforce-tier.mjs', exec: false },
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
for (const { name, exec } of filesToCopy) {
|
|
52
|
-
const src = join(PKG_ROOT, 'hooks', name);
|
|
53
|
-
const dst = join(hooksDir, name);
|
|
54
|
-
if (existsSync(src)) {
|
|
55
|
-
cpSync(src, dst);
|
|
56
|
-
if (exec) {
|
|
57
|
-
try { chmodSync(dst, 0o755); } catch {}
|
|
58
|
-
}
|
|
59
|
-
installed.push(`hooks/${name}`);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Read existing settings (or start fresh)
|
|
64
|
-
let settings = {};
|
|
65
|
-
try {
|
|
66
|
-
settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
67
|
-
} catch {
|
|
68
|
-
// File doesn't exist or is malformed — start empty
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Ensure hooks.PreToolUse array exists
|
|
72
|
-
if (!settings.hooks) settings.hooks = {};
|
|
73
|
-
if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];
|
|
74
|
-
|
|
75
|
-
const preToolUse = settings.hooks.PreToolUse;
|
|
76
|
-
|
|
77
|
-
// Merge: for each desired hook, add only if command is not already registered for that matcher
|
|
78
|
-
for (const { matcher, command } of DESIRED_HOOKS) {
|
|
79
|
-
const alreadyPresent = preToolUse.some(entry =>
|
|
80
|
-
entry.matcher === matcher &&
|
|
81
|
-
Array.isArray(entry.hooks) &&
|
|
82
|
-
entry.hooks.some(h => h.command === command)
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
if (alreadyPresent) {
|
|
86
|
-
skipped.push(`PreToolUse[${matcher}]`);
|
|
87
|
-
} else {
|
|
88
|
-
preToolUse.push({
|
|
89
|
-
matcher,
|
|
90
|
-
hooks: [{ type: 'command', command }],
|
|
91
|
-
});
|
|
92
|
-
installed.push(`PreToolUse[${matcher}]`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Write back merged settings
|
|
97
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
98
|
-
|
|
99
|
-
return { installed, skipped };
|
|
100
|
-
}
|
package/src/integrity.mjs
DELETED
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* integrity.mjs — State integrity primitives for dual-brain
|
|
3
|
-
*
|
|
4
|
-
* Provides:
|
|
5
|
-
* - atomicWriteJson / readJsonSafe — safe JSON file I/O with schema versioning
|
|
6
|
-
* - acquireLock / releaseLock / withLock — advisory file locks
|
|
7
|
-
* - lockedUpdate — locked atomic read-modify-write
|
|
8
|
-
* - atomicAppend — append-only ledger with lock
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
12
|
-
import { dirname } from 'node:path';
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// 1. Atomic JSON writes
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Write JSON to filePath atomically via a temp file + rename.
|
|
20
|
-
* Adds _schemaVersion and _writtenAt to plain objects.
|
|
21
|
-
*
|
|
22
|
-
* @param {string} filePath - Destination file path
|
|
23
|
-
* @param {*} data - Value to serialize
|
|
24
|
-
* @param {object} opts
|
|
25
|
-
* @param {number} [opts.schemaVersion=1] - Schema version stamped into data
|
|
26
|
-
* @param {boolean}[opts.backup=false] - Keep a .bak copy of the previous file
|
|
27
|
-
*/
|
|
28
|
-
export function atomicWriteJson(filePath, data, opts = {}) {
|
|
29
|
-
const { schemaVersion = 1, backup = false } = opts;
|
|
30
|
-
|
|
31
|
-
// Stamp schema version onto plain objects
|
|
32
|
-
if (data && typeof data === 'object' && !Array.isArray(data)) {
|
|
33
|
-
data._schemaVersion = schemaVersion;
|
|
34
|
-
data._writtenAt = new Date().toISOString();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const dir = dirname(filePath);
|
|
38
|
-
mkdirSync(dir, { recursive: true });
|
|
39
|
-
|
|
40
|
-
const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}`;
|
|
41
|
-
const json = JSON.stringify(data, null, 2) + '\n';
|
|
42
|
-
|
|
43
|
-
// Write to temp file
|
|
44
|
-
writeFileSync(tmpPath, json);
|
|
45
|
-
|
|
46
|
-
// Validate the temp file is parseable before committing
|
|
47
|
-
try {
|
|
48
|
-
JSON.parse(readFileSync(tmpPath, 'utf8'));
|
|
49
|
-
} catch (err) {
|
|
50
|
-
unlinkSync(tmpPath);
|
|
51
|
-
throw new Error(`atomicWrite: validation failed for ${filePath}: ${err.message}`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Optionally back up the existing file
|
|
55
|
-
if (backup && existsSync(filePath)) {
|
|
56
|
-
const backupPath = filePath + '.bak';
|
|
57
|
-
try { renameSync(filePath, backupPath); } catch {}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Atomic rename — either fully succeeds or the original is untouched
|
|
61
|
-
renameSync(tmpPath, filePath);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Read and parse a JSON file safely, with optional schema migration.
|
|
66
|
-
* Falls back to a .bak copy on parse failure.
|
|
67
|
-
* Returns null when the file is absent or unrecoverable.
|
|
68
|
-
*
|
|
69
|
-
* @param {string} filePath
|
|
70
|
-
* @param {object} opts
|
|
71
|
-
* @param {number} [opts.expectedVersion] - Schema version to verify
|
|
72
|
-
* @param {Function}[opts.migrate] - (data, fromVersion, toVersion) => data
|
|
73
|
-
* @returns {*|null}
|
|
74
|
-
*/
|
|
75
|
-
export function readJsonSafe(filePath, opts = {}) {
|
|
76
|
-
const { expectedVersion, migrate } = opts;
|
|
77
|
-
|
|
78
|
-
if (!existsSync(filePath)) return null;
|
|
79
|
-
|
|
80
|
-
let data;
|
|
81
|
-
try {
|
|
82
|
-
data = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
83
|
-
} catch {
|
|
84
|
-
// Primary file corrupt — try backup
|
|
85
|
-
const bakPath = filePath + '.bak';
|
|
86
|
-
if (existsSync(bakPath)) {
|
|
87
|
-
try {
|
|
88
|
-
data = JSON.parse(readFileSync(bakPath, 'utf8'));
|
|
89
|
-
} catch { return null; }
|
|
90
|
-
} else {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Schema version check with optional migration
|
|
96
|
-
if (expectedVersion !== undefined && data?._schemaVersion !== expectedVersion) {
|
|
97
|
-
if (migrate && typeof migrate === 'function') {
|
|
98
|
-
data = migrate(data, data?._schemaVersion, expectedVersion);
|
|
99
|
-
}
|
|
100
|
-
// Tolerant read: return data even without a migrator
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return data;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ---------------------------------------------------------------------------
|
|
107
|
-
// 2. Advisory file locks
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
|
|
110
|
-
const LOCK_TIMEOUT_MS = 10_000; // stale lock threshold
|
|
111
|
-
const LOCK_RETRY_MS = 50; // busy-wait interval
|
|
112
|
-
const LOCK_MAX_RETRIES = 100; // max retries (~5 s)
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Acquire an advisory lock for filePath by creating filePath.lock.
|
|
116
|
-
* Stale locks (> LOCK_TIMEOUT_MS old) are cleared automatically.
|
|
117
|
-
*
|
|
118
|
-
* @param {string} filePath
|
|
119
|
-
* @returns {{ acquired: boolean, lockPath: string, reason?: string }}
|
|
120
|
-
*/
|
|
121
|
-
export function acquireLock(filePath) {
|
|
122
|
-
const lockPath = filePath + '.lock';
|
|
123
|
-
|
|
124
|
-
// Clear stale or corrupt lock
|
|
125
|
-
if (existsSync(lockPath)) {
|
|
126
|
-
try {
|
|
127
|
-
const lockData = JSON.parse(readFileSync(lockPath, 'utf8'));
|
|
128
|
-
const age = Date.now() - (lockData.createdAt || 0);
|
|
129
|
-
if (age > LOCK_TIMEOUT_MS) {
|
|
130
|
-
unlinkSync(lockPath);
|
|
131
|
-
}
|
|
132
|
-
} catch {
|
|
133
|
-
try { unlinkSync(lockPath); } catch {}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Spin-try to create the lock exclusively
|
|
138
|
-
let retries = 0;
|
|
139
|
-
while (retries < LOCK_MAX_RETRIES) {
|
|
140
|
-
try {
|
|
141
|
-
writeFileSync(lockPath, JSON.stringify({
|
|
142
|
-
pid: process.pid,
|
|
143
|
-
createdAt: Date.now(),
|
|
144
|
-
holder: process.argv[1] || 'unknown',
|
|
145
|
-
}), { flag: 'wx' }); // 'wx' = exclusive create, EEXIST if present
|
|
146
|
-
return { acquired: true, lockPath };
|
|
147
|
-
} catch (err) {
|
|
148
|
-
if (err.code === 'EEXIST') {
|
|
149
|
-
retries++;
|
|
150
|
-
// Synchronous busy-wait — intentional; only triggered under contention
|
|
151
|
-
const start = Date.now();
|
|
152
|
-
while (Date.now() - start < LOCK_RETRY_MS) {}
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
throw err; // Unexpected error — propagate
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return { acquired: false, lockPath, reason: 'timeout' };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Release a previously acquired lock.
|
|
164
|
-
*
|
|
165
|
-
* @param {{ lockPath?: string }} lockResult - Return value of acquireLock
|
|
166
|
-
*/
|
|
167
|
-
export function releaseLock(lockResult) {
|
|
168
|
-
if (lockResult?.lockPath) {
|
|
169
|
-
try { unlinkSync(lockResult.lockPath); } catch {}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Run fn while holding an advisory lock on filePath.
|
|
175
|
-
* Throws if the lock cannot be acquired within the retry window.
|
|
176
|
-
*
|
|
177
|
-
* @param {string} filePath
|
|
178
|
-
* @param {Function} fn
|
|
179
|
-
* @returns {*} Return value of fn
|
|
180
|
-
*/
|
|
181
|
-
export function withLock(filePath, fn) {
|
|
182
|
-
const lock = acquireLock(filePath);
|
|
183
|
-
if (!lock.acquired) {
|
|
184
|
-
throw new Error(`Could not acquire lock for ${filePath}: ${lock.reason}`);
|
|
185
|
-
}
|
|
186
|
-
try {
|
|
187
|
-
return fn();
|
|
188
|
-
} finally {
|
|
189
|
-
releaseLock(lock);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Locked atomic read-modify-write.
|
|
195
|
-
* Reads the current JSON, passes it to updateFn, then writes the result.
|
|
196
|
-
* If updateFn returns undefined the file is left unchanged.
|
|
197
|
-
*
|
|
198
|
-
* @param {string} filePath
|
|
199
|
-
* @param {Function} updateFn - (currentData: *|null) => updatedData | undefined
|
|
200
|
-
* @param {object} opts - Forwarded to readJsonSafe and atomicWriteJson
|
|
201
|
-
* @returns {*} Return value of updateFn
|
|
202
|
-
*/
|
|
203
|
-
export function lockedUpdate(filePath, updateFn, opts = {}) {
|
|
204
|
-
return withLock(filePath, () => {
|
|
205
|
-
const current = readJsonSafe(filePath, opts);
|
|
206
|
-
const updated = updateFn(current);
|
|
207
|
-
if (updated !== undefined) {
|
|
208
|
-
atomicWriteJson(filePath, updated, opts);
|
|
209
|
-
}
|
|
210
|
-
return updated;
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
215
|
-
// 3. Append-only ledger with lock
|
|
216
|
-
// ---------------------------------------------------------------------------
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Append a NDJSON record to filePath under an advisory lock.
|
|
220
|
-
* On lock failure the write is attempted without a lock (best-effort).
|
|
221
|
-
*
|
|
222
|
-
* @param {string} filePath
|
|
223
|
-
* @param {*} record - Value to serialize as one JSON line
|
|
224
|
-
*/
|
|
225
|
-
export async function atomicAppend(filePath, record) {
|
|
226
|
-
const { appendFileSync } = await import('node:fs');
|
|
227
|
-
const line = JSON.stringify(record) + '\n';
|
|
228
|
-
|
|
229
|
-
const lock = acquireLock(filePath);
|
|
230
|
-
if (!lock.acquired) {
|
|
231
|
-
// Non-fatal: best-effort append without lock
|
|
232
|
-
try {
|
|
233
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
234
|
-
appendFileSync(filePath, line);
|
|
235
|
-
} catch {}
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
try {
|
|
240
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
241
|
-
appendFileSync(filePath, line);
|
|
242
|
-
} finally {
|
|
243
|
-
releaseLock(lock);
|
|
244
|
-
}
|
|
245
|
-
}
|