moflo 4.10.14 → 4.10.16
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.
|
@@ -145,6 +145,10 @@ For the full `moflo.yaml` schema, gate toggles, model routing, and sandbox confi
|
|
|
145
145
|
| Every 5+ file changes | `map` | Update codebase map |
|
|
146
146
|
| Complex debugging | `deepdive` | Deep code analysis |
|
|
147
147
|
|
|
148
|
+
### Worker Report Location
|
|
149
|
+
|
|
150
|
+
Headless workers (`optimize`, `testgaps`, `ultralearn`, `refactor`, `deepdive`) write the latest run's full output to `.moflo/reports/<workerType>.<ext>` (`.md` for markdown workers, `.json` for `ultralearn`). The path is overwritten each run; for history, see `.moflo/logs/headless/`. The directory is gitignored by `flo init`, so reports never reach a consumer's commit. When the user asks "what did testgaps find?" or "where's the optimize report?", read `.moflo/reports/<workerType>.md` directly — do NOT re-run the worker.
|
|
151
|
+
|
|
148
152
|
### Memory-Enhanced Development
|
|
149
153
|
|
|
150
154
|
| Action | When |
|
package/README.md
CHANGED
|
@@ -419,7 +419,7 @@ flo daemon status # shows whether the service is registered AND running
|
|
|
419
419
|
|
|
420
420
|
`flo spell schedule create` warns when the daemon isn't installed so you don't quietly miss runs.
|
|
421
421
|
|
|
422
|
-
**Monitoring.** **[The Luminarium](#the-luminarium)** — moflo's localhost daemon dashboard — surfaces live schedules, recent executions, and per-schedule controls (disable / re-enable / run now), alongside worker health, memory stats, and Claude Code session stats.
|
|
422
|
+
**Monitoring.** **[The Luminarium](#the-luminarium)** — moflo's localhost daemon dashboard — surfaces live schedules, recent executions, and per-schedule controls (disable / re-enable / run now), alongside worker health, memory stats, and Claude Code session stats. Ask `/luminarium` in your Claude session and it'll print the link.
|
|
423
423
|
|
|
424
424
|
For full configuration (`scheduler:` block in `moflo.yaml`), event types, and the catch-up window after restarts, see [docs/SPELLS.md#scheduling](docs/SPELLS.md#scheduling).
|
|
425
425
|
|
|
@@ -465,13 +465,7 @@ The Luminarium is moflo's localhost daemon dashboard. It boots automatically wit
|
|
|
465
465
|
|
|
466
466
|
### Finding the URL
|
|
467
467
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
Three ways to get the URL:
|
|
471
|
-
|
|
472
|
-
- **`/luminarium`** — inside a Claude Code session in a moflo project, this skill reads `.moflo/daemon.lock` and prints `http://localhost:<port>`. Fastest path.
|
|
473
|
-
- **`flo daemon status`** — prints the URL alongside the health summary.
|
|
474
|
-
- **`cat .moflo/daemon.lock`** — read the JSON directly: `{ "pid": ..., "port": 33421, ... }`.
|
|
468
|
+
Inside a Claude Code session in a moflo project, ask **`/luminarium`** — the skill prints the dashboard URL for the current project. `flo daemon status` prints the same URL alongside the health summary. Each project binds its own port so two projects on the same machine never collide.
|
|
475
469
|
|
|
476
470
|
### What it shows
|
|
477
471
|
|
|
@@ -486,7 +480,7 @@ Three ways to get the URL:
|
|
|
486
480
|
### Flags
|
|
487
481
|
|
|
488
482
|
- `flo daemon start --no-dashboard` — disable the HTTP server entirely (the daemon itself still runs)
|
|
489
|
-
- `flo daemon start --dashboard-port <N>` — pin to a specific port, overriding the
|
|
483
|
+
- `flo daemon start --dashboard-port <N>` — pin to a specific port, overriding the per-project default. Also accepts the `MOFLO_DAEMON_PORT` env var, which the rest of moflo respects when talking to the daemon
|
|
490
484
|
|
|
491
485
|
## Commands
|
|
492
486
|
|
|
@@ -90,7 +90,7 @@ Provide actionable suggestions with code examples.`,
|
|
|
90
90
|
- Check for missing error handling tests
|
|
91
91
|
- Identify integration test gaps
|
|
92
92
|
|
|
93
|
-
For each gap,
|
|
93
|
+
For each gap, include a test skeleton inline in the report as a fenced code block — DO NOT create separate test files; the consumer will copy the skeletons into their test tree by hand.`,
|
|
94
94
|
sandbox: 'permissive',
|
|
95
95
|
model: 'sonnet',
|
|
96
96
|
outputFormat: 'markdown',
|
|
@@ -300,11 +300,13 @@ export class HeadlessWorkerExecutor extends EventEmitter {
|
|
|
300
300
|
maxContextFiles: options?.maxContextFiles ?? 20,
|
|
301
301
|
maxCharsPerFile: options?.maxCharsPerFile ?? 5000,
|
|
302
302
|
logDir: options?.logDir ?? join(projectRoot, '.moflo', 'logs', 'headless'),
|
|
303
|
+
reportsDir: options?.reportsDir ?? join(projectRoot, '.moflo', 'reports'),
|
|
303
304
|
cacheContext: options?.cacheContext ?? true,
|
|
304
305
|
cacheTtlMs: options?.cacheTtlMs ?? 60000, // 1 minute default
|
|
305
306
|
};
|
|
306
|
-
// Ensure log
|
|
307
|
+
// Ensure log + reports directories exist
|
|
307
308
|
this.ensureLogDir();
|
|
309
|
+
this.ensureReportsDir();
|
|
308
310
|
// Register for process-exit cleanup via the shared listener.
|
|
309
311
|
ensureExitListener();
|
|
310
312
|
liveExecutors.add(this);
|
|
@@ -546,6 +548,29 @@ export class HeadlessWorkerExecutor extends EventEmitter {
|
|
|
546
548
|
this.emit('warning', { message: 'Failed to create log directory', error });
|
|
547
549
|
}
|
|
548
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* Ensure the reports directory exists. Reports land under `.moflo/reports/`
|
|
553
|
+
* (gitignored by `flo init`); without this directory the post-spawn write
|
|
554
|
+
* would no-op and the consumer would lose the report entirely.
|
|
555
|
+
*/
|
|
556
|
+
ensureReportsDir() {
|
|
557
|
+
try {
|
|
558
|
+
if (!existsSync(this.config.reportsDir)) {
|
|
559
|
+
mkdirSync(this.config.reportsDir, { recursive: true });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
this.emit('warning', { message: 'Failed to create reports directory', error });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Resolve the on-disk report path for a worker. Extension matches the
|
|
568
|
+
* declared outputFormat so tools reading the report don't have to sniff.
|
|
569
|
+
*/
|
|
570
|
+
getReportPath(workerType, outputFormat) {
|
|
571
|
+
const ext = outputFormat === 'json' ? 'json' : outputFormat === 'text' ? 'txt' : 'md';
|
|
572
|
+
return join(this.config.reportsDir, `${workerType}.${ext}`);
|
|
573
|
+
}
|
|
549
574
|
/**
|
|
550
575
|
* Internal execution logic
|
|
551
576
|
*/
|
|
@@ -558,8 +583,11 @@ export class HeadlessWorkerExecutor extends EventEmitter {
|
|
|
558
583
|
try {
|
|
559
584
|
// Build context from file patterns
|
|
560
585
|
const context = await this.buildContext(headless.contextPatterns || []);
|
|
586
|
+
// Resolve the on-disk report path before prompt assembly so the prompt
|
|
587
|
+
// can quote the exact absolute path Claude should write to.
|
|
588
|
+
const reportPath = this.getReportPath(workerType, headless.outputFormat);
|
|
561
589
|
// Build the full prompt
|
|
562
|
-
const fullPrompt = this.buildPrompt(headless.promptTemplate, context);
|
|
590
|
+
const fullPrompt = this.buildPrompt(headless.promptTemplate, context, reportPath);
|
|
563
591
|
// Log prompt for debugging
|
|
564
592
|
this.logExecution(executionId, 'prompt', fullPrompt);
|
|
565
593
|
// Execute Claude Code headlessly
|
|
@@ -571,6 +599,13 @@ export class HeadlessWorkerExecutor extends EventEmitter {
|
|
|
571
599
|
workerType,
|
|
572
600
|
signal,
|
|
573
601
|
});
|
|
602
|
+
// Persist the spawn output to the canonical report path. Belt-and-braces
|
|
603
|
+
// against Claude ignoring the in-prompt instruction — this guarantees
|
|
604
|
+
// the consumer ends up with a report at a deterministic location no
|
|
605
|
+
// matter what the model chose to do with its Write tool.
|
|
606
|
+
if (result.success && result.output && result.output.trim().length > 0) {
|
|
607
|
+
this.writeReport(reportPath, result.output);
|
|
608
|
+
}
|
|
574
609
|
// Parse output based on format
|
|
575
610
|
let parsedOutput;
|
|
576
611
|
if (headless.outputFormat === 'json' && result.output) {
|
|
@@ -768,15 +803,23 @@ export class HeadlessWorkerExecutor extends EventEmitter {
|
|
|
768
803
|
return name === pattern;
|
|
769
804
|
}
|
|
770
805
|
/**
|
|
771
|
-
* Build full prompt with context
|
|
806
|
+
* Build full prompt with context. The report path is injected so Claude
|
|
807
|
+
* saves output to the canonical `.moflo/reports/` location instead of
|
|
808
|
+
* dropping `*-analysis.md` / `*-report.md` files at the project root — the
|
|
809
|
+
* behaviour consumers were seeing before this change.
|
|
772
810
|
*/
|
|
773
|
-
buildPrompt(template, context) {
|
|
811
|
+
buildPrompt(template, context, reportPath) {
|
|
812
|
+
const ioInstructions = `## Output
|
|
813
|
+
|
|
814
|
+
Save the full report to \`${reportPath}\` using the Write tool. Overwrite any prior content at that path. DO NOT create any other files anywhere in the project; if you need to suggest test skeletons or code samples, include them inline in the report as fenced code blocks. Moflo persists the same output to that path after you finish, so the location is authoritative.`;
|
|
774
815
|
if (!context) {
|
|
775
816
|
return `${template}
|
|
776
817
|
|
|
777
818
|
## Instructions
|
|
778
819
|
|
|
779
|
-
Analyze the codebase and provide your response following the format specified in the task
|
|
820
|
+
Analyze the codebase and provide your response following the format specified in the task.
|
|
821
|
+
|
|
822
|
+
${ioInstructions}`;
|
|
780
823
|
}
|
|
781
824
|
return `${template}
|
|
782
825
|
|
|
@@ -786,7 +829,9 @@ ${context}
|
|
|
786
829
|
|
|
787
830
|
## Instructions
|
|
788
831
|
|
|
789
|
-
Analyze the above codebase context and provide your response following the format specified in the task
|
|
832
|
+
Analyze the above codebase context and provide your response following the format specified in the task.
|
|
833
|
+
|
|
834
|
+
${ioInstructions}`;
|
|
790
835
|
}
|
|
791
836
|
/**
|
|
792
837
|
* Execute Claude Code in headless mode
|
|
@@ -1011,6 +1056,22 @@ Analyze the above codebase context and provide your response following the forma
|
|
|
1011
1056
|
// Ignore log write errors
|
|
1012
1057
|
}
|
|
1013
1058
|
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Persist a worker's report to the canonical `.moflo/reports/` location.
|
|
1061
|
+
* The directory was created at construction time; we recreate it here as a
|
|
1062
|
+
* safety net in case it was removed between construction and execution.
|
|
1063
|
+
*/
|
|
1064
|
+
writeReport(reportPath, content) {
|
|
1065
|
+
try {
|
|
1066
|
+
if (!existsSync(this.config.reportsDir)) {
|
|
1067
|
+
mkdirSync(this.config.reportsDir, { recursive: true });
|
|
1068
|
+
}
|
|
1069
|
+
writeFileSync(reportPath, content);
|
|
1070
|
+
}
|
|
1071
|
+
catch (error) {
|
|
1072
|
+
this.emit('warning', { message: 'Failed to write worker report', reportPath, error });
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1014
1075
|
}
|
|
1015
1076
|
// Export default
|
|
1016
1077
|
export default HeadlessWorkerExecutor;
|
|
@@ -262,14 +262,26 @@ export function computeHookBlockDrift(consumerHooks, referenceHooks) {
|
|
|
262
262
|
// Settings.json helpers — shared between launcher + doctor
|
|
263
263
|
// ────────────────────────────────────────────────────────────────────────────
|
|
264
264
|
/**
|
|
265
|
-
* True when the user has set `
|
|
266
|
-
*
|
|
265
|
+
* True when the user has set `moflo.hooks.locked: true` (canonical) or
|
|
266
|
+
* `claudeFlow.hooks.locked: true` (legacy alias) in their settings.json —
|
|
267
|
+
* a sentinel that suppresses drift surfacing entirely.
|
|
268
|
+
*
|
|
269
|
+
* The `claudeFlow.*` settings tree is a pre-rebrand legacy name that survives
|
|
270
|
+
* in writers + readers across the codebase. Renaming the whole tree is a
|
|
271
|
+
* separate effort; this one reader accepts `moflo.hooks.locked` ahead of the
|
|
272
|
+
* legacy key so the #1180 escape hatch is documented under the canonical
|
|
273
|
+
* brand from day one and consumers never have to migrate the key after we
|
|
274
|
+
* tell them to set it.
|
|
267
275
|
*/
|
|
268
276
|
export function isHookBlockLocked(settings) {
|
|
269
277
|
const root = settings;
|
|
278
|
+
const moflo = root?.moflo;
|
|
279
|
+
const mofloHooks = moflo?.hooks;
|
|
280
|
+
if (mofloHooks?.locked === true)
|
|
281
|
+
return true;
|
|
270
282
|
const cf = root?.claudeFlow;
|
|
271
|
-
const
|
|
272
|
-
return
|
|
283
|
+
const cfHooks = cf?.hooks;
|
|
284
|
+
return cfHooks?.locked === true;
|
|
273
285
|
}
|
|
274
286
|
/**
|
|
275
287
|
* Additively repair drift: for every entry in `report.missing`, locate the
|
|
@@ -309,26 +321,135 @@ export function applyAdditiveRegeneration(settings, report) {
|
|
|
309
321
|
settings.hooks = hooks;
|
|
310
322
|
return { settings, added, removed: 0 };
|
|
311
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Set of helper-script basenames that moflo ships under `.claude/helpers/` and
|
|
326
|
+
* `.claude/scripts/`. Built once from `getReferenceHookBlock()` so it always
|
|
327
|
+
* tracks whatever the current reference block actually emits. Used by the
|
|
328
|
+
* wholesale-regen path to tell "stale moflo entry from a removed reference
|
|
329
|
+
* shape" (drop) apart from "consumer customisation we never owned" (preserve).
|
|
330
|
+
*/
|
|
331
|
+
let MOFLO_HELPER_BASENAMES_CACHE = null;
|
|
332
|
+
function getMofloHelperBasenames() {
|
|
333
|
+
if (MOFLO_HELPER_BASENAMES_CACHE)
|
|
334
|
+
return MOFLO_HELPER_BASENAMES_CACHE;
|
|
335
|
+
const out = new Set();
|
|
336
|
+
const tree = getReferenceHookBlock();
|
|
337
|
+
for (const event of Object.keys(tree)) {
|
|
338
|
+
for (const block of tree[event]) {
|
|
339
|
+
for (const hook of block.hooks) {
|
|
340
|
+
const m = hook.command.match(/\.claude\/(?:helpers|scripts)\/([\w.\-]+)/);
|
|
341
|
+
if (m)
|
|
342
|
+
out.add(m[1]);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
MOFLO_HELPER_BASENAMES_CACHE = out;
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* True when a hook command references a moflo-shipped helper basename.
|
|
351
|
+
*
|
|
352
|
+
* Design: moflo "owns" the slot for any command pointing at one of its shipped
|
|
353
|
+
* helpers, so wholesale regen is free to replace that slot with the current
|
|
354
|
+
* reference. Conversely, a command pointing at a non-moflo helper basename is
|
|
355
|
+
* a consumer-owned customisation that must survive (#1180).
|
|
356
|
+
*
|
|
357
|
+
* Trade-off — hand-edits to a moflo helper command (e.g. consumer adds
|
|
358
|
+
* `--my-flag` to `gate-hook.mjs check-dangerous-command`) are treated as
|
|
359
|
+
* moflo-owned and replaced with the current reference shape on regen. The
|
|
360
|
+
* alternative (preserve any non-exact match) would silently retain stale
|
|
361
|
+
* entries like `gate.cjs session-reset` (removed in #842), defeating the
|
|
362
|
+
* whole point of wholesale regen. Consumers who need a tweaked moflo command
|
|
363
|
+
* should either lock the hook block via `moflo.hooks.locked: true`
|
|
364
|
+
* (`claudeFlow.hooks.locked` is still honoured as a legacy alias) or route
|
|
365
|
+
* through their own helper basename.
|
|
366
|
+
*
|
|
367
|
+
* Cross-platform: regex matches forward slashes only — what moflo always
|
|
368
|
+
* emits (Claude Code expands `$CLAUDE_PROJECT_DIR` on every OS). A consumer
|
|
369
|
+
* who hand-edited `settings.json` with backslashes on Windows would fail to
|
|
370
|
+
* match here and be treated as a customisation (preserved). That's the safer
|
|
371
|
+
* default — we'd rather keep an unfamiliar entry than delete user work.
|
|
372
|
+
*/
|
|
373
|
+
function isMofloOwnedHookEntry(command) {
|
|
374
|
+
const m = command.match(/\.claude\/(?:helpers|scripts)\/([\w.\-]+)/);
|
|
375
|
+
return m ? getMofloHelperBasenames().has(m[1]) : false;
|
|
376
|
+
}
|
|
312
377
|
/**
|
|
313
378
|
* Wholesale regeneration: replace `settings.hooks` with the canonical reference
|
|
314
|
-
* block
|
|
315
|
-
* `gate.cjs session-reset` SessionStart hook removed in #842) AND
|
|
316
|
-
* entries — the additive variant only does the latter.
|
|
379
|
+
* block, then graft consumer customisations back in. Drops stale moflo entries
|
|
380
|
+
* (e.g. the `gate.cjs session-reset` SessionStart hook removed in #842) AND
|
|
381
|
+
* adds missing entries — the additive variant only does the latter.
|
|
382
|
+
*
|
|
383
|
+
* #1180 — `report.extra` carries both stale moflo entries AND consumer-owned
|
|
384
|
+
* customisations (e.g. waxstak's `project-analysis-gate.cjs`). The wholesale
|
|
385
|
+
* path used to drop both; it now distinguishes them via the helper-basename
|
|
386
|
+
* discriminator: any extra command referencing a moflo-shipped helper under
|
|
387
|
+
* `.claude/helpers/` or `.claude/scripts/` is stale moflo and gets dropped;
|
|
388
|
+
* anything else is consumer-owned and gets grafted back into the fresh tree
|
|
389
|
+
* under the same `(event, matcher)`, with its original `HookEntry`
|
|
390
|
+
* (type/timeout) snapshotted before the replace.
|
|
317
391
|
*
|
|
318
392
|
* The caller MUST check `isHookBlockLocked(settings)` first; if locked, the
|
|
319
393
|
* user has opted out and this function should not be called. Non-hooks fields
|
|
320
|
-
* on `settings` (permissions, env, claudeFlow.*, etc.) are preserved.
|
|
394
|
+
* on `settings` (permissions, env, moflo.*, claudeFlow.*, etc.) are preserved.
|
|
321
395
|
*
|
|
322
396
|
* Mutates `settings` in place; caller is responsible for writing the file.
|
|
323
397
|
*/
|
|
324
398
|
export function applyWholesaleRegeneration(settings, report) {
|
|
325
399
|
if (!report.drifted)
|
|
326
400
|
return { settings, added: 0, removed: 0 };
|
|
401
|
+
// Snapshot full `HookEntry` objects for consumer customisations BEFORE we
|
|
402
|
+
// overwrite settings.hooks — the drift report carries command strings only,
|
|
403
|
+
// not timeout/type. Walk the consumer's existing tree, match against each
|
|
404
|
+
// non-moflo `extra` on (event, matcher, command).
|
|
405
|
+
const customisations = [];
|
|
406
|
+
const consumerHooks = (settings.hooks ?? {});
|
|
407
|
+
for (const extra of report.extra) {
|
|
408
|
+
if (isMofloOwnedHookEntry(extra.command))
|
|
409
|
+
continue;
|
|
410
|
+
const evtArr = consumerHooks[extra.event];
|
|
411
|
+
if (!Array.isArray(evtArr))
|
|
412
|
+
continue;
|
|
413
|
+
for (const block of evtArr) {
|
|
414
|
+
if ((block?.matcher ?? '') !== extra.matcher)
|
|
415
|
+
continue;
|
|
416
|
+
const found = Array.isArray(block.hooks)
|
|
417
|
+
? block.hooks.find((h) => h?.command === extra.command)
|
|
418
|
+
: undefined;
|
|
419
|
+
if (found) {
|
|
420
|
+
customisations.push({ event: extra.event, matcher: extra.matcher, hook: found });
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
327
425
|
// Clone the cached reference so a later mutation of settings.hooks (by the
|
|
328
426
|
// launcher's settings.json migrations, doctor --fix, etc.) cannot corrupt
|
|
329
427
|
// the cached tree shared across `computeHookBlockDrift` calls in this process.
|
|
330
|
-
|
|
331
|
-
|
|
428
|
+
const fresh = structuredClone(getCachedReference().tree);
|
|
429
|
+
// Graft customisations into the fresh tree, slotting them under the same
|
|
430
|
+
// matcher block (created if absent).
|
|
431
|
+
for (const { event, matcher, hook } of customisations) {
|
|
432
|
+
let arr = fresh[event];
|
|
433
|
+
if (!Array.isArray(arr)) {
|
|
434
|
+
arr = [];
|
|
435
|
+
fresh[event] = arr;
|
|
436
|
+
}
|
|
437
|
+
let block = arr.find(b => (b?.matcher ?? '') === matcher);
|
|
438
|
+
if (!block) {
|
|
439
|
+
block = { hooks: [] };
|
|
440
|
+
if (matcher)
|
|
441
|
+
block.matcher = matcher;
|
|
442
|
+
arr.push(block);
|
|
443
|
+
}
|
|
444
|
+
if (!Array.isArray(block.hooks))
|
|
445
|
+
block.hooks = [];
|
|
446
|
+
if (!block.hooks.some((h) => h?.command === hook.command)) {
|
|
447
|
+
block.hooks.push(hook);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
settings.hooks = fresh;
|
|
451
|
+
const removed = report.extra.length - customisations.length;
|
|
452
|
+
return { settings, added: report.missing.length, removed };
|
|
332
453
|
}
|
|
333
454
|
/**
|
|
334
455
|
* Format a drift report for human-readable output (multi-line, no colour).
|
package/dist/src/cli/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moflo",
|
|
3
|
-
"version": "4.10.
|
|
3
|
+
"version": "4.10.16",
|
|
4
4
|
"description": "MoFlo — AI agent orchestration for Claude Code. A standalone, opinionated toolkit with semantic memory, learned routing, gates, spells, and the /flo issue-execution skill.",
|
|
5
5
|
"main": "dist/src/cli/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
96
96
|
"@typescript-eslint/parser": "^7.18.0",
|
|
97
97
|
"eslint": "^8.0.0",
|
|
98
|
-
"moflo": "^4.10.
|
|
98
|
+
"moflo": "^4.10.15",
|
|
99
99
|
"tsx": "^4.21.0",
|
|
100
100
|
"typescript": "^5.9.3",
|
|
101
101
|
"vitest": "^4.0.0"
|