moflo 4.8.87-rc.4 → 4.8.87
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/bin/hooks.mjs +10 -3
- package/bin/lib/daemon-config.mjs +19 -0
- package/bin/lib/process-manager.mjs +24 -3
- package/bin/session-start-launcher.mjs +36 -0
- package/dist/src/cli/init/settings-generator.js +4 -1
- package/dist/src/cli/memory/memory-initializer.js +10 -11
- package/dist/src/cli/services/spell-gate.js +4 -2
- package/dist/src/cli/services/worker-daemon.js +14 -2
- package/dist/src/cli/shared/utils/atomic-file-write.d.ts +9 -2
- package/dist/src/cli/shared/utils/atomic-file-write.js +10 -3
- package/dist/src/cli/version.d.ts +1 -1
- package/dist/src/cli/version.js +1 -1
- package/package.json +3 -3
package/bin/hooks.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { existsSync, appendFileSync, readFileSync, writeFileSync, mkdirSync, sta
|
|
|
24
24
|
import { resolve, dirname } from 'path';
|
|
25
25
|
import { fileURLToPath } from 'url';
|
|
26
26
|
import { createProcessManager } from './lib/process-manager.mjs';
|
|
27
|
+
import { shouldDaemonAutoStart } from './lib/daemon-config.mjs';
|
|
27
28
|
|
|
28
29
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
30
|
const __dirname = dirname(__filename);
|
|
@@ -560,13 +561,19 @@ function touchSpawnStamp() {
|
|
|
560
561
|
|
|
561
562
|
// Run daemon start in background (non-blocking) — skip if already running
|
|
562
563
|
function runDaemonStartBackground() {
|
|
563
|
-
// 1.
|
|
564
|
+
// 1. Honor user opt-out via .claude/settings.json claudeFlow.daemon.autoStart
|
|
565
|
+
if (!shouldDaemonAutoStart(projectRoot)) {
|
|
566
|
+
log('info', 'Daemon autoStart disabled in settings, skipping');
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 2. Check if a live daemon already holds the lock
|
|
564
571
|
if (isDaemonLockHeld()) {
|
|
565
572
|
log('info', 'Daemon already running (lock held), skipping start');
|
|
566
573
|
return;
|
|
567
574
|
}
|
|
568
575
|
|
|
569
|
-
//
|
|
576
|
+
// 3. Debounce: skip if we spawned recently (prevents thundering herd)
|
|
570
577
|
if (isDaemonSpawnRecent()) {
|
|
571
578
|
log('info', 'Daemon spawn debounced (recent attempt), skipping');
|
|
572
579
|
return;
|
|
@@ -578,7 +585,7 @@ function runDaemonStartBackground() {
|
|
|
578
585
|
return;
|
|
579
586
|
}
|
|
580
587
|
|
|
581
|
-
//
|
|
588
|
+
// 4. Write stamp BEFORE spawning so concurrent callers see it immediately
|
|
582
589
|
touchSpawnStamp();
|
|
583
590
|
|
|
584
591
|
spawnWindowless('node', [localCli, 'daemon', 'start', '--quiet'], 'daemon');
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Daemon spawn-gate for bin/ scripts. Reads .claude/settings.json — the
|
|
2
|
+
// Claude-Code-facing surface. moflo.yaml's daemon.auto_start is a separate
|
|
3
|
+
// parallel gate honored by src/cli/index.ts maybeAutoStartDaemon.
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import { resolve } from 'path';
|
|
7
|
+
|
|
8
|
+
// Default-true on missing file, missing key, or malformed JSON so a broken
|
|
9
|
+
// config can't silently disable the daemon.
|
|
10
|
+
export function shouldDaemonAutoStart(projectRoot) {
|
|
11
|
+
try {
|
|
12
|
+
const settingsPath = resolve(projectRoot, '.claude', 'settings.json');
|
|
13
|
+
if (!existsSync(settingsPath)) return true;
|
|
14
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
15
|
+
return settings?.claudeFlow?.daemon?.autoStart !== false;
|
|
16
|
+
} catch {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -56,7 +56,8 @@ function lockPath(root) {
|
|
|
56
56
|
return resolve(root, '.claude-flow', 'spawn.lock');
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
/** Raw read — returns whatever's on disk without filtering or rewriting. */
|
|
60
|
+
function readRegistryRaw(root) {
|
|
60
61
|
const p = registryPath(root);
|
|
61
62
|
if (!existsSync(p)) return [];
|
|
62
63
|
try {
|
|
@@ -67,6 +68,22 @@ function readRegistry(root) {
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Default read — auto-prunes dead PIDs and rewrites the file when stale
|
|
73
|
+
* entries are detected. Catches the "abnormal session termination" case
|
|
74
|
+
* (Claude killed via task manager, OS reboot, hard crash) where session-end
|
|
75
|
+
* never ran and the registry would otherwise grow unboundedly. Rewrite is
|
|
76
|
+
* skipped when nothing changed so steady-state reads stay cheap.
|
|
77
|
+
*/
|
|
78
|
+
function readRegistry(root) {
|
|
79
|
+
const raw = readRegistryRaw(root);
|
|
80
|
+
const live = raw.filter(e => e && typeof e.pid === 'number' && isAlive(e.pid));
|
|
81
|
+
if (live.length !== raw.length) {
|
|
82
|
+
try { writeRegistry(root, live); } catch { /* non-fatal */ }
|
|
83
|
+
}
|
|
84
|
+
return live;
|
|
85
|
+
}
|
|
86
|
+
|
|
70
87
|
/** Atomic write: write to tmp file then rename to avoid torn reads. */
|
|
71
88
|
function writeRegistry(root, entries) {
|
|
72
89
|
const p = registryPath(root);
|
|
@@ -191,7 +208,9 @@ export function createProcessManager(root) {
|
|
|
191
208
|
* @returns {{ killed: number, total: number }}
|
|
192
209
|
*/
|
|
193
210
|
killAll() {
|
|
194
|
-
|
|
211
|
+
// Use raw read so `total` reflects every on-disk entry — including
|
|
212
|
+
// dead ones we'd otherwise skip silently. Matches the prior contract.
|
|
213
|
+
const entries = readRegistryRaw(projectRoot);
|
|
195
214
|
let killed = 0;
|
|
196
215
|
|
|
197
216
|
for (const entry of entries) {
|
|
@@ -227,7 +246,9 @@ export function createProcessManager(root) {
|
|
|
227
246
|
* @returns {{ pruned: number, remaining: number }}
|
|
228
247
|
*/
|
|
229
248
|
prune() {
|
|
230
|
-
|
|
249
|
+
// Use raw read so the pruned count reflects what was on disk, not
|
|
250
|
+
// what the auto-pruning readRegistry would have already cleaned up.
|
|
251
|
+
const entries = readRegistryRaw(projectRoot);
|
|
231
252
|
const alive = entries.filter(e => isAlive(e.pid));
|
|
232
253
|
writeRegistry(projectRoot, alive);
|
|
233
254
|
return { pruned: entries.length - alive.length, remaining: alive.length };
|
|
@@ -222,6 +222,42 @@ try {
|
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
// Recycle the running daemon — its in-process module cache holds the
|
|
226
|
+
// previous moflo image. After an upgrade that cache is stale, which
|
|
227
|
+
// shows up as warnings from removed code paths (e.g. the
|
|
228
|
+
// `[neural-tools] @moflo/embeddings not resolvable` spam from #639,
|
|
229
|
+
// emitted by pre-#592 collapse code that no longer exists in source)
|
|
230
|
+
// and means freshly-disabled workers keep running.
|
|
231
|
+
//
|
|
232
|
+
// Recycle = stop old + start new. We kill the lock-recorded PID,
|
|
233
|
+
// remove the lock, then fire a fresh daemon so the user keeps the
|
|
234
|
+
// functionality they had. `daemon.autoStart` only governs the
|
|
235
|
+
// cold-start case (no daemon existed); here a daemon was actually
|
|
236
|
+
// running, so replacing it with a current-code copy is the desired
|
|
237
|
+
// behaviour regardless of that flag.
|
|
238
|
+
try {
|
|
239
|
+
const lockFile = resolve(projectRoot, '.claude-flow', 'daemon.lock');
|
|
240
|
+
if (existsSync(lockFile)) {
|
|
241
|
+
let stalePid = null;
|
|
242
|
+
try {
|
|
243
|
+
const lock = JSON.parse(readFileSync(lockFile, 'utf-8'));
|
|
244
|
+
if (typeof lock?.pid === 'number' && lock.pid > 0) stalePid = lock.pid;
|
|
245
|
+
} catch { /* malformed lock — fall through to unlink */ }
|
|
246
|
+
if (stalePid !== null) {
|
|
247
|
+
try { process.kill(stalePid, 'SIGTERM'); } catch { /* already dead */ }
|
|
248
|
+
}
|
|
249
|
+
try { unlinkSync(lockFile); } catch { /* non-fatal */ }
|
|
250
|
+
// Respawn only if a live daemon was actually recorded — no point
|
|
251
|
+
// starting one when there wasn't one before.
|
|
252
|
+
if (stalePid !== null) {
|
|
253
|
+
const localCliPath = resolve(binDir, 'cli.js');
|
|
254
|
+
if (existsSync(localCliPath)) {
|
|
255
|
+
fireAndForget('node', [localCliPath, 'daemon', 'start', '--quiet'], 'daemon-recycle');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch { /* non-fatal — daemon recycle is best-effort */ }
|
|
260
|
+
|
|
225
261
|
// Write updated manifest + version stamp
|
|
226
262
|
try {
|
|
227
263
|
const cfDir = resolve(projectRoot, '.claude-flow');
|
|
@@ -104,9 +104,12 @@ export function generateSettings(options) {
|
|
|
104
104
|
},
|
|
105
105
|
daemon: {
|
|
106
106
|
autoStart: true,
|
|
107
|
+
// Note: this list is documentation for the user — the daemon's actual
|
|
108
|
+
// worker registry lives in src/cli/services/worker-daemon.ts DEFAULT_WORKERS.
|
|
109
|
+
// 'audit' is intentionally absent here because it's default-disabled
|
|
110
|
+
// pending the perf fix in #631.
|
|
107
111
|
workers: [
|
|
108
112
|
'map', // Codebase mapping
|
|
109
|
-
'audit', // Security auditing (critical priority)
|
|
110
113
|
'optimize', // Performance optimization (high priority)
|
|
111
114
|
'consolidate', // Memory consolidation
|
|
112
115
|
'testgaps', // Test coverage gaps
|
|
@@ -17,16 +17,17 @@ import { createEmbeddingService } from '../embeddings/index.js';
|
|
|
17
17
|
import { HnswLite } from './hnsw-lite.js';
|
|
18
18
|
/**
|
|
19
19
|
* Write vector-stats.json cache for the statusline (no subprocess needed).
|
|
20
|
-
* Called after memory store
|
|
20
|
+
* Called after memory store in the raw-sql.js fallback path. The bridge path
|
|
21
|
+
* goes through refreshVectorStatsCache() in bridge-core.ts instead.
|
|
21
22
|
* @param dbPath - path to the SQLite database file
|
|
22
|
-
* @param stats -
|
|
23
|
+
* @param stats - exact counts from a db query already in progress (required —
|
|
24
|
+
* making this optional caused issue #639 by silently writing 0)
|
|
23
25
|
*/
|
|
24
26
|
function writeVectorStatsCache(dbPath, stats) {
|
|
25
27
|
try {
|
|
26
28
|
const fileStat = fs.statSync(dbPath);
|
|
27
29
|
const dbSizeKB = Math.floor(fileStat.size / 1024);
|
|
28
|
-
const vectorCount = stats
|
|
29
|
-
const namespaces = stats?.namespaces ?? 0;
|
|
30
|
+
const { vectorCount, namespaces } = stats;
|
|
30
31
|
// Check HNSW index presence
|
|
31
32
|
const dbDir = path.dirname(dbPath);
|
|
32
33
|
const projectDir = path.dirname(dbDir); // .swarm -> project root
|
|
@@ -1568,17 +1569,15 @@ export async function verifyMemoryInit(dbPath, options) {
|
|
|
1568
1569
|
* This bypasses MCP and writes directly to the database
|
|
1569
1570
|
*/
|
|
1570
1571
|
export async function storeEntry(options) {
|
|
1571
|
-
// ADR-053: Try AgentDB v3 bridge first
|
|
1572
|
+
// ADR-053: Try AgentDB v3 bridge first. The bridge calls
|
|
1573
|
+
// refreshVectorStatsCache() itself (bridge-entries.ts:191) — a second
|
|
1574
|
+
// write here was redundant and previously clobbered the correct count
|
|
1575
|
+
// with 0 (#639).
|
|
1572
1576
|
const bridge = await getBridge();
|
|
1573
1577
|
if (bridge) {
|
|
1574
1578
|
const bridgeResult = await bridge.bridgeStoreEntry(options);
|
|
1575
|
-
if (bridgeResult)
|
|
1576
|
-
// Update statusline cache after successful bridge store
|
|
1577
|
-
const swarmDir = path.join(process.cwd(), '.swarm');
|
|
1578
|
-
const dbFile = options.dbPath || path.join(swarmDir, 'memory.db');
|
|
1579
|
-
writeVectorStatsCache(dbFile);
|
|
1579
|
+
if (bridgeResult)
|
|
1580
1580
|
return bridgeResult;
|
|
1581
|
-
}
|
|
1582
1581
|
}
|
|
1583
1582
|
// Fallback: raw sql.js
|
|
1584
1583
|
const { key, value, namespace = 'default', generateEmbeddingFlag = true, tags = [], ttl, dbPath: customPath, upsert = false } = options;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import * as fs from 'fs';
|
|
19
19
|
import * as path from 'path';
|
|
20
|
+
import { atomicWriteFileSync } from './atomic-file-write.js';
|
|
20
21
|
import { loadMofloConfig } from '../config/moflo-config.js';
|
|
21
22
|
// ============================================================================
|
|
22
23
|
// Constants
|
|
@@ -108,10 +109,11 @@ export class GateService {
|
|
|
108
109
|
writeState(state) {
|
|
109
110
|
try {
|
|
110
111
|
fs.mkdirSync(path.dirname(this.stateFilePath), { recursive: true });
|
|
111
|
-
|
|
112
|
+
// Atomic write so concurrent gate-hook processes never produce torn JSON.
|
|
113
|
+
atomicWriteFileSync(this.stateFilePath, JSON.stringify(state, null, 2));
|
|
112
114
|
}
|
|
113
115
|
catch {
|
|
114
|
-
// Non-fatal
|
|
116
|
+
// Non-fatal — last-writer-wins; updates may be lost, never corrupted.
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
119
|
// --------------------------------------------------------------------------
|
|
@@ -11,13 +11,17 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { EventEmitter } from 'events';
|
|
13
13
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
14
|
+
import { atomicWriteFileSync } from './atomic-file-write.js';
|
|
14
15
|
import { cpus } from 'os';
|
|
15
16
|
import { join } from 'path';
|
|
16
17
|
import { HeadlessWorkerExecutor, isHeadlessWorker, } from './headless-worker-executor.js';
|
|
17
18
|
// Default worker configurations with improved intervals (P0 fix: map 5min -> 15min)
|
|
18
19
|
const DEFAULT_WORKERS = [
|
|
19
20
|
{ type: 'map', intervalMs: 15 * 60 * 1000, offsetMs: 0, priority: 'normal', description: 'Codebase mapping', enabled: true },
|
|
20
|
-
|
|
21
|
+
// Default-disabled until the perf regression in #631 is remediated. The
|
|
22
|
+
// worker averages 238 s/run on real installs, saturating cores back-to-back
|
|
23
|
+
// when scheduled at the 10-minute interval. Re-enable here when #631 ships.
|
|
24
|
+
{ type: 'audit', intervalMs: 10 * 60 * 1000, offsetMs: 2 * 60 * 1000, priority: 'critical', description: 'Security analysis', enabled: false },
|
|
21
25
|
{ type: 'optimize', intervalMs: 15 * 60 * 1000, offsetMs: 4 * 60 * 1000, priority: 'high', description: 'Performance optimization', enabled: true },
|
|
22
26
|
{ type: 'consolidate', intervalMs: 30 * 60 * 1000, offsetMs: 6 * 60 * 1000, priority: 'low', description: 'Memory consolidation', enabled: true },
|
|
23
27
|
{ type: 'testgaps', intervalMs: 20 * 60 * 1000, offsetMs: 8 * 60 * 1000, priority: 'normal', description: 'Test coverage analysis', enabled: true },
|
|
@@ -336,10 +340,17 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
336
340
|
this.startedAt = new Date();
|
|
337
341
|
this.emit('started', { pid: process.pid, startedAt: this.startedAt });
|
|
338
342
|
// Schedule all enabled workers
|
|
343
|
+
const skipped = [];
|
|
339
344
|
for (const workerConfig of this.config.workers) {
|
|
340
345
|
if (workerConfig.enabled) {
|
|
341
346
|
this.scheduleWorker(workerConfig);
|
|
342
347
|
}
|
|
348
|
+
else {
|
|
349
|
+
skipped.push(workerConfig.type);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (skipped.length > 0) {
|
|
353
|
+
this.log('info', `Skipping disabled workers: ${skipped.join(', ')}`);
|
|
343
354
|
}
|
|
344
355
|
if (this.scheduler && !this.scheduler.isRunning) {
|
|
345
356
|
this.scheduler.start();
|
|
@@ -881,7 +892,8 @@ export class WorkerDaemon extends EventEmitter {
|
|
|
881
892
|
savedAt: new Date().toISOString(),
|
|
882
893
|
};
|
|
883
894
|
try {
|
|
884
|
-
|
|
895
|
+
// Atomic write so a force-kill mid-write can't leave partial JSON behind.
|
|
896
|
+
atomicWriteFileSync(this.config.stateFile, JSON.stringify(state, null, 2));
|
|
885
897
|
}
|
|
886
898
|
catch (error) {
|
|
887
899
|
this.log('error', `Failed to save state: ${error}`);
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Atomic filesystem writes for files that must not be left corrupted if the
|
|
3
|
-
* process is interrupted mid-write (SIGINT, power loss, ENOSPC)
|
|
3
|
+
* process is interrupted mid-write (SIGINT, power loss, ENOSPC) or if multiple
|
|
4
|
+
* processes write to the same target concurrently.
|
|
4
5
|
*
|
|
5
|
-
* Pattern: write to
|
|
6
|
+
* Pattern: write to a process-unique temp path `<target>.tmp.<pid>.<rand>`,
|
|
7
|
+
* then rename onto `target`.
|
|
6
8
|
* - `fs.renameSync` is atomic on POSIX.
|
|
7
9
|
* - On Windows, Node maps it to `MoveFileExW(..., MOVEFILE_REPLACE_EXISTING)`,
|
|
8
10
|
* which replaces the destination near-atomically — concurrent readers
|
|
9
11
|
* always observe either the old file or the new, never a truncated one.
|
|
12
|
+
* - The unique temp path means concurrent writers can't clobber each other's
|
|
13
|
+
* in-flight bytes (#635). Last-writer-wins semantics: each rename is fully
|
|
14
|
+
* atomic, so the destination always reflects exactly one writer's data.
|
|
15
|
+
* Updates from earlier writers may be lost — that's a separate concern
|
|
16
|
+
* requiring read-modify-write under a file lock.
|
|
10
17
|
*
|
|
11
18
|
* On any failure, the temp file is best-effort removed and the original
|
|
12
19
|
* `target` stays intact. The underlying error is always re-thrown.
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Atomic filesystem writes for files that must not be left corrupted if the
|
|
3
|
-
* process is interrupted mid-write (SIGINT, power loss, ENOSPC)
|
|
3
|
+
* process is interrupted mid-write (SIGINT, power loss, ENOSPC) or if multiple
|
|
4
|
+
* processes write to the same target concurrently.
|
|
4
5
|
*
|
|
5
|
-
* Pattern: write to
|
|
6
|
+
* Pattern: write to a process-unique temp path `<target>.tmp.<pid>.<rand>`,
|
|
7
|
+
* then rename onto `target`.
|
|
6
8
|
* - `fs.renameSync` is atomic on POSIX.
|
|
7
9
|
* - On Windows, Node maps it to `MoveFileExW(..., MOVEFILE_REPLACE_EXISTING)`,
|
|
8
10
|
* which replaces the destination near-atomically — concurrent readers
|
|
9
11
|
* always observe either the old file or the new, never a truncated one.
|
|
12
|
+
* - The unique temp path means concurrent writers can't clobber each other's
|
|
13
|
+
* in-flight bytes (#635). Last-writer-wins semantics: each rename is fully
|
|
14
|
+
* atomic, so the destination always reflects exactly one writer's data.
|
|
15
|
+
* Updates from earlier writers may be lost — that's a separate concern
|
|
16
|
+
* requiring read-modify-write under a file lock.
|
|
10
17
|
*
|
|
11
18
|
* On any failure, the temp file is best-effort removed and the original
|
|
12
19
|
* `target` stays intact. The underlying error is always re-thrown.
|
|
@@ -18,7 +25,7 @@
|
|
|
18
25
|
*/
|
|
19
26
|
import * as realFs from 'node:fs';
|
|
20
27
|
export function atomicWriteFileSync(targetPath, data, fs = realFs) {
|
|
21
|
-
const tmpPath = `${targetPath}.tmp`;
|
|
28
|
+
const tmpPath = `${targetPath}.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`;
|
|
22
29
|
try {
|
|
23
30
|
fs.writeFileSync(tmpPath, data);
|
|
24
31
|
fs.renameSync(tmpPath, targetPath);
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.8.87
|
|
3
|
+
"version": "4.8.87",
|
|
4
4
|
"description": "MoFlo — AI agent orchestration for Claude Code. Forked from ruflo/claude-flow with patches applied to source, plus feature-level orchestration.",
|
|
5
5
|
"main": "dist/src/cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"scripts": {
|
|
46
46
|
"dev": "tsx watch src/cli/index.ts",
|
|
47
47
|
"prebuild": "node scripts/sync-version.mjs && node scripts/clean-dist.mjs",
|
|
48
|
-
"build": "tsc
|
|
48
|
+
"build": "tsc && node -e \"const{cpSync}=require('fs');cpSync('src/cli/epic/spells','dist/src/cli/epic/spells',{recursive:true})\"",
|
|
49
49
|
"prepublishOnly": "npm run build",
|
|
50
50
|
"postinstall": "node scripts/prune-native-binaries.mjs",
|
|
51
51
|
"test": "node scripts/test-runner.mjs",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
80
80
|
"@typescript-eslint/parser": "^7.18.0",
|
|
81
81
|
"eslint": "^8.0.0",
|
|
82
|
-
"moflo": "^4.8.87-rc.
|
|
82
|
+
"moflo": "^4.8.87-rc.4",
|
|
83
83
|
"tsx": "^4.21.0",
|
|
84
84
|
"typescript": "^5.9.3",
|
|
85
85
|
"vitest": "^4.0.0"
|