@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
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Model Store — aggregation layer that pulls from all existing data stores
|
|
3
|
+
* and computes a unified project model ("the brain").
|
|
4
|
+
*
|
|
5
|
+
* Storage: ~/.weaver/projects/{hash8}/model.json (cached with 5-minute TTL)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import * as crypto from 'node:crypto';
|
|
12
|
+
import type {
|
|
13
|
+
ProjectModel, WorkflowHealth, FailurePattern, BotProfile,
|
|
14
|
+
ApprovalDecision, OperationEffectiveness,
|
|
15
|
+
GenesisCycleRecord,
|
|
16
|
+
} from './types.js';
|
|
17
|
+
import { computeTrustLevel } from './trust-calculator.js';
|
|
18
|
+
|
|
19
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
20
|
+
const BOTS_DIR = path.join(os.homedir(), '.weaver', 'bots');
|
|
21
|
+
|
|
22
|
+
export class ProjectModelStore {
|
|
23
|
+
private readonly hash8: string;
|
|
24
|
+
private readonly modelPath: string;
|
|
25
|
+
|
|
26
|
+
constructor(private readonly projectDir: string) {
|
|
27
|
+
this.hash8 = crypto.createHash('sha256').update(projectDir).digest('hex').slice(0, 8);
|
|
28
|
+
const modelDir = path.join(os.homedir(), '.weaver', 'projects', this.hash8);
|
|
29
|
+
fs.mkdirSync(modelDir, { recursive: true });
|
|
30
|
+
this.modelPath = path.join(modelDir, 'model.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getOrBuild(): Promise<ProjectModel> {
|
|
34
|
+
try {
|
|
35
|
+
if (fs.existsSync(this.modelPath)) {
|
|
36
|
+
const raw = fs.readFileSync(this.modelPath, 'utf-8');
|
|
37
|
+
const cached = JSON.parse(raw) as ProjectModel;
|
|
38
|
+
if (Date.now() - cached.builtAt < CACHE_TTL_MS) {
|
|
39
|
+
return cached;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch { /* cache corrupt — rebuild */ }
|
|
43
|
+
|
|
44
|
+
return this.build();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async build(): Promise<ProjectModel> {
|
|
48
|
+
// 1. Load stores with dynamic imports (avoid circular deps)
|
|
49
|
+
let runs: Array<{ workflowFile: string; startedAt: string; finishedAt: string; durationMs: number; success: boolean; outcome: string; summary: string }> = [];
|
|
50
|
+
try {
|
|
51
|
+
const { RunStore } = await import('./run-store.js');
|
|
52
|
+
const allRuns = new RunStore().list({ limit: 10_000 });
|
|
53
|
+
// Filter to runs within this project directory, excluding node_modules
|
|
54
|
+
runs = allRuns.filter(r =>
|
|
55
|
+
r.workflowFile.startsWith(this.projectDir) &&
|
|
56
|
+
!r.workflowFile.includes('/node_modules/')
|
|
57
|
+
);
|
|
58
|
+
} catch { /* unavailable */ }
|
|
59
|
+
|
|
60
|
+
let auditEvents: Array<{ type: string; timestamp: string; runId: string; data?: Record<string, unknown> }> = [];
|
|
61
|
+
try {
|
|
62
|
+
const { AuditStore } = await import('./audit-store.js');
|
|
63
|
+
auditEvents = new AuditStore().queryRecent(500);
|
|
64
|
+
} catch { /* unavailable */ }
|
|
65
|
+
|
|
66
|
+
let costRecords: Array<{ runId?: string; model?: string; promptTokens?: number; completionTokens?: number; estimatedCost?: number; cost?: number; timestamp: number }> = [];
|
|
67
|
+
try {
|
|
68
|
+
const { CostStore } = await import('./cost-store.js');
|
|
69
|
+
costRecords = new CostStore().query();
|
|
70
|
+
} catch { /* unavailable */ }
|
|
71
|
+
|
|
72
|
+
let genesisCycles: GenesisCycleRecord[] = [];
|
|
73
|
+
try {
|
|
74
|
+
if (fs.existsSync(path.join(this.projectDir, '.genesis'))) {
|
|
75
|
+
const { GenesisStore } = await import('./genesis-store.js');
|
|
76
|
+
genesisCycles = new GenesisStore(this.projectDir).loadHistory().cycles;
|
|
77
|
+
}
|
|
78
|
+
} catch { /* unavailable */ }
|
|
79
|
+
|
|
80
|
+
let conversationCount = 0;
|
|
81
|
+
try {
|
|
82
|
+
const { ConversationStore } = await import('./conversation-store.js');
|
|
83
|
+
conversationCount = new ConversationStore().list().length;
|
|
84
|
+
} catch { /* unavailable */ }
|
|
85
|
+
|
|
86
|
+
// 2. Build sections
|
|
87
|
+
const workflows = this.buildWorkflowHealth(runs);
|
|
88
|
+
const overall = workflows.length > 0
|
|
89
|
+
? Math.round(workflows.reduce((s, w) => s + w.score * w.totalRuns, 0) / Math.max(1, workflows.reduce((s, w) => s + w.totalRuns, 0)))
|
|
90
|
+
: 0;
|
|
91
|
+
|
|
92
|
+
const bots = this.buildBotProfiles(runs);
|
|
93
|
+
|
|
94
|
+
let classifyError: ((msg: string) => { isTransient: boolean; category: string }) | null = null;
|
|
95
|
+
try {
|
|
96
|
+
const mod = await import('./error-classifier.js');
|
|
97
|
+
classifyError = (msg: string) => mod.classifyError(msg);
|
|
98
|
+
} catch { /* unavailable */ }
|
|
99
|
+
|
|
100
|
+
const failurePatterns = this.buildFailurePatterns(auditEvents, classifyError);
|
|
101
|
+
const userPreferences = this.buildUserPreferences(genesisCycles);
|
|
102
|
+
const evolution = this.buildEvolution(genesisCycles);
|
|
103
|
+
const cost = this.buildCost(costRecords, runs);
|
|
104
|
+
|
|
105
|
+
// 3. Assemble partial model, compute trust
|
|
106
|
+
const partial = {
|
|
107
|
+
projectDir: this.projectDir,
|
|
108
|
+
builtAt: Date.now(),
|
|
109
|
+
health: { overall, workflows },
|
|
110
|
+
bots,
|
|
111
|
+
failurePatterns,
|
|
112
|
+
userPreferences,
|
|
113
|
+
evolution,
|
|
114
|
+
cost,
|
|
115
|
+
_conversationCount: conversationCount,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const trust = computeTrustLevel(partial);
|
|
119
|
+
const model: ProjectModel = {
|
|
120
|
+
projectDir: partial.projectDir,
|
|
121
|
+
builtAt: partial.builtAt,
|
|
122
|
+
health: partial.health,
|
|
123
|
+
bots: partial.bots,
|
|
124
|
+
failurePatterns: partial.failurePatterns,
|
|
125
|
+
userPreferences: partial.userPreferences,
|
|
126
|
+
evolution: partial.evolution,
|
|
127
|
+
cost: partial.cost,
|
|
128
|
+
trust,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// 4. Cache
|
|
132
|
+
try {
|
|
133
|
+
fs.writeFileSync(this.modelPath, JSON.stringify(model, null, 2), 'utf-8');
|
|
134
|
+
} catch { /* non-fatal */ }
|
|
135
|
+
|
|
136
|
+
return model;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
invalidate(): void {
|
|
140
|
+
try {
|
|
141
|
+
if (fs.existsSync(this.modelPath)) fs.unlinkSync(this.modelPath);
|
|
142
|
+
} catch { /* non-fatal */ }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
formatSummary(model: ProjectModel): string {
|
|
146
|
+
const { health, bots, failurePatterns, cost, trust, evolution } = model;
|
|
147
|
+
const healthyCount = health.workflows.filter(w => w.trend !== 'degrading').length;
|
|
148
|
+
const degradingCount = health.workflows.filter(w => w.trend === 'degrading').length;
|
|
149
|
+
const ejectedCount = bots.filter(b => b.ejected).length;
|
|
150
|
+
const criticalPatterns = failurePatterns.filter(f => f.occurrences >= 5 && !f.transient).length;
|
|
151
|
+
|
|
152
|
+
const phaseNames: Record<number, string> = {
|
|
153
|
+
1: 'insights + suggestions',
|
|
154
|
+
2: 'proposals with explanation',
|
|
155
|
+
3: 'proposals with visual diff',
|
|
156
|
+
4: 'auto-apply COSMETIC',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return [
|
|
160
|
+
`Health: ${health.overall}/100 (${health.workflows.length} workflows, ${healthyCount} healthy, ${degradingCount} degrading)`,
|
|
161
|
+
`Bots: ${bots.length} registered (${ejectedCount} ejected)`,
|
|
162
|
+
`Failures: ${failurePatterns.length} recurring patterns (${criticalPatterns} critical)`,
|
|
163
|
+
`Cost: $${cost.last7Days.toFixed(2)} last 7 days (${cost.trend})`,
|
|
164
|
+
`Trust: Phase ${trust.phase} (${phaseNames[trust.phase]})`,
|
|
165
|
+
`Evolution: ${evolution.totalCycles} cycles (${evolution.totalCycles > 0 ? Math.round(evolution.successRate * 100) : 0}% success)`,
|
|
166
|
+
].join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
formatSessionGreeting(model: ProjectModel): string {
|
|
170
|
+
const insightCount = model.failurePatterns.filter(f => f.occurrences >= 3).length
|
|
171
|
+
+ model.health.workflows.filter(w => w.trend === 'degrading').length;
|
|
172
|
+
return `Health ${model.health.overall}/100 \u00b7 ${model.bots.length} bots \u00b7 ${insightCount} insights \u00b7 $${model.cost.last7Days.toFixed(2)}/7d`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---- Private builders ----
|
|
176
|
+
|
|
177
|
+
private buildWorkflowHealth(runs: typeof ProjectModelStore.prototype.build extends (...args: any[]) => any ? any : never): WorkflowHealth[] {
|
|
178
|
+
const byWorkflow = new Map<string, Array<{ startedAt: string; durationMs: number; success: boolean }>>();
|
|
179
|
+
for (const run of runs) {
|
|
180
|
+
if (!byWorkflow.has(run.workflowFile)) byWorkflow.set(run.workflowFile, []);
|
|
181
|
+
byWorkflow.get(run.workflowFile)!.push(run);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
const d7 = now - 7 * 86_400_000;
|
|
186
|
+
const d14 = now - 14 * 86_400_000;
|
|
187
|
+
const result: WorkflowHealth[] = [];
|
|
188
|
+
|
|
189
|
+
for (const [absFile, wfRuns] of byWorkflow) {
|
|
190
|
+
// Convert to relative path for display
|
|
191
|
+
const file = absFile.startsWith(this.projectDir)
|
|
192
|
+
? path.relative(this.projectDir, absFile)
|
|
193
|
+
: absFile;
|
|
194
|
+
|
|
195
|
+
const totalRuns = wfRuns.length;
|
|
196
|
+
const successRate = totalRuns > 0 ? wfRuns.filter(r => r.success).length / totalRuns : 0;
|
|
197
|
+
const avgDurationMs = totalRuns > 0 ? Math.round(wfRuns.reduce((s, r) => s + (r.durationMs ?? 0), 0) / totalRuns) : 0;
|
|
198
|
+
const sorted = [...wfRuns].sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
199
|
+
const lastRun = sorted[0]?.startedAt ?? null;
|
|
200
|
+
|
|
201
|
+
const recent = wfRuns.filter(r => new Date(r.startedAt).getTime() >= d7);
|
|
202
|
+
const prev = wfRuns.filter(r => { const t = new Date(r.startedAt).getTime(); return t >= d14 && t < d7; });
|
|
203
|
+
const recentRate = recent.length > 0 ? recent.filter(r => r.success).length / recent.length : successRate;
|
|
204
|
+
const prevRate = prev.length > 0 ? prev.filter(r => r.success).length / prev.length : successRate;
|
|
205
|
+
const diff = recentRate - prevRate;
|
|
206
|
+
const trend: WorkflowHealth['trend'] = diff > 0.05 ? 'improving' : diff < -0.05 ? 'degrading' : 'stable';
|
|
207
|
+
|
|
208
|
+
result.push({ file, score: Math.round(successRate * 100), totalRuns, successRate, avgDurationMs, lastRun, trend });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return result;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private buildBotProfiles(runs: Array<{ workflowFile: string; success: boolean; durationMs: number }>): BotProfile[] {
|
|
215
|
+
const profiles: BotProfile[] = [];
|
|
216
|
+
try {
|
|
217
|
+
if (!fs.existsSync(BOTS_DIR)) return profiles;
|
|
218
|
+
for (const name of fs.readdirSync(BOTS_DIR)) {
|
|
219
|
+
const metaPath = path.join(BOTS_DIR, name, 'meta.json');
|
|
220
|
+
if (!fs.existsSync(metaPath)) continue;
|
|
221
|
+
try {
|
|
222
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
223
|
+
if (meta.projectDir !== this.projectDir) continue;
|
|
224
|
+
|
|
225
|
+
const ejectedPath = path.join(this.projectDir, '.fw', 'bots', name);
|
|
226
|
+
const ejected = fs.existsSync(ejectedPath);
|
|
227
|
+
|
|
228
|
+
// Compute bot task stats from runs associated with this bot
|
|
229
|
+
const botRuns = runs.filter(r => r.workflowFile.includes(name) || r.workflowFile.includes('weaver-bot'));
|
|
230
|
+
const totalTasksRun = botRuns.length;
|
|
231
|
+
const successRate = totalTasksRun > 0 ? botRuns.filter(r => r.success).length / totalTasksRun : 0;
|
|
232
|
+
const avgTaskDurationMs = totalTasksRun > 0 ? Math.round(botRuns.reduce((s, r) => s + (r.durationMs ?? 0), 0) / totalTasksRun) : 0;
|
|
233
|
+
|
|
234
|
+
profiles.push({
|
|
235
|
+
name: meta.name ?? name,
|
|
236
|
+
workflowFile: ejected ? path.join(ejectedPath, 'weaver-bot.ts') : 'node_modules/@synergenius/flow-weaver-pack-weaver/src/workflows/weaver-bot.ts',
|
|
237
|
+
ejected,
|
|
238
|
+
totalTasksRun,
|
|
239
|
+
successRate,
|
|
240
|
+
avgTaskDurationMs,
|
|
241
|
+
topFailurePatterns: [],
|
|
242
|
+
});
|
|
243
|
+
} catch { /* corrupt meta */ }
|
|
244
|
+
}
|
|
245
|
+
} catch { /* bots dir read failed */ }
|
|
246
|
+
return profiles;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private buildFailurePatterns(
|
|
250
|
+
auditEvents: Array<{ type: string; timestamp: string; data?: Record<string, unknown> }>,
|
|
251
|
+
classifyError: ((msg: string) => { isTransient: boolean; category: string }) | null,
|
|
252
|
+
): FailurePattern[] {
|
|
253
|
+
const errorEvents = auditEvents.filter(e =>
|
|
254
|
+
e.type.includes('error') || e.type.includes('fail') || e.data?.error !== undefined
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const groups = new Map<string, { message: string; count: number; lastSeen: string; workflows: Set<string> }>();
|
|
258
|
+
for (const event of errorEvents) {
|
|
259
|
+
const rawMsg = String(event.data?.error ?? event.data?.message ?? event.type);
|
|
260
|
+
const key = rawMsg.slice(0, 50);
|
|
261
|
+
const existing = groups.get(key);
|
|
262
|
+
const wf = String(event.data?.workflowFile ?? event.data?.file ?? '');
|
|
263
|
+
if (existing) {
|
|
264
|
+
existing.count++;
|
|
265
|
+
if (event.timestamp > existing.lastSeen) existing.lastSeen = event.timestamp;
|
|
266
|
+
if (wf) existing.workflows.add(wf);
|
|
267
|
+
} else {
|
|
268
|
+
const wfSet = new Set<string>();
|
|
269
|
+
if (wf) wfSet.add(wf);
|
|
270
|
+
groups.set(key, { message: rawMsg, count: 1, lastSeen: event.timestamp, workflows: wfSet });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const patterns: FailurePattern[] = [];
|
|
275
|
+
for (const [, g] of groups) {
|
|
276
|
+
let transient = false;
|
|
277
|
+
let category = 'unknown';
|
|
278
|
+
if (classifyError) {
|
|
279
|
+
try {
|
|
280
|
+
const c = classifyError(g.message);
|
|
281
|
+
transient = c.isTransient;
|
|
282
|
+
category = c.category;
|
|
283
|
+
} catch { /* classification failed */ }
|
|
284
|
+
}
|
|
285
|
+
patterns.push({
|
|
286
|
+
pattern: g.message.slice(0, 120),
|
|
287
|
+
category,
|
|
288
|
+
occurrences: g.count,
|
|
289
|
+
lastSeen: g.lastSeen,
|
|
290
|
+
workflows: [...g.workflows],
|
|
291
|
+
transient,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return patterns.sort((a, b) => b.occurrences - a.occurrences);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private buildUserPreferences(genesisCycles: GenesisCycleRecord[]): ProjectModel['userPreferences'] {
|
|
299
|
+
const approvalHistory: ApprovalDecision[] = [];
|
|
300
|
+
|
|
301
|
+
for (const cycle of genesisCycles) {
|
|
302
|
+
if (cycle.approved === null) continue;
|
|
303
|
+
approvalHistory.push({
|
|
304
|
+
timestamp: cycle.timestamp,
|
|
305
|
+
proposalSummary: cycle.proposal?.summary ?? '',
|
|
306
|
+
impactLevel: cycle.proposal?.impactLevel ?? 'MINOR',
|
|
307
|
+
approved: cycle.approved,
|
|
308
|
+
reason: cycle.rejectionReason,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Auto-approve: 5+ consecutive approvals for a given impact level
|
|
313
|
+
const autoApprovePatterns: string[] = [];
|
|
314
|
+
const neverApprovePatterns: string[] = [];
|
|
315
|
+
const byImpact = new Map<string, boolean[]>();
|
|
316
|
+
for (const d of approvalHistory) {
|
|
317
|
+
if (!byImpact.has(d.impactLevel)) byImpact.set(d.impactLevel, []);
|
|
318
|
+
byImpact.get(d.impactLevel)!.push(d.approved);
|
|
319
|
+
}
|
|
320
|
+
for (const [level, decisions] of byImpact) {
|
|
321
|
+
let consec = 0;
|
|
322
|
+
for (let i = decisions.length - 1; i >= 0; i--) {
|
|
323
|
+
if (decisions[i]) consec++;
|
|
324
|
+
else break;
|
|
325
|
+
}
|
|
326
|
+
if (consec >= 5) autoApprovePatterns.push(level);
|
|
327
|
+
|
|
328
|
+
let consecRejects = 0;
|
|
329
|
+
for (let i = decisions.length - 1; i >= 0; i--) {
|
|
330
|
+
if (!decisions[i]) consecRejects++;
|
|
331
|
+
else break;
|
|
332
|
+
}
|
|
333
|
+
if (consecRejects >= 3) neverApprovePatterns.push(level);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return { approvalHistory, autoApprovePatterns, neverApprovePatterns };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private buildEvolution(genesisCycles: GenesisCycleRecord[]): ProjectModel['evolution'] {
|
|
340
|
+
const totalCycles = genesisCycles.length;
|
|
341
|
+
const applied = genesisCycles.filter(c => c.outcome === 'applied').length;
|
|
342
|
+
const successRate = totalCycles > 0 ? applied / totalCycles : 0;
|
|
343
|
+
|
|
344
|
+
const byOp = new Map<string, { proposed: number; applied: number; rolledBack: number }>();
|
|
345
|
+
for (const cycle of genesisCycles) {
|
|
346
|
+
if (!cycle.proposal) continue;
|
|
347
|
+
for (const op of cycle.proposal.operations) {
|
|
348
|
+
if (!byOp.has(op.type)) byOp.set(op.type, { proposed: 0, applied: 0, rolledBack: 0 });
|
|
349
|
+
const e = byOp.get(op.type)!;
|
|
350
|
+
e.proposed++;
|
|
351
|
+
if (cycle.outcome === 'applied') e.applied++;
|
|
352
|
+
else if (cycle.outcome === 'rolled-back') e.rolledBack++;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const byOperationType: Record<string, OperationEffectiveness> = {};
|
|
357
|
+
for (const [type, stats] of byOp) {
|
|
358
|
+
byOperationType[type] = {
|
|
359
|
+
proposed: stats.proposed,
|
|
360
|
+
applied: stats.applied,
|
|
361
|
+
rolledBack: stats.rolledBack,
|
|
362
|
+
effectiveness: stats.proposed > 0 ? stats.applied / stats.proposed : 0,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
totalCycles,
|
|
368
|
+
successRate,
|
|
369
|
+
byOperationType,
|
|
370
|
+
recentCycles: genesisCycles.slice(-10),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private buildCost(
|
|
375
|
+
costRecords: Array<{ estimatedCost?: number; cost?: number; timestamp: number; model?: string }>,
|
|
376
|
+
runs: Array<{ success: boolean }>,
|
|
377
|
+
): ProjectModel['cost'] {
|
|
378
|
+
const now = Date.now();
|
|
379
|
+
const d7 = now - 7 * 86_400_000;
|
|
380
|
+
const d14 = now - 14 * 86_400_000;
|
|
381
|
+
const d30 = now - 30 * 86_400_000;
|
|
382
|
+
|
|
383
|
+
const getCost = (r: { estimatedCost?: number; cost?: number }) => r.estimatedCost ?? r.cost ?? 0;
|
|
384
|
+
const totalSpent = costRecords.reduce((s, r) => s + getCost(r), 0);
|
|
385
|
+
const last7Days = costRecords.filter(r => r.timestamp >= d7).reduce((s, r) => s + getCost(r), 0);
|
|
386
|
+
const prev7Days = costRecords.filter(r => r.timestamp >= d14 && r.timestamp < d7).reduce((s, r) => s + getCost(r), 0);
|
|
387
|
+
const last30Days = costRecords.filter(r => r.timestamp >= d30).reduce((s, r) => s + getCost(r), 0);
|
|
388
|
+
|
|
389
|
+
let trend: ProjectModel['cost']['trend'] = 'stable';
|
|
390
|
+
if (prev7Days > 0) {
|
|
391
|
+
const ratio = last7Days / prev7Days;
|
|
392
|
+
if (ratio > 1.15) trend = 'increasing';
|
|
393
|
+
else if (ratio < 0.85) trend = 'decreasing';
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const successfulRuns = runs.filter(r => r.success).length;
|
|
397
|
+
const costPerSuccessfulRun = successfulRuns > 0 ? totalSpent / successfulRuns : 0;
|
|
398
|
+
|
|
399
|
+
// High cost workflows — from cost records grouped by model (simplified)
|
|
400
|
+
const highCostWorkflows: Array<{ workflow: string; avgCost: number }> = [];
|
|
401
|
+
|
|
402
|
+
return { totalSpent, last7Days, last30Days, trend, costPerSuccessfulRun, highCostWorkflows };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -36,7 +36,6 @@ export function formatResponse(text: string, cwd: string): string {
|
|
|
36
36
|
|
|
37
37
|
function supportsHyperlinks(): boolean {
|
|
38
38
|
const term = process.env.TERM_PROGRAM ?? '';
|
|
39
|
-
// Known terminals that support OSC 8
|
|
40
|
-
return ['iTerm.app', 'WezTerm', 'vscode', 'Hyper'].includes(term)
|
|
41
|
-
|| !!process.env.TERM_PROGRAM_VERSION; // Most modern terminals
|
|
39
|
+
// Known terminals that support OSC 8 hyperlinks
|
|
40
|
+
return ['iTerm.app', 'WezTerm', 'vscode', 'Hyper'].includes(term);
|
|
42
41
|
}
|
package/src/bot/run-store.ts
CHANGED
|
@@ -110,9 +110,12 @@ export class RunStore {
|
|
|
110
110
|
});
|
|
111
111
|
fs.unlinkSync(path.join(this.dir, file));
|
|
112
112
|
}
|
|
113
|
-
} catch {
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.warn(`[weaver] checkOrphans: removing corrupt marker file ${file}:`, err);
|
|
115
|
+
try { fs.unlinkSync(path.join(this.dir, file)); } catch { /* already gone */ }
|
|
116
|
+
}
|
|
114
117
|
}
|
|
115
|
-
} catch {
|
|
118
|
+
} catch (err) { console.warn(`[weaver] checkOrphans: failed to read directory ${this.dir}:`, err); }
|
|
116
119
|
return orphans;
|
|
117
120
|
}
|
|
118
121
|
|
package/src/bot/safe-path.ts
CHANGED
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
* user input, AI output, or external configuration.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import * as fs from 'node:fs';
|
|
9
10
|
import * as path from 'node:path';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Validate that a relative path does not escape the base directory.
|
|
13
14
|
* Returns the resolved absolute path if safe, or null if the path
|
|
14
|
-
* attempts traversal.
|
|
15
|
+
* attempts traversal. Also resolves symlinks to prevent bypass attacks.
|
|
15
16
|
*/
|
|
16
17
|
export function safePath(baseDir: string, relativePath: string): string | null {
|
|
17
18
|
const normalized = path.normalize(relativePath);
|
|
@@ -28,6 +29,24 @@ export function safePath(baseDir: string, relativePath: string): string | null {
|
|
|
28
29
|
return null;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
// Symlink protection: walk up to find deepest existing segment and
|
|
33
|
+
// verify its real path stays within the base directory.
|
|
34
|
+
let checkPath = resolved;
|
|
35
|
+
while (!fs.existsSync(checkPath) && checkPath !== resolvedBase) {
|
|
36
|
+
checkPath = path.dirname(checkPath);
|
|
37
|
+
}
|
|
38
|
+
if (fs.existsSync(checkPath)) {
|
|
39
|
+
try {
|
|
40
|
+
const realPath = fs.realpathSync(checkPath);
|
|
41
|
+
const realBase = fs.realpathSync(resolvedBase);
|
|
42
|
+
if (!realPath.startsWith(realBase + path.sep) && realPath !== realBase) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
return null; // Can't verify safety, reject
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
31
50
|
return resolved;
|
|
32
51
|
}
|
|
33
52
|
|
package/src/bot/safety.ts
CHANGED
|
@@ -1,14 +1,68 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared safety constants and checks — single source of truth.
|
|
3
|
+
*
|
|
4
|
+
* Both step-executor.ts and assistant-tools.ts MUST use these patterns.
|
|
5
|
+
* Do not duplicate blocklists elsewhere.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
/** Shell commands that are NEVER allowed (destructive or evasive operations). */
|
|
9
|
+
export const BLOCKED_SHELL_PATTERNS: RegExp[] = [
|
|
10
|
+
// Destructive file operations
|
|
11
|
+
/\brm\s+(-[a-z]*r|-[a-z]*f)[a-z]*\s/i, // rm with -r or -f flags
|
|
12
|
+
|
|
13
|
+
// Git remote/destructive operations
|
|
14
|
+
/\bgit\s+push\b/i, // git push (no remote ops)
|
|
15
|
+
/\bgit\s+reset\s+--hard\b/i, // git reset --hard
|
|
16
|
+
|
|
17
|
+
// Package publishing
|
|
18
|
+
/\bnpm\s+publish\b/i, // npm publish
|
|
19
|
+
|
|
20
|
+
// Remote code execution via download
|
|
21
|
+
/\bcurl\b.*\|\s*(sh|bash)\b/i, // curl | sh/bash
|
|
22
|
+
/\bwget\b.*\|\s*(sh|bash)\b/i, // wget | sh/bash
|
|
23
|
+
|
|
24
|
+
// Privilege escalation
|
|
25
|
+
/\bsudo\b/i, // sudo
|
|
26
|
+
|
|
27
|
+
// Dangerous permissions
|
|
28
|
+
/\bchmod\s+777\b/i, // chmod 777
|
|
29
|
+
|
|
30
|
+
// Process killing
|
|
31
|
+
/\bkill\s+-9\b/i, // kill -9
|
|
32
|
+
|
|
33
|
+
// Disk destruction
|
|
34
|
+
/\bmkfs\b/i, // format disk
|
|
35
|
+
/\bdd\s+if=/i, // dd (disk destroyer)
|
|
36
|
+
/>\s*\/dev\/sd/i, // write to raw disk
|
|
37
|
+
|
|
38
|
+
// Pipe-to-interpreter: encoded payload execution
|
|
39
|
+
/\bbase64\b.*\|\s*(sh|bash|zsh)\b/i, // base64 | bash
|
|
40
|
+
|
|
41
|
+
// Inline code execution via interpreters
|
|
42
|
+
/\bnode\s+(-e|--eval)\b/i, // node -e / node --eval
|
|
43
|
+
/\bpython[23]?\s+-c\b/i, // python -c / python3 -c
|
|
44
|
+
/\bperl\s+-e\b/i, // perl -e
|
|
45
|
+
/\bruby\s+-e\b/i, // ruby -e
|
|
46
|
+
|
|
47
|
+
// Shell eval (arbitrary code execution)
|
|
48
|
+
/\beval\s+/i, // eval ...
|
|
49
|
+
];
|
|
50
|
+
|
|
6
51
|
export const BLOCKED_URL_PATTERN = /localhost|127\.0\.0\.1|0\.0\.0\.0|10\.\d|172\.(1[6-9]|2\d|3[01])\.|192\.168\./i;
|
|
7
52
|
export const MAX_READ_SIZE = 1_048_576;
|
|
8
53
|
export const CHARS_PER_TOKEN = 4;
|
|
9
54
|
|
|
10
|
-
|
|
11
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Check if a shell command matches any blocked pattern.
|
|
57
|
+
* Returns the matching pattern source string, or false if safe.
|
|
58
|
+
*/
|
|
59
|
+
export function isBlockedCommand(cmd: string): string | false {
|
|
60
|
+
for (const pattern of BLOCKED_SHELL_PATTERNS) {
|
|
61
|
+
if (pattern.test(cmd)) {
|
|
62
|
+
return pattern.source;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
12
66
|
}
|
|
13
67
|
|
|
14
68
|
export function isBlockedUrl(url: string): boolean {
|
package/src/bot/session-state.ts
CHANGED
|
@@ -42,16 +42,30 @@ export class SessionStore {
|
|
|
42
42
|
return JSON.parse(fs.readFileSync(this.filePath, 'utf-8')) as SessionState;
|
|
43
43
|
} catch (err) {
|
|
44
44
|
if (process.env.WEAVER_VERBOSE) process.stderr.write(`[weaver] session state load failed: ${err}\n`);
|
|
45
|
+
// Try backup recovery
|
|
46
|
+
return this.loadBackup();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Recover session from backup file when primary is corrupt. */
|
|
51
|
+
private loadBackup(): SessionState | null {
|
|
52
|
+
const backupPath = this.filePath + '.bak';
|
|
53
|
+
if (!fs.existsSync(backupPath)) return null;
|
|
54
|
+
try {
|
|
55
|
+
const data = JSON.parse(fs.readFileSync(backupPath, 'utf-8')) as SessionState;
|
|
56
|
+
// Restore backup to primary
|
|
57
|
+
try { this.writeAtomic(data); } catch { /* best effort */ }
|
|
58
|
+
return data;
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (process.env.WEAVER_VERBOSE) process.stderr.write(`[weaver] session backup load failed: ${err}\n`);
|
|
45
61
|
return null;
|
|
46
62
|
}
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
async save(state: SessionState): Promise<void> {
|
|
50
66
|
return withFileLock(this.filePath, () => {
|
|
51
|
-
const dir = path.dirname(this.filePath);
|
|
52
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
53
67
|
state.lastActivity = Date.now();
|
|
54
|
-
|
|
68
|
+
this.writeAtomic(state);
|
|
55
69
|
});
|
|
56
70
|
}
|
|
57
71
|
|
|
@@ -60,15 +74,41 @@ export class SessionStore {
|
|
|
60
74
|
const state = this.load();
|
|
61
75
|
if (!state) return null;
|
|
62
76
|
Object.assign(state, patch);
|
|
63
|
-
const dir = path.dirname(this.filePath);
|
|
64
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
65
77
|
state.lastActivity = Date.now();
|
|
66
|
-
|
|
78
|
+
this.writeAtomic(state);
|
|
67
79
|
return state;
|
|
68
80
|
});
|
|
69
81
|
}
|
|
70
82
|
|
|
71
83
|
clear(): void {
|
|
72
|
-
try {
|
|
84
|
+
try {
|
|
85
|
+
fs.unlinkSync(this.filePath);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Atomic write: write to temp file, backup existing, rename into place. */
|
|
92
|
+
private writeAtomic(state: SessionState): void {
|
|
93
|
+
const dir = path.dirname(this.filePath);
|
|
94
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
95
|
+
|
|
96
|
+
const tmpPath = this.filePath + `.tmp.${process.pid}`;
|
|
97
|
+
const backupPath = this.filePath + '.bak';
|
|
98
|
+
const content = JSON.stringify(state, null, 2);
|
|
99
|
+
|
|
100
|
+
// Write to temp file first
|
|
101
|
+
fs.writeFileSync(tmpPath, content, 'utf-8');
|
|
102
|
+
|
|
103
|
+
// Backup current file if it exists
|
|
104
|
+
if (fs.existsSync(this.filePath)) {
|
|
105
|
+
try { fs.copyFileSync(this.filePath, backupPath); } catch { /* best effort */ }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Atomic rename
|
|
109
|
+
fs.renameSync(tmpPath, this.filePath);
|
|
110
|
+
|
|
111
|
+
// Update backup after successful write
|
|
112
|
+
try { fs.copyFileSync(this.filePath, backupPath); } catch { /* best effort */ }
|
|
73
113
|
}
|
|
74
114
|
}
|