@synergenius/flow-weaver-pack-weaver 0.9.7 → 0.9.9
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/dist/bot/agent-loop.d.ts +20 -0
- package/dist/bot/agent-loop.d.ts.map +1 -0
- package/dist/bot/agent-loop.js +331 -0
- package/dist/bot/agent-loop.js.map +1 -0
- package/dist/bot/agent-provider.d.ts.map +1 -1
- package/dist/bot/agent-provider.js +3 -2
- package/dist/bot/agent-provider.js.map +1 -1
- package/dist/bot/approvals.js +17 -8
- package/dist/bot/approvals.js.map +1 -1
- package/dist/bot/assistant-core.d.ts +17 -0
- package/dist/bot/assistant-core.d.ts.map +1 -1
- package/dist/bot/assistant-core.js +418 -60
- package/dist/bot/assistant-core.js.map +1 -1
- package/dist/bot/assistant-tools.d.ts +1 -1
- package/dist/bot/assistant-tools.d.ts.map +1 -1
- package/dist/bot/assistant-tools.js +283 -9
- package/dist/bot/assistant-tools.js.map +1 -1
- package/dist/bot/bot-agent-channel.d.ts.map +1 -1
- package/dist/bot/bot-agent-channel.js +2 -0
- package/dist/bot/bot-agent-channel.js.map +1 -1
- package/dist/bot/bot-manager.d.ts +4 -0
- package/dist/bot/bot-manager.d.ts.map +1 -1
- package/dist/bot/bot-manager.js +72 -27
- package/dist/bot/bot-manager.js.map +1 -1
- package/dist/bot/conversation-store.d.ts +6 -5
- package/dist/bot/conversation-store.d.ts.map +1 -1
- package/dist/bot/conversation-store.js +98 -42
- package/dist/bot/conversation-store.js.map +1 -1
- package/dist/bot/cost-store.d.ts +3 -0
- package/dist/bot/cost-store.d.ts.map +1 -1
- package/dist/bot/cost-store.js +21 -10
- package/dist/bot/cost-store.js.map +1 -1
- package/dist/bot/cost-tracker.d.ts.map +1 -1
- package/dist/bot/cost-tracker.js +14 -1
- package/dist/bot/cost-tracker.js.map +1 -1
- package/dist/bot/cron-parser.d.ts.map +1 -1
- package/dist/bot/cron-parser.js +2 -0
- package/dist/bot/cron-parser.js.map +1 -1
- package/dist/bot/cron-scheduler.d.ts.map +1 -1
- package/dist/bot/cron-scheduler.js +1 -0
- package/dist/bot/cron-scheduler.js.map +1 -1
- package/dist/bot/device-connection.d.ts +13 -0
- package/dist/bot/device-connection.d.ts.map +1 -0
- package/dist/bot/device-connection.js +102 -0
- package/dist/bot/device-connection.js.map +1 -0
- package/dist/bot/error-classifier.d.ts.map +1 -1
- package/dist/bot/error-classifier.js +5 -0
- package/dist/bot/error-classifier.js.map +1 -1
- package/dist/bot/file-lock.d.ts.map +1 -1
- package/dist/bot/file-lock.js +13 -3
- package/dist/bot/file-lock.js.map +1 -1
- package/dist/bot/file-watcher.d.ts.map +1 -1
- package/dist/bot/file-watcher.js +1 -0
- package/dist/bot/file-watcher.js.map +1 -1
- package/dist/bot/genesis-prompt-context.d.ts +5 -0
- package/dist/bot/genesis-prompt-context.d.ts.map +1 -1
- package/dist/bot/genesis-prompt-context.js +55 -0
- package/dist/bot/genesis-prompt-context.js.map +1 -1
- package/dist/bot/genesis-store.d.ts +4 -0
- package/dist/bot/genesis-store.d.ts.map +1 -1
- package/dist/bot/genesis-store.js +79 -12
- package/dist/bot/genesis-store.js.map +1 -1
- package/dist/bot/improve-loop.d.ts +46 -0
- package/dist/bot/improve-loop.d.ts.map +1 -0
- package/dist/bot/improve-loop.js +592 -0
- package/dist/bot/improve-loop.js.map +1 -0
- package/dist/bot/insight-engine.d.ts +12 -0
- package/dist/bot/insight-engine.d.ts.map +1 -0
- package/dist/bot/insight-engine.js +256 -0
- package/dist/bot/insight-engine.js.map +1 -0
- package/dist/bot/knowledge-store.d.ts.map +1 -1
- package/dist/bot/knowledge-store.js +4 -1
- package/dist/bot/knowledge-store.js.map +1 -1
- package/dist/bot/pipeline-runner.d.ts.map +1 -1
- package/dist/bot/pipeline-runner.js +12 -4
- package/dist/bot/pipeline-runner.js.map +1 -1
- package/dist/bot/project-model.d.ts +25 -0
- package/dist/bot/project-model.d.ts.map +1 -0
- package/dist/bot/project-model.js +372 -0
- package/dist/bot/project-model.js.map +1 -0
- package/dist/bot/response-formatter.js +2 -3
- package/dist/bot/response-formatter.js.map +1 -1
- package/dist/bot/run-store.d.ts.map +1 -1
- package/dist/bot/run-store.js +10 -2
- package/dist/bot/run-store.js.map +1 -1
- package/dist/bot/safe-path.d.ts +1 -1
- package/dist/bot/safe-path.d.ts.map +1 -1
- package/dist/bot/safe-path.js +20 -1
- package/dist/bot/safe-path.js.map +1 -1
- package/dist/bot/safety.d.ts +10 -2
- package/dist/bot/safety.d.ts.map +1 -1
- package/dist/bot/safety.js +45 -2
- package/dist/bot/safety.js.map +1 -1
- package/dist/bot/session-state.d.ts +4 -0
- package/dist/bot/session-state.d.ts.map +1 -1
- package/dist/bot/session-state.js +52 -9
- package/dist/bot/session-state.js.map +1 -1
- package/dist/bot/slash-commands.d.ts.map +1 -1
- package/dist/bot/slash-commands.js +109 -3
- package/dist/bot/slash-commands.js.map +1 -1
- package/dist/bot/steering-engine.d.ts +67 -0
- package/dist/bot/steering-engine.d.ts.map +1 -0
- package/dist/bot/steering-engine.js +198 -0
- package/dist/bot/steering-engine.js.map +1 -0
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +62 -25
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/system-prompt.d.ts.map +1 -1
- package/dist/bot/system-prompt.js +5 -2
- package/dist/bot/system-prompt.js.map +1 -1
- package/dist/bot/task-queue.d.ts +6 -1
- package/dist/bot/task-queue.d.ts.map +1 -1
- package/dist/bot/task-queue.js +43 -4
- package/dist/bot/task-queue.js.map +1 -1
- package/dist/bot/tool-registry.d.ts +1 -1
- package/dist/bot/tool-registry.d.ts.map +1 -1
- package/dist/bot/tool-registry.js +65 -4
- package/dist/bot/tool-registry.js.map +1 -1
- package/dist/bot/trust-calculator.d.ts +34 -0
- package/dist/bot/trust-calculator.d.ts.map +1 -0
- package/dist/bot/trust-calculator.js +67 -0
- package/dist/bot/trust-calculator.js.map +1 -0
- package/dist/bot/types.d.ts +97 -0
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/bot/update-checker.d.ts +21 -0
- package/dist/bot/update-checker.d.ts.map +1 -0
- package/dist/bot/update-checker.js +129 -0
- package/dist/bot/update-checker.js.map +1 -0
- package/dist/bot/weaver-tools.d.ts.map +1 -1
- package/dist/bot/weaver-tools.js +11 -4
- package/dist/bot/weaver-tools.js.map +1 -1
- package/dist/cli-bridge.d.ts +2 -0
- package/dist/cli-bridge.d.ts.map +1 -1
- package/dist/cli-bridge.js +3 -1
- package/dist/cli-bridge.js.map +1 -1
- package/dist/cli-handlers.d.ts +10 -1
- package/dist/cli-handlers.d.ts.map +1 -1
- package/dist/cli-handlers.js +141 -24
- package/dist/cli-handlers.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +749 -0
- package/dist/cli.js.map +1 -0
- package/dist/docs/weaver-config.md +15 -9
- package/dist/handlers/on-execution-completed.d.ts +11 -0
- package/dist/handlers/on-execution-completed.d.ts.map +1 -0
- package/dist/handlers/on-execution-completed.js +25 -0
- package/dist/handlers/on-execution-completed.js.map +1 -0
- package/dist/mcp-tools.d.ts.map +1 -1
- package/dist/mcp-tools.js +33 -0
- package/dist/mcp-tools.js.map +1 -1
- package/dist/node-types/genesis-approve.d.ts.map +1 -1
- package/dist/node-types/genesis-approve.js +28 -3
- package/dist/node-types/genesis-approve.js.map +1 -1
- package/dist/node-types/genesis-observe.d.ts.map +1 -1
- package/dist/node-types/genesis-observe.js +23 -13
- package/dist/node-types/genesis-observe.js.map +1 -1
- package/dist/node-types/genesis-propose.d.ts.map +1 -1
- package/dist/node-types/genesis-propose.js +8 -0
- package/dist/node-types/genesis-propose.js.map +1 -1
- package/dist/node-types/genesis-update-history.d.ts.map +1 -1
- package/dist/node-types/genesis-update-history.js +13 -0
- package/dist/node-types/genesis-update-history.js.map +1 -1
- package/dist/templates/weaver-template.d.ts +11 -0
- package/dist/templates/weaver-template.d.ts.map +1 -0
- package/dist/templates/weaver-template.js +53 -0
- package/dist/templates/weaver-template.js.map +1 -0
- package/dist/workflows/weaver-bot-session.d.ts +65 -0
- package/dist/workflows/weaver-bot-session.d.ts.map +1 -0
- package/dist/workflows/weaver-bot-session.js +68 -0
- package/dist/workflows/weaver-bot-session.js.map +1 -0
- package/dist/workflows/weaver.d.ts +24 -0
- package/dist/workflows/weaver.d.ts.map +1 -0
- package/dist/workflows/weaver.js +28 -0
- package/dist/workflows/weaver.js.map +1 -0
- package/flowweaver.manifest.json +28 -1
- package/package.json +6 -3
- package/src/bot/agent-provider.ts +3 -2
- package/src/bot/approvals.ts +16 -8
- package/src/bot/assistant-core.ts +420 -63
- package/src/bot/assistant-tools.ts +291 -9
- package/src/bot/bot-agent-channel.ts +2 -0
- package/src/bot/bot-manager.ts +70 -29
- package/src/bot/conversation-store.ts +87 -42
- package/src/bot/cost-store.ts +20 -9
- package/src/bot/cost-tracker.ts +13 -1
- package/src/bot/cron-parser.ts +1 -0
- package/src/bot/cron-scheduler.ts +1 -0
- package/src/bot/device-connection.ts +102 -0
- package/src/bot/error-classifier.ts +5 -0
- package/src/bot/file-lock.ts +12 -2
- package/src/bot/file-watcher.ts +1 -0
- package/src/bot/genesis-prompt-context.ts +61 -0
- package/src/bot/genesis-store.ts +68 -16
- package/src/bot/improve-loop.ts +651 -0
- package/src/bot/insight-engine.ts +273 -0
- package/src/bot/knowledge-store.ts +4 -1
- package/src/bot/pipeline-runner.ts +11 -6
- package/src/bot/project-model.ts +404 -0
- package/src/bot/response-formatter.ts +2 -3
- package/src/bot/run-store.ts +5 -2
- package/src/bot/safe-path.ts +20 -1
- package/src/bot/safety.ts +57 -3
- package/src/bot/session-state.ts +47 -7
- package/src/bot/slash-commands.ts +111 -3
- package/src/bot/steering-engine.ts +233 -0
- package/src/bot/step-executor.ts +66 -26
- package/src/bot/system-prompt.ts +5 -2
- package/src/bot/task-queue.ts +40 -4
- package/src/bot/tool-registry.ts +67 -5
- package/src/bot/trust-calculator.ts +87 -0
- package/src/bot/types.ts +104 -0
- package/src/bot/update-checker.ts +138 -0
- package/src/bot/weaver-tools.ts +10 -4
- package/src/cli-bridge.ts +4 -1
- package/src/cli-handlers.ts +150 -29
- package/src/handlers/on-execution-completed.ts +30 -0
- package/src/mcp-tools.ts +38 -0
- package/src/node-types/genesis-approve.ts +28 -3
- package/src/node-types/genesis-observe.ts +23 -12
- package/src/node-types/genesis-propose.ts +8 -0
- package/src/node-types/genesis-update-history.ts +12 -0
- package/src/ui/evolution-panel.tsx +96 -0
- package/src/ui/insights-widget.tsx +77 -0
|
@@ -49,8 +49,8 @@ export class ConversationStore {
|
|
|
49
49
|
fs.mkdirSync(this.baseDir, { recursive: true });
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
create(projectDir: string): ConversationRecord {
|
|
53
|
-
const id = crypto.randomUUID().slice(0,
|
|
52
|
+
async create(projectDir: string): Promise<ConversationRecord> {
|
|
53
|
+
const id = crypto.randomUUID().replace(/-/g, '').slice(0, 16);
|
|
54
54
|
const now = Date.now();
|
|
55
55
|
const record: ConversationRecord = {
|
|
56
56
|
id,
|
|
@@ -67,10 +67,12 @@ export class ConversationStore {
|
|
|
67
67
|
const convDir = path.join(this.baseDir, id);
|
|
68
68
|
fs.mkdirSync(convDir, { recursive: true });
|
|
69
69
|
|
|
70
|
-
// Add to index
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
// Add to index under file lock for concurrent safety
|
|
71
|
+
await withFileLock(this.indexPath, () => {
|
|
72
|
+
const index = this.readIndex();
|
|
73
|
+
index.unshift(record);
|
|
74
|
+
this.writeIndexAtomic(index);
|
|
75
|
+
});
|
|
74
76
|
|
|
75
77
|
return record;
|
|
76
78
|
}
|
|
@@ -89,11 +91,13 @@ export class ConversationStore {
|
|
|
89
91
|
return index.length > 0 ? index[0] : null;
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
delete(id: string): void {
|
|
93
|
-
// Remove from index
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
async delete(id: string): Promise<void> {
|
|
95
|
+
// Remove from index under file lock for concurrent safety
|
|
96
|
+
await withFileLock(this.indexPath, () => {
|
|
97
|
+
const index = this.readIndex();
|
|
98
|
+
const filtered = index.filter(c => c.id !== id);
|
|
99
|
+
this.writeIndexAtomic(filtered);
|
|
100
|
+
});
|
|
97
101
|
|
|
98
102
|
// Remove files
|
|
99
103
|
const convDir = path.join(this.baseDir, id);
|
|
@@ -108,6 +112,7 @@ export class ConversationStore {
|
|
|
108
112
|
|
|
109
113
|
const content = fs.readFileSync(msgPath, 'utf-8');
|
|
110
114
|
const messages: AgentMessage[] = [];
|
|
115
|
+
let corruptCount = 0;
|
|
111
116
|
|
|
112
117
|
for (const line of content.split('\n')) {
|
|
113
118
|
if (!line.trim()) continue;
|
|
@@ -121,33 +126,43 @@ export class ConversationStore {
|
|
|
121
126
|
if (stored.toolCallId) msg.toolCallId = stored.toolCallId;
|
|
122
127
|
messages.push(msg);
|
|
123
128
|
} catch {
|
|
124
|
-
|
|
129
|
+
corruptCount++;
|
|
125
130
|
}
|
|
126
131
|
}
|
|
127
132
|
|
|
133
|
+
if (corruptCount > 0) {
|
|
134
|
+
console.warn(` (Warning: skipped ${corruptCount} corrupt message(s) in conversation ${id})`);
|
|
135
|
+
}
|
|
136
|
+
|
|
128
137
|
return messages;
|
|
129
138
|
}
|
|
130
139
|
|
|
131
|
-
appendMessages(id: string, messages: AgentMessage[]):
|
|
132
|
-
if (messages.length === 0) return;
|
|
140
|
+
appendMessages(id: string, messages: AgentMessage[]): boolean {
|
|
141
|
+
if (messages.length === 0) return true;
|
|
133
142
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
try {
|
|
144
|
+
const convDir = path.join(this.baseDir, id);
|
|
145
|
+
fs.mkdirSync(convDir, { recursive: true });
|
|
146
|
+
const msgPath = path.join(convDir, 'messages.ndjson');
|
|
147
|
+
|
|
148
|
+
const lines = messages.map(m => {
|
|
149
|
+
const stored: StoredMessage = {
|
|
150
|
+
role: m.role,
|
|
151
|
+
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
152
|
+
timestamp: Date.now(),
|
|
153
|
+
tokens: Math.ceil((typeof m.content === 'string' ? m.content.length : JSON.stringify(m.content).length) / CHARS_PER_TOKEN),
|
|
154
|
+
};
|
|
155
|
+
if (m.toolCalls) stored.toolCalls = m.toolCalls;
|
|
156
|
+
if (m.toolCallId) stored.toolCallId = m.toolCallId;
|
|
157
|
+
return JSON.stringify(stored);
|
|
158
|
+
});
|
|
149
159
|
|
|
150
|
-
|
|
160
|
+
fs.appendFileSync(msgPath, lines.join('\n') + '\n');
|
|
161
|
+
return true;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
process.stderr.write(`[weaver] failed to persist ${messages.length} message(s) for conversation ${id}: ${err instanceof Error ? err.message : err}\n`);
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
151
166
|
}
|
|
152
167
|
|
|
153
168
|
async updateAfterTurn(id: string, newMessages: AgentMessage[], tokensUsed: number): Promise<void> {
|
|
@@ -167,12 +182,16 @@ export class ConversationStore {
|
|
|
167
182
|
index.unshift(record);
|
|
168
183
|
}
|
|
169
184
|
|
|
170
|
-
// Cap index size
|
|
185
|
+
// Cap index size — clean up evicted conversation directories
|
|
171
186
|
if (index.length > MAX_INDEX_SIZE) {
|
|
172
|
-
index.splice(MAX_INDEX_SIZE);
|
|
187
|
+
const evicted = index.splice(MAX_INDEX_SIZE);
|
|
188
|
+
for (const conv of evicted) {
|
|
189
|
+
const convDir = path.join(this.baseDir, conv.id);
|
|
190
|
+
try { fs.rmSync(convDir, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
191
|
+
}
|
|
173
192
|
}
|
|
174
193
|
|
|
175
|
-
this.
|
|
194
|
+
this.writeIndexAtomic(index);
|
|
176
195
|
});
|
|
177
196
|
}
|
|
178
197
|
|
|
@@ -214,7 +233,7 @@ export class ConversationStore {
|
|
|
214
233
|
const record = index.find(c => c.id === id);
|
|
215
234
|
if (record) {
|
|
216
235
|
record.title = title.slice(0, 80).replace(/\n/g, ' ').trim();
|
|
217
|
-
this.
|
|
236
|
+
this.writeIndexAtomic(index);
|
|
218
237
|
}
|
|
219
238
|
});
|
|
220
239
|
}
|
|
@@ -225,7 +244,7 @@ export class ConversationStore {
|
|
|
225
244
|
const record = index.find(c => c.id === id);
|
|
226
245
|
if (record && !record.botIds.includes(botId)) {
|
|
227
246
|
record.botIds.push(botId);
|
|
228
|
-
this.
|
|
247
|
+
this.writeIndexAtomic(index);
|
|
229
248
|
}
|
|
230
249
|
});
|
|
231
250
|
}
|
|
@@ -238,17 +257,43 @@ export class ConversationStore {
|
|
|
238
257
|
return JSON.parse(fs.readFileSync(this.indexPath, 'utf-8'));
|
|
239
258
|
} catch (err) {
|
|
240
259
|
if (process.env.WEAVER_VERBOSE) process.stderr.write(`[weaver] conversation index parse failed: ${err}\n`);
|
|
241
|
-
|
|
260
|
+
// Try backup
|
|
261
|
+
return this.readBackupIndex();
|
|
242
262
|
}
|
|
243
263
|
}
|
|
244
264
|
|
|
245
|
-
private
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
265
|
+
private readBackupIndex(): ConversationRecord[] {
|
|
266
|
+
const backupPath = this.indexPath + '.bak';
|
|
267
|
+
if (!fs.existsSync(backupPath)) return [];
|
|
268
|
+
try {
|
|
269
|
+
const data = JSON.parse(fs.readFileSync(backupPath, 'utf-8'));
|
|
270
|
+
// Restore backup to main index
|
|
271
|
+
try { this.writeIndexAtomic(data); } catch { /* best effort */ }
|
|
272
|
+
return data;
|
|
273
|
+
} catch (err) {
|
|
274
|
+
if (process.env.WEAVER_VERBOSE) process.stderr.write(`[weaver] conversation backup parse failed: ${err}\n`);
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
249
277
|
}
|
|
250
278
|
|
|
251
|
-
|
|
252
|
-
|
|
279
|
+
/** Atomic write: serialize to temp file, backup existing, rename into place. */
|
|
280
|
+
private writeIndexAtomic(index: ConversationRecord[]): void {
|
|
281
|
+
const tmpPath = this.indexPath + `.tmp.${process.pid}`;
|
|
282
|
+
const backupPath = this.indexPath + '.bak';
|
|
283
|
+
const content = JSON.stringify(index, null, 2);
|
|
284
|
+
|
|
285
|
+
// Write to temp file first
|
|
286
|
+
fs.writeFileSync(tmpPath, content);
|
|
287
|
+
|
|
288
|
+
// Backup current index if it exists
|
|
289
|
+
if (fs.existsSync(this.indexPath)) {
|
|
290
|
+
try { fs.copyFileSync(this.indexPath, backupPath); } catch { /* best effort */ }
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Atomic rename
|
|
294
|
+
fs.renameSync(tmpPath, this.indexPath);
|
|
295
|
+
|
|
296
|
+
// Always update backup after successful write
|
|
297
|
+
try { fs.copyFileSync(this.indexPath, backupPath); } catch { /* best effort */ }
|
|
253
298
|
}
|
|
254
299
|
}
|
package/src/bot/cost-store.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from 'node:path';
|
|
|
3
3
|
import * as os from 'node:os';
|
|
4
4
|
import type { CostRecord, CostSummary } from './types.js';
|
|
5
5
|
import { parseNdjson } from './safe-json.js';
|
|
6
|
+
import { withFileLock } from './file-lock.js';
|
|
6
7
|
|
|
7
8
|
const MAX_ENTRIES = 10_000;
|
|
8
9
|
const CAP_CHECK_INTERVAL = 100;
|
|
@@ -11,6 +12,7 @@ export class CostStore {
|
|
|
11
12
|
private readonly dir: string;
|
|
12
13
|
private readonly filePath: string;
|
|
13
14
|
private appendCount = 0;
|
|
15
|
+
private pendingCap: Promise<void> | null = null;
|
|
14
16
|
|
|
15
17
|
constructor(dir?: string) {
|
|
16
18
|
this.dir = dir ?? process.env.WEAVER_DATA_DIR ?? path.join(os.homedir(), '.weaver');
|
|
@@ -22,10 +24,15 @@ export class CostStore {
|
|
|
22
24
|
fs.appendFileSync(this.filePath, JSON.stringify(record) + '\n', 'utf-8');
|
|
23
25
|
this.appendCount++;
|
|
24
26
|
if (this.appendCount % CAP_CHECK_INTERVAL === 0) {
|
|
25
|
-
this.enforceCap();
|
|
27
|
+
this.pendingCap = this.enforceCap().catch(() => {}).finally(() => { this.pendingCap = null; });
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/** Wait for any in-flight cap enforcement to finish (used in tests). */
|
|
32
|
+
async waitForPendingCap(): Promise<void> {
|
|
33
|
+
if (this.pendingCap) await this.pendingCap;
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
query(filters?: { since?: number; model?: string }): CostRecord[] {
|
|
30
37
|
if (!fs.existsSync(this.filePath)) return [];
|
|
31
38
|
|
|
@@ -75,17 +82,21 @@ export class CostStore {
|
|
|
75
82
|
return summary;
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
private enforceCap(): void {
|
|
85
|
+
private async enforceCap(): Promise<void> {
|
|
79
86
|
if (!fs.existsSync(this.filePath)) return;
|
|
80
87
|
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
await withFileLock(this.filePath, () => {
|
|
89
|
+
if (!fs.existsSync(this.filePath)) return;
|
|
90
|
+
|
|
91
|
+
const content = fs.readFileSync(this.filePath, 'utf-8');
|
|
92
|
+
const lines = content.split('\n').filter((l) => l.trim().length > 0);
|
|
83
93
|
|
|
84
|
-
|
|
94
|
+
if (lines.length <= MAX_ENTRIES) return;
|
|
85
95
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
const kept = lines.slice(lines.length - MAX_ENTRIES);
|
|
97
|
+
const tmpPath = this.filePath + '.tmp';
|
|
98
|
+
fs.writeFileSync(tmpPath, kept.join('\n') + '\n', 'utf-8');
|
|
99
|
+
fs.renameSync(tmpPath, this.filePath);
|
|
100
|
+
});
|
|
90
101
|
}
|
|
91
102
|
}
|
package/src/bot/cost-tracker.ts
CHANGED
|
@@ -33,7 +33,19 @@ export class CostTracker {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
static estimateCost(model: string, usage: TokenUsage): number {
|
|
36
|
-
|
|
36
|
+
let pricing = MODEL_PRICING[model];
|
|
37
|
+
if (!pricing) {
|
|
38
|
+
// Fallback: prefix match for model variants with date suffixes
|
|
39
|
+
// e.g., 'claude-sonnet-4-6-20260301' matches 'claude-sonnet-4-6'
|
|
40
|
+
// Use longest matching prefix to avoid ambiguity.
|
|
41
|
+
let bestKey = '';
|
|
42
|
+
for (const key of Object.keys(MODEL_PRICING)) {
|
|
43
|
+
if (model.startsWith(key) && key.length > bestKey.length) {
|
|
44
|
+
bestKey = key;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (bestKey) pricing = MODEL_PRICING[bestKey];
|
|
48
|
+
}
|
|
37
49
|
if (!pricing) return 0;
|
|
38
50
|
|
|
39
51
|
return (
|
package/src/bot/cron-parser.ts
CHANGED
|
@@ -38,6 +38,7 @@ function parseField(token: string, min: number, max: number): CronField {
|
|
|
38
38
|
end = max;
|
|
39
39
|
} else if (rangeStr!.includes('-')) {
|
|
40
40
|
[start, end] = rangeStr!.split('-').map(Number) as [number, number];
|
|
41
|
+
if (isNaN(start) || isNaN(end)) throw new Error(`Invalid range in "${part}"`);
|
|
41
42
|
} else {
|
|
42
43
|
start = parseInt(rangeStr!, 10);
|
|
43
44
|
end = max;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Connection — Weaver-specific handlers for the core DeviceConnection.
|
|
3
|
+
*
|
|
4
|
+
* The transport (WebSocket, heartbeat, reconnect) lives in @synergenius/flow-weaver/agent.
|
|
5
|
+
* This module registers handlers for file operations, health, insights, improve status.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
|
|
12
|
+
// Re-export the core DeviceConnection for convenience
|
|
13
|
+
export { DeviceConnection } from '@synergenius/flow-weaver/agent';
|
|
14
|
+
export type { DeviceConnectionOptions, DeviceInfo, DeviceEvent } from '@synergenius/flow-weaver/agent';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register weaver-specific request handlers on a DeviceConnection.
|
|
18
|
+
*/
|
|
19
|
+
export function registerWeaverHandlers(
|
|
20
|
+
conn: import('@synergenius/flow-weaver/agent').DeviceConnection,
|
|
21
|
+
projectDir: string,
|
|
22
|
+
): void {
|
|
23
|
+
// Advertise weaver capabilities
|
|
24
|
+
conn.addCapability('file_read');
|
|
25
|
+
conn.addCapability('file_list');
|
|
26
|
+
conn.addCapability('health');
|
|
27
|
+
conn.addCapability('insights');
|
|
28
|
+
conn.addCapability('improve');
|
|
29
|
+
conn.addCapability('assistant');
|
|
30
|
+
|
|
31
|
+
// File read
|
|
32
|
+
conn.onRequest('file:read', async (_method, params) => {
|
|
33
|
+
const filePath = path.resolve(projectDir, String(params.path ?? ''));
|
|
34
|
+
if (!filePath.startsWith(projectDir)) throw new Error('Path outside project directory');
|
|
35
|
+
if (!fs.existsSync(filePath)) throw new Error('File not found');
|
|
36
|
+
const stat = fs.statSync(filePath);
|
|
37
|
+
if (stat.isDirectory()) {
|
|
38
|
+
return { type: 'directory', entries: fs.readdirSync(filePath) };
|
|
39
|
+
}
|
|
40
|
+
if (stat.size > 1_048_576) throw new Error('File too large (>1MB)');
|
|
41
|
+
return { type: 'file', content: fs.readFileSync(filePath, 'utf-8') };
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// File list
|
|
45
|
+
conn.onRequest('file:list', async (_method, params) => {
|
|
46
|
+
const dirPath = path.resolve(projectDir, String(params.path ?? '.'));
|
|
47
|
+
if (!dirPath.startsWith(projectDir)) throw new Error('Path outside project directory');
|
|
48
|
+
if (!fs.existsSync(dirPath)) throw new Error('Directory not found');
|
|
49
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
50
|
+
return entries
|
|
51
|
+
.filter(e => !e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== 'dist')
|
|
52
|
+
.map(e => ({
|
|
53
|
+
name: e.name,
|
|
54
|
+
type: e.isDirectory() ? 'directory' : 'file',
|
|
55
|
+
path: path.relative(projectDir, path.join(dirPath, e.name)),
|
|
56
|
+
}));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Project health
|
|
60
|
+
conn.onRequest('health', async () => {
|
|
61
|
+
try {
|
|
62
|
+
const { ProjectModelStore } = await import('./project-model.js');
|
|
63
|
+
const pms = new ProjectModelStore(projectDir);
|
|
64
|
+
const model = await pms.getOrBuild();
|
|
65
|
+
return { health: model.health, trust: model.trust, cost: model.cost, bots: model.bots };
|
|
66
|
+
} catch {
|
|
67
|
+
return { error: 'Project model not available' };
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Insights
|
|
72
|
+
conn.onRequest('insights', async () => {
|
|
73
|
+
try {
|
|
74
|
+
const { ProjectModelStore } = await import('./project-model.js');
|
|
75
|
+
const { InsightEngine } = await import('./insight-engine.js');
|
|
76
|
+
const model = await new ProjectModelStore(projectDir).getOrBuild();
|
|
77
|
+
return new InsightEngine().analyze(model);
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Improve status
|
|
84
|
+
conn.onRequest('improve:status', async () => {
|
|
85
|
+
try {
|
|
86
|
+
const summaryDir = path.join(os.homedir(), '.weaver', 'improve');
|
|
87
|
+
if (!fs.existsSync(summaryDir)) return { running: false, lastRun: null };
|
|
88
|
+
const files = fs.readdirSync(summaryDir).filter(f => f.endsWith('.json')).sort().reverse();
|
|
89
|
+
if (files.length === 0) return { running: false, lastRun: null };
|
|
90
|
+
const latest = JSON.parse(fs.readFileSync(path.join(summaryDir, files[0]!), 'utf-8'));
|
|
91
|
+
let running = false;
|
|
92
|
+
try {
|
|
93
|
+
const { execFileSync } = await import('node:child_process');
|
|
94
|
+
const worktrees = execFileSync('git', ['worktree', 'list'], { encoding: 'utf-8', cwd: projectDir });
|
|
95
|
+
running = worktrees.includes('weaver-improve');
|
|
96
|
+
} catch { /* git not available */ }
|
|
97
|
+
return { running, lastRun: latest };
|
|
98
|
+
} catch {
|
|
99
|
+
return { running: false, lastRun: null };
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -16,7 +16,12 @@ const PATTERNS: Array<{ pattern: RegExp; isTransient: boolean; guidance: string;
|
|
|
16
16
|
{ pattern: /EPIPE|ENOTFOUND/i, isTransient: true, guidance: 'Network error.', category: 'network' },
|
|
17
17
|
{ pattern: /401|authentication|invalid.*key/i, isTransient: false, guidance: 'Authentication failed. Check API key or run "weaver init".', category: 'auth' },
|
|
18
18
|
{ pattern: /403|forbidden/i, isTransient: false, guidance: 'Access denied. API key may lack permissions.', category: 'auth' },
|
|
19
|
+
{ pattern: /400|bad request/i, isTransient: false, guidance: 'Bad request (400). Check input payload or parameters.', category: 'parse' },
|
|
20
|
+
{ pattern: /404|not found/i, isTransient: false, guidance: 'Resource not found (404). Check URL or resource ID.', category: 'network' },
|
|
21
|
+
{ pattern: /408|request timeout/i, isTransient: true, guidance: 'Request timeout (408). Will auto-retry.', category: 'timeout' },
|
|
22
|
+
{ pattern: /422|unprocessable/i, isTransient: false, guidance: 'Unprocessable entity (422). Input is well-formed but semantically invalid.', category: 'parse' },
|
|
19
23
|
{ pattern: /429|rate.?limit|too many requests/i, isTransient: true, guidance: 'Rate limited. Wait a few minutes or reduce --parallel.', category: 'rate-limit' },
|
|
24
|
+
{ pattern: /500|internal server error/i, isTransient: true, guidance: 'Internal server error (500). Will auto-retry.', category: 'network' },
|
|
20
25
|
{ pattern: /502|bad gateway/i, isTransient: true, guidance: 'Server error (502). Will auto-retry.', category: 'network' },
|
|
21
26
|
{ pattern: /503|service unavailable|overloaded/i, isTransient: true, guidance: 'Service overloaded. Will auto-retry.', category: 'network' },
|
|
22
27
|
{ pattern: /504|gateway timeout/i, isTransient: true, guidance: 'Gateway timeout (504). Will auto-retry.', category: 'timeout' },
|
package/src/bot/file-lock.ts
CHANGED
|
@@ -58,8 +58,18 @@ export async function withFileLock<T>(
|
|
|
58
58
|
continue;
|
|
59
59
|
}
|
|
60
60
|
} catch {
|
|
61
|
-
// Can't read lock info
|
|
62
|
-
|
|
61
|
+
// Can't read lock info — lock holder may still be writing it.
|
|
62
|
+
// Only clean up if the lock directory itself is stale.
|
|
63
|
+
try {
|
|
64
|
+
const stat = fs.statSync(lockDir);
|
|
65
|
+
if (Date.now() - stat.mtimeMs > staleMs) {
|
|
66
|
+
try { fs.rmSync(lockDir, { recursive: true }); } catch { /* ignore */ }
|
|
67
|
+
}
|
|
68
|
+
} catch { /* lockDir gone, will retry */ }
|
|
69
|
+
|
|
70
|
+
if (attempt < retries) {
|
|
71
|
+
await new Promise(r => setTimeout(r, retryWait));
|
|
72
|
+
}
|
|
63
73
|
continue;
|
|
64
74
|
}
|
|
65
75
|
|
package/src/bot/file-watcher.ts
CHANGED
|
@@ -133,3 +133,64 @@ export async function getWorkflowDescription(filePath: string): Promise<string>
|
|
|
133
133
|
return '(workflow description unavailable)';
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Builds intelligence context from the project model for Genesis proposals.
|
|
139
|
+
* Includes health scores, insights, operation effectiveness, and user preferences.
|
|
140
|
+
*/
|
|
141
|
+
export async function getGenesisInsightContext(projectDir: string): Promise<string> {
|
|
142
|
+
try {
|
|
143
|
+
const { ProjectModelStore } = await import('./project-model.js');
|
|
144
|
+
const pms = new ProjectModelStore(projectDir);
|
|
145
|
+
const model = await pms.getOrBuild();
|
|
146
|
+
|
|
147
|
+
const lines: string[] = ['## Project Intelligence', ''];
|
|
148
|
+
lines.push(`Health: ${model.health.overall}/100`);
|
|
149
|
+
lines.push(`Trust Phase: ${model.trust.phase}`);
|
|
150
|
+
lines.push('');
|
|
151
|
+
|
|
152
|
+
// Top failure patterns as genesis candidates
|
|
153
|
+
const candidates = model.failurePatterns
|
|
154
|
+
.filter(f => !f.transient && f.occurrences >= 3)
|
|
155
|
+
.slice(0, 3);
|
|
156
|
+
if (candidates.length > 0) {
|
|
157
|
+
lines.push('### Top Issues (Genesis Candidates)');
|
|
158
|
+
for (const c of candidates) {
|
|
159
|
+
lines.push(`- [${c.occurrences}x] ${c.pattern} (${c.category})`);
|
|
160
|
+
}
|
|
161
|
+
lines.push('');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Operation effectiveness
|
|
165
|
+
const ops = Object.entries(model.evolution.byOperationType);
|
|
166
|
+
if (ops.length > 0) {
|
|
167
|
+
lines.push('### Operation Effectiveness');
|
|
168
|
+
for (const [op, stats] of ops) {
|
|
169
|
+
lines.push(`- ${op}: ${Math.round(stats.effectiveness * 100)}% effective (${stats.applied}/${stats.proposed})`);
|
|
170
|
+
}
|
|
171
|
+
lines.push('');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// User preferences
|
|
175
|
+
if (model.userPreferences.autoApprovePatterns.length > 0) {
|
|
176
|
+
lines.push(`Auto-approve patterns: ${model.userPreferences.autoApprovePatterns.join(', ')}`);
|
|
177
|
+
}
|
|
178
|
+
if (model.userPreferences.neverApprovePatterns.length > 0) {
|
|
179
|
+
lines.push(`Never-approve patterns: ${model.userPreferences.neverApprovePatterns.join(', ')}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Bot performance
|
|
183
|
+
const weakBots = model.bots.filter(b => b.successRate < 0.5 && b.totalTasksRun > 3);
|
|
184
|
+
if (weakBots.length > 0) {
|
|
185
|
+
lines.push('');
|
|
186
|
+
lines.push('### Underperforming Bots');
|
|
187
|
+
for (const b of weakBots) {
|
|
188
|
+
lines.push(`- ${b.name}: ${Math.round(b.successRate * 100)}% success (${b.totalTasksRun} tasks)`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return lines.join('\n');
|
|
193
|
+
} catch {
|
|
194
|
+
return '';
|
|
195
|
+
}
|
|
196
|
+
}
|
package/src/bot/genesis-store.ts
CHANGED
|
@@ -33,9 +33,13 @@ export class GenesisStore {
|
|
|
33
33
|
fs.writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2), 'utf-8');
|
|
34
34
|
return { ...DEFAULT_CONFIG };
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
try {
|
|
37
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
38
|
+
const raw = jsonParseOr(content, {} as Record<string, unknown>, 'genesis config');
|
|
39
|
+
return { ...DEFAULT_CONFIG, ...raw };
|
|
40
|
+
} catch {
|
|
41
|
+
return { ...DEFAULT_CONFIG };
|
|
42
|
+
}
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
saveConfig(config: GenesisConfig): void {
|
|
@@ -45,18 +49,26 @@ export class GenesisStore {
|
|
|
45
49
|
|
|
46
50
|
loadHistory(): GenesisHistory {
|
|
47
51
|
const historyPath = path.join(this.genesisDir, 'history.json');
|
|
52
|
+
const fallback: GenesisHistory = { configHash: '', cycles: [] };
|
|
48
53
|
if (!fs.existsSync(historyPath)) {
|
|
49
|
-
return
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const content = fs.readFileSync(historyPath, 'utf-8');
|
|
58
|
+
const parsed = jsonParseOr<GenesisHistory>(content, null as unknown as GenesisHistory, 'genesis history');
|
|
59
|
+
if (parsed && Array.isArray(parsed.cycles)) return parsed;
|
|
60
|
+
// Primary file corrupt — try backup
|
|
61
|
+
return this.readJsonBackup<GenesisHistory>(historyPath, fallback);
|
|
62
|
+
} catch {
|
|
63
|
+
return this.readJsonBackup<GenesisHistory>(historyPath, fallback);
|
|
50
64
|
}
|
|
51
|
-
const content = fs.readFileSync(historyPath, 'utf-8');
|
|
52
|
-
return jsonParseOr<GenesisHistory>(content, { configHash: '', cycles: [] }, 'genesis history');
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
appendCycle(cycle: GenesisCycleRecord): void {
|
|
56
68
|
const history = this.loadHistory();
|
|
57
69
|
history.cycles.push(cycle);
|
|
58
70
|
this.ensureDirs();
|
|
59
|
-
|
|
71
|
+
this.writeJsonAtomic(path.join(this.genesisDir, 'history.json'), history);
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
saveSnapshot(cycleId: string, content: string): string {
|
|
@@ -82,8 +94,12 @@ export class GenesisStore {
|
|
|
82
94
|
getLastFingerprint(): GenesisFingerprint | null {
|
|
83
95
|
const fpPath = path.join(this.genesisDir, 'fingerprint.json');
|
|
84
96
|
if (!fs.existsSync(fpPath)) return null;
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
try {
|
|
98
|
+
const content = fs.readFileSync(fpPath, 'utf-8');
|
|
99
|
+
return jsonParseOr<GenesisFingerprint | null>(content, null, 'genesis fingerprint');
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
87
103
|
}
|
|
88
104
|
|
|
89
105
|
getRecentOutcomes(count: number): string[] {
|
|
@@ -142,19 +158,21 @@ export class GenesisStore {
|
|
|
142
158
|
loadSelfHistory(): GenesisSelfMigrationRecord[] {
|
|
143
159
|
const histPath = path.join(this.genesisDir, 'self-history.json');
|
|
144
160
|
if (!fs.existsSync(histPath)) return [];
|
|
145
|
-
|
|
146
|
-
|
|
161
|
+
try {
|
|
162
|
+
const content = fs.readFileSync(histPath, 'utf-8');
|
|
163
|
+
const parsed = jsonParseOr<GenesisSelfMigrationRecord[]>(content, null as unknown as GenesisSelfMigrationRecord[], 'genesis self-history');
|
|
164
|
+
if (Array.isArray(parsed)) return parsed;
|
|
165
|
+
return this.readJsonBackup<GenesisSelfMigrationRecord[]>(histPath, []);
|
|
166
|
+
} catch {
|
|
167
|
+
return this.readJsonBackup<GenesisSelfMigrationRecord[]>(histPath, []);
|
|
168
|
+
}
|
|
147
169
|
}
|
|
148
170
|
|
|
149
171
|
appendSelfMigration(record: GenesisSelfMigrationRecord): void {
|
|
150
172
|
const records = this.loadSelfHistory();
|
|
151
173
|
records.push(record);
|
|
152
174
|
this.ensureDirs();
|
|
153
|
-
|
|
154
|
-
path.join(this.genesisDir, 'self-history.json'),
|
|
155
|
-
JSON.stringify(records, null, 2),
|
|
156
|
-
'utf-8',
|
|
157
|
-
);
|
|
175
|
+
this.writeJsonAtomic(path.join(this.genesisDir, 'self-history.json'), records);
|
|
158
176
|
}
|
|
159
177
|
|
|
160
178
|
getSelfFailureCount(): number {
|
|
@@ -167,6 +185,40 @@ export class GenesisStore {
|
|
|
167
185
|
return count;
|
|
168
186
|
}
|
|
169
187
|
|
|
188
|
+
/** Atomic write: serialize to temp file, backup existing, rename into place. */
|
|
189
|
+
private writeJsonAtomic(filePath: string, data: unknown): void {
|
|
190
|
+
const tmpPath = filePath + `.tmp.${process.pid}`;
|
|
191
|
+
const backupPath = filePath + '.bak';
|
|
192
|
+
const content = JSON.stringify(data, null, 2);
|
|
193
|
+
|
|
194
|
+
fs.writeFileSync(tmpPath, content, 'utf-8');
|
|
195
|
+
|
|
196
|
+
// Backup current file before overwriting
|
|
197
|
+
if (fs.existsSync(filePath)) {
|
|
198
|
+
try { fs.copyFileSync(filePath, backupPath); } catch { /* best effort */ }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
fs.renameSync(tmpPath, filePath);
|
|
202
|
+
|
|
203
|
+
// Update backup after successful write
|
|
204
|
+
try { fs.copyFileSync(filePath, backupPath); } catch { /* best effort */ }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Read from .bak file when primary is corrupt. */
|
|
208
|
+
private readJsonBackup<T>(filePath: string, fallback: T): T {
|
|
209
|
+
const backupPath = filePath + '.bak';
|
|
210
|
+
if (!fs.existsSync(backupPath)) return fallback;
|
|
211
|
+
try {
|
|
212
|
+
const content = fs.readFileSync(backupPath, 'utf-8');
|
|
213
|
+
const data = JSON.parse(content) as T;
|
|
214
|
+
// Restore backup to primary
|
|
215
|
+
try { this.writeJsonAtomic(filePath, data); } catch { /* best effort */ }
|
|
216
|
+
return data;
|
|
217
|
+
} catch {
|
|
218
|
+
return fallback;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
170
222
|
static hashFile(filePath: string): string {
|
|
171
223
|
const content = fs.readFileSync(filePath);
|
|
172
224
|
return crypto.createHash('sha256').update(content).digest('hex');
|