@dv.nghiem/flowdeck 0.3.2 → 0.3.4
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/README.md +18 -13
- package/dist/hooks/orchestrator-guard-hook.d.ts +4 -1
- package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
- package/dist/hooks/session-idle-hook.d.ts.map +1 -1
- package/dist/hooks/telemetry-hook.d.ts +14 -1
- package/dist/hooks/telemetry-hook.d.ts.map +1 -1
- package/dist/hooks/telemetry-hook.test.d.ts +2 -0
- package/dist/hooks/telemetry-hook.test.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +583 -240
- package/dist/tools/council.d.ts.map +1 -1
- package/dist/tools/delegate.d.ts.map +1 -1
- package/dist/tools/dispatch-routing.d.ts +6 -0
- package/dist/tools/dispatch-routing.d.ts.map +1 -0
- package/dist/tools/memory-status.d.ts +3 -0
- package/dist/tools/memory-status.d.ts.map +1 -0
- package/dist/tools/run-pipeline.d.ts.map +1 -1
- package/docs/commands.md +102 -9
- package/docs/installation.md +6 -17
- package/docs/intelligence.md +18 -33
- package/docs/optimization-baseline.md +21 -0
- package/docs/quick-start.md +44 -23
- package/docs/rules.md +9 -36
- package/docs/workflows.md +18 -17
- package/package.json +4 -2
- package/src/commands/fd-execute.md +192 -0
- package/src/commands/fd-new-feature.md +44 -157
- package/src/commands/fd-new-project.md +1 -2
- package/src/commands/fd-plan.md +1 -1
- package/src/commands/fd-suggest.md +84 -0
- package/src/commands/fd-verify.md +126 -0
- package/src/rules/README.md +10 -0
- package/src/rules/common/agent-orchestration.md +5 -5
- package/src/rules/common/coding-style.md +17 -0
- package/src/rules/typescript/patterns.md +1 -1
- package/src/skills/backend-patterns/SKILL.md +6 -0
- package/src/skills/clean-architecture/SKILL.md +6 -0
- package/src/skills/cqrs/SKILL.md +6 -0
- package/src/skills/ddd-architecture/SKILL.md +6 -0
- package/src/skills/event-driven-architecture/SKILL.md +6 -0
- package/src/skills/hexagonal-architecture/SKILL.md +6 -0
- package/src/skills/layered-architecture/SKILL.md +6 -0
- package/src/skills/postgres-patterns/SKILL.md +6 -0
- package/src/skills/saga-architecture/SKILL.md +6 -0
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
3
|
-
import { join as
|
|
2
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync24, existsSync as existsSync28 } from "fs";
|
|
3
|
+
import { join as join27, basename } from "path";
|
|
4
4
|
import { dirname as dirname4 } from "path";
|
|
5
5
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
6
|
|
|
@@ -509,6 +509,154 @@ var workspaceStateTool = tool3({
|
|
|
509
509
|
|
|
510
510
|
// src/tools/run-pipeline.ts
|
|
511
511
|
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
512
|
+
|
|
513
|
+
// src/services/agent-performance.ts
|
|
514
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
515
|
+
import { join as join5 } from "path";
|
|
516
|
+
function perfPath(dir) {
|
|
517
|
+
return join5(codebaseDir(dir), "AGENT_PERF.json");
|
|
518
|
+
}
|
|
519
|
+
function loadStore(dir) {
|
|
520
|
+
const p = perfPath(dir);
|
|
521
|
+
if (!existsSync5(p))
|
|
522
|
+
return { entries: [], updated_at: new Date().toISOString() };
|
|
523
|
+
try {
|
|
524
|
+
return JSON.parse(readFileSync5(p, "utf-8"));
|
|
525
|
+
} catch {
|
|
526
|
+
return { entries: [], updated_at: new Date().toISOString() };
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function saveStore(dir, store) {
|
|
530
|
+
const cd = codebaseDir(dir);
|
|
531
|
+
if (!existsSync5(cd))
|
|
532
|
+
mkdirSync2(cd, { recursive: true });
|
|
533
|
+
writeFileSync5(perfPath(dir), JSON.stringify(store, null, 2), "utf-8");
|
|
534
|
+
}
|
|
535
|
+
function makeKey(agent, model, task_type) {
|
|
536
|
+
return `${agent}::${model}::${task_type}`;
|
|
537
|
+
}
|
|
538
|
+
function recordRun(dir, agent, model, task_type, success, duration_ms, cost = 0) {
|
|
539
|
+
const store = loadStore(dir);
|
|
540
|
+
const key = makeKey(agent, model, task_type);
|
|
541
|
+
const existing = store.entries.find((e) => makeKey(e.agent, e.model, e.task_type) === key);
|
|
542
|
+
if (existing) {
|
|
543
|
+
existing.runs++;
|
|
544
|
+
if (success)
|
|
545
|
+
existing.successes++;
|
|
546
|
+
else
|
|
547
|
+
existing.failures++;
|
|
548
|
+
existing.total_duration_ms += duration_ms;
|
|
549
|
+
existing.total_cost += cost;
|
|
550
|
+
existing.last_run = new Date().toISOString();
|
|
551
|
+
existing.last_status = success ? "success" : "failure";
|
|
552
|
+
} else {
|
|
553
|
+
store.entries.push({
|
|
554
|
+
agent,
|
|
555
|
+
model,
|
|
556
|
+
task_type,
|
|
557
|
+
runs: 1,
|
|
558
|
+
successes: success ? 1 : 0,
|
|
559
|
+
failures: success ? 0 : 1,
|
|
560
|
+
total_duration_ms: duration_ms,
|
|
561
|
+
total_cost: cost,
|
|
562
|
+
last_run: new Date().toISOString(),
|
|
563
|
+
last_status: success ? "success" : "failure"
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
store.updated_at = new Date().toISOString();
|
|
567
|
+
saveStore(dir, store);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/services/model-router.ts
|
|
571
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
572
|
+
import { join as join6 } from "path";
|
|
573
|
+
var DEFAULT_ROUTING = {
|
|
574
|
+
planning: { primary: "claude-sonnet-4-5", temperature: 0.3, reasoning_effort: "medium" },
|
|
575
|
+
implementation: { primary: "claude-opus-4-5", fallback: "claude-sonnet-4-5", high_risk_override: "claude-opus-4-5", temperature: 0.2, reasoning_effort: "high" },
|
|
576
|
+
debugging: { primary: "claude-sonnet-4-5", high_risk_override: "claude-opus-4-5", temperature: 0.2, reasoning_effort: "high" },
|
|
577
|
+
review: { primary: "gemini-2.5-flash", fallback: "claude-haiku-4-5", temperature: 0.1, reasoning_effort: "medium" },
|
|
578
|
+
testing: { primary: "claude-haiku-4-5", fallback: "gemini-2.5-flash", temperature: 0.1, reasoning_effort: "low" },
|
|
579
|
+
documentation: { primary: "claude-sonnet-4-5", fallback: "gemini-2.5-flash", temperature: 0.3, reasoning_effort: "low" },
|
|
580
|
+
analysis: { primary: "claude-sonnet-4-5", temperature: 0.3, reasoning_effort: "medium" },
|
|
581
|
+
security: { primary: "claude-opus-4-5", high_risk_override: "claude-opus-4-5", temperature: 0.1, reasoning_effort: "high" },
|
|
582
|
+
orchestration: { primary: "claude-sonnet-4-5", temperature: 0.3, reasoning_effort: "medium" }
|
|
583
|
+
};
|
|
584
|
+
function getRouterConfig(dir) {
|
|
585
|
+
const p = join6(codebaseDir(dir), "MODEL_ROUTER.json");
|
|
586
|
+
if (!existsSync6(p))
|
|
587
|
+
return DEFAULT_ROUTING;
|
|
588
|
+
try {
|
|
589
|
+
const overrides = JSON.parse(readFileSync6(p, "utf-8"));
|
|
590
|
+
return { ...DEFAULT_ROUTING, ...overrides };
|
|
591
|
+
} catch {
|
|
592
|
+
return DEFAULT_ROUTING;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function routeModel(dir, task_type, risk_score = 100) {
|
|
596
|
+
const config = getRouterConfig(dir);
|
|
597
|
+
const route = config[task_type] ?? DEFAULT_ROUTING.implementation;
|
|
598
|
+
const is_high_risk = risk_score < 40;
|
|
599
|
+
let model = route.primary;
|
|
600
|
+
let is_override = false;
|
|
601
|
+
if (is_high_risk && route.high_risk_override) {
|
|
602
|
+
model = route.high_risk_override;
|
|
603
|
+
is_override = true;
|
|
604
|
+
}
|
|
605
|
+
return {
|
|
606
|
+
model,
|
|
607
|
+
temperature: route.temperature ?? 0.3,
|
|
608
|
+
reasoning_effort: route.reasoning_effort,
|
|
609
|
+
task_type,
|
|
610
|
+
is_high_risk,
|
|
611
|
+
is_override
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/tools/dispatch-routing.ts
|
|
616
|
+
function shouldRetry(promptRes) {
|
|
617
|
+
if (!promptRes)
|
|
618
|
+
return false;
|
|
619
|
+
const detail = promptRes.error?.detail;
|
|
620
|
+
if (isTransientError(detail))
|
|
621
|
+
return true;
|
|
622
|
+
const infoError = promptRes.data?.info?.error;
|
|
623
|
+
const text = typeof infoError === "string" ? infoError : JSON.stringify(infoError ?? "");
|
|
624
|
+
return isTransientError(text);
|
|
625
|
+
}
|
|
626
|
+
function isTransientError(text) {
|
|
627
|
+
if (!text)
|
|
628
|
+
return false;
|
|
629
|
+
const haystack = text.toLowerCase();
|
|
630
|
+
return haystack.includes("overload") || haystack.includes("rate limit") || haystack.includes("timeout") || haystack.includes("temporar") || haystack.includes("econnreset");
|
|
631
|
+
}
|
|
632
|
+
function normalizeTaskType(taskType, agent) {
|
|
633
|
+
const normalized = (taskType ?? "").trim().toLowerCase();
|
|
634
|
+
if (isTaskType(normalized))
|
|
635
|
+
return normalized;
|
|
636
|
+
const a = agent.toLowerCase();
|
|
637
|
+
if (a.includes("review"))
|
|
638
|
+
return "review";
|
|
639
|
+
if (a.includes("test"))
|
|
640
|
+
return "testing";
|
|
641
|
+
if (a.includes("debug"))
|
|
642
|
+
return "debugging";
|
|
643
|
+
if (a.includes("security"))
|
|
644
|
+
return "security";
|
|
645
|
+
if (a.includes("doc"))
|
|
646
|
+
return "documentation";
|
|
647
|
+
if (a.includes("architect") || a.includes("planner"))
|
|
648
|
+
return "planning";
|
|
649
|
+
if (a.includes("orchestrator") || a.includes("coordinator"))
|
|
650
|
+
return "orchestration";
|
|
651
|
+
if (a.includes("analyst") || a.includes("research"))
|
|
652
|
+
return "analysis";
|
|
653
|
+
return "implementation";
|
|
654
|
+
}
|
|
655
|
+
function isTaskType(value) {
|
|
656
|
+
return value === "planning" || value === "implementation" || value === "debugging" || value === "review" || value === "testing" || value === "documentation" || value === "analysis" || value === "security" || value === "orchestration";
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/tools/run-pipeline.ts
|
|
512
660
|
function extractText(parts) {
|
|
513
661
|
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
514
662
|
`);
|
|
@@ -519,16 +667,20 @@ function createRunPipelineTool(client) {
|
|
|
519
667
|
args: {
|
|
520
668
|
steps: tool4.schema.array(tool4.schema.object({
|
|
521
669
|
agent: tool4.schema.string(),
|
|
522
|
-
prompt: tool4.schema.string()
|
|
670
|
+
prompt: tool4.schema.string(),
|
|
671
|
+
task_type: tool4.schema.string().optional()
|
|
523
672
|
})),
|
|
524
673
|
initial_context: tool4.schema.string().optional(),
|
|
525
|
-
abort_on_failure: tool4.schema.boolean().optional().default(true)
|
|
674
|
+
abort_on_failure: tool4.schema.boolean().optional().default(true),
|
|
675
|
+
retry_attempts: tool4.schema.number().optional().default(1)
|
|
526
676
|
},
|
|
527
677
|
async execute(args, context) {
|
|
528
678
|
const startTime = Date.now();
|
|
529
679
|
const trace = [];
|
|
530
680
|
let carryContext = args.initial_context ?? "";
|
|
531
681
|
let aborted = false;
|
|
682
|
+
const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
|
|
683
|
+
const maxRetries = Math.max(0, Math.floor(retryAttempts));
|
|
532
684
|
let inflightChildId = null;
|
|
533
685
|
const abortHandler = () => {
|
|
534
686
|
if (inflightChildId) {
|
|
@@ -546,6 +698,8 @@ function createRunPipelineTool(client) {
|
|
|
546
698
|
break;
|
|
547
699
|
}
|
|
548
700
|
const stepStart = Date.now();
|
|
701
|
+
const taskType = normalizeTaskType(step.task_type, step.agent);
|
|
702
|
+
const routing = routeModel(context.directory, taskType);
|
|
549
703
|
const stepInput = carryContext ? `${carryContext}
|
|
550
704
|
|
|
551
705
|
---
|
|
@@ -557,28 +711,37 @@ ${step.prompt}` : step.prompt;
|
|
|
557
711
|
});
|
|
558
712
|
if (createRes.error || !createRes.data?.id) {
|
|
559
713
|
const errMsg = `Failed to create session: ${createRes.error?.detail ?? "unknown"}`;
|
|
560
|
-
trace.push({ agent: step.agent, input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
|
|
714
|
+
trace.push({ agent: step.agent, task_type: taskType, model: routing.model, input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
|
|
561
715
|
aborted = true;
|
|
562
716
|
break;
|
|
563
717
|
}
|
|
564
718
|
inflightChildId = createRes.data.id;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
719
|
+
let promptRes = null;
|
|
720
|
+
let retriesUsed = 0;
|
|
721
|
+
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
722
|
+
promptRes = await client.session.prompt({
|
|
723
|
+
path: { id: inflightChildId },
|
|
724
|
+
body: {
|
|
725
|
+
agent: step.agent,
|
|
726
|
+
model: routing.model,
|
|
727
|
+
parts: [{ type: "text", text: stepInput }],
|
|
728
|
+
tools: { question: false }
|
|
729
|
+
},
|
|
730
|
+
query: { directory: context.directory }
|
|
731
|
+
});
|
|
732
|
+
if (!shouldRetry(promptRes) || attempt === maxRetries)
|
|
733
|
+
break;
|
|
734
|
+
retriesUsed++;
|
|
735
|
+
}
|
|
574
736
|
inflightChildId = null;
|
|
575
737
|
if (context.abort.aborted) {
|
|
576
738
|
aborted = true;
|
|
577
739
|
break;
|
|
578
740
|
}
|
|
579
|
-
if (promptRes.error) {
|
|
580
|
-
const errMsg = `Prompt failed: ${promptRes
|
|
581
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, input: stepInput, output: errMsg
|
|
741
|
+
if (!promptRes || promptRes.error) {
|
|
742
|
+
const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
|
|
743
|
+
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: routing.model, input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
|
|
744
|
+
recordRun(context.directory, step.agent, routing.model, taskType, false, Date.now() - stepStart);
|
|
582
745
|
if (args.abort_on_failure) {
|
|
583
746
|
aborted = true;
|
|
584
747
|
break;
|
|
@@ -588,7 +751,8 @@ ${step.prompt}` : step.prompt;
|
|
|
588
751
|
const info = promptRes.data?.info;
|
|
589
752
|
if (info?.error) {
|
|
590
753
|
const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
|
|
591
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, input: stepInput, output: errMsg
|
|
754
|
+
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: routing.model, input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
|
|
755
|
+
recordRun(context.directory, step.agent, routing.model, taskType, false, Date.now() - stepStart);
|
|
592
756
|
if (args.abort_on_failure) {
|
|
593
757
|
aborted = true;
|
|
594
758
|
break;
|
|
@@ -596,7 +760,8 @@ ${step.prompt}` : step.prompt;
|
|
|
596
760
|
continue;
|
|
597
761
|
}
|
|
598
762
|
const output = extractText(promptRes.data?.parts ?? []);
|
|
599
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, input: stepInput, output: output || "(no text output)", duration_ms: Date.now() - stepStart, success: true });
|
|
763
|
+
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: routing.model, input: stepInput, output: output || "(no text output)", duration_ms: Date.now() - stepStart, success: true });
|
|
764
|
+
recordRun(context.directory, step.agent, routing.model, taskType, true, Date.now() - stepStart);
|
|
600
765
|
carryContext = output;
|
|
601
766
|
}
|
|
602
767
|
} finally {
|
|
@@ -623,10 +788,16 @@ function createDelegateTool(client) {
|
|
|
623
788
|
args: {
|
|
624
789
|
agent: tool5.schema.string(),
|
|
625
790
|
prompt: tool5.schema.string(),
|
|
626
|
-
context: tool5.schema.string().optional()
|
|
791
|
+
context: tool5.schema.string().optional(),
|
|
792
|
+
task_type: tool5.schema.string().optional(),
|
|
793
|
+
retry_attempts: tool5.schema.number().optional().default(1)
|
|
627
794
|
},
|
|
628
795
|
async execute(args, context) {
|
|
629
796
|
const startTime = Date.now();
|
|
797
|
+
const taskType = normalizeTaskType(args.task_type, args.agent);
|
|
798
|
+
const routing = routeModel(context.directory, taskType);
|
|
799
|
+
const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
|
|
800
|
+
const maxRetries = Math.max(0, Math.floor(retryAttempts));
|
|
630
801
|
const createRes = await client.session.create({
|
|
631
802
|
body: { parentID: context.sessionID, title: `${args.agent}-delegate` },
|
|
632
803
|
query: { directory: context.directory }
|
|
@@ -651,40 +822,60 @@ function createDelegateTool(client) {
|
|
|
651
822
|
---
|
|
652
823
|
|
|
653
824
|
${args.prompt}` : args.prompt;
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
825
|
+
let promptRes = null;
|
|
826
|
+
let retriesUsed = 0;
|
|
827
|
+
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
828
|
+
promptRes = await client.session.prompt({
|
|
829
|
+
path: { id: childId },
|
|
830
|
+
body: {
|
|
831
|
+
agent: args.agent,
|
|
832
|
+
model: routing.model,
|
|
833
|
+
parts: [{ type: "text", text: fullPrompt }],
|
|
834
|
+
tools: { question: false }
|
|
835
|
+
},
|
|
836
|
+
query: { directory: context.directory }
|
|
837
|
+
});
|
|
838
|
+
if (!shouldRetry(promptRes) || attempt === maxRetries)
|
|
839
|
+
break;
|
|
840
|
+
retriesUsed++;
|
|
841
|
+
}
|
|
842
|
+
if (!promptRes || promptRes.error) {
|
|
843
|
+
recordRun(context.directory, args.agent, routing.model, taskType, false, Date.now() - startTime);
|
|
664
844
|
return JSON.stringify({
|
|
665
845
|
agent: args.agent,
|
|
666
846
|
session_id: childId,
|
|
667
847
|
success: false,
|
|
668
|
-
error: `Prompt failed: ${promptRes
|
|
848
|
+
error: `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`,
|
|
849
|
+
task_type: taskType,
|
|
850
|
+
model: routing.model,
|
|
851
|
+
retries_used: retriesUsed,
|
|
669
852
|
duration_ms: Date.now() - startTime
|
|
670
853
|
});
|
|
671
854
|
}
|
|
672
855
|
const info = promptRes.data?.info;
|
|
673
856
|
if (info?.error) {
|
|
857
|
+
recordRun(context.directory, args.agent, routing.model, taskType, false, Date.now() - startTime);
|
|
674
858
|
return JSON.stringify({
|
|
675
859
|
agent: args.agent,
|
|
676
860
|
session_id: childId,
|
|
677
861
|
success: false,
|
|
678
862
|
error: `Agent error: ${JSON.stringify(info.error)}`,
|
|
863
|
+
task_type: taskType,
|
|
864
|
+
model: routing.model,
|
|
865
|
+
retries_used: retriesUsed,
|
|
679
866
|
duration_ms: Date.now() - startTime
|
|
680
867
|
});
|
|
681
868
|
}
|
|
682
869
|
const output = extractText2(promptRes.data?.parts ?? []);
|
|
870
|
+
recordRun(context.directory, args.agent, routing.model, taskType, true, Date.now() - startTime);
|
|
683
871
|
return JSON.stringify({
|
|
684
872
|
agent: args.agent,
|
|
685
873
|
session_id: childId,
|
|
686
874
|
success: true,
|
|
687
875
|
output: output || "(no text output)",
|
|
876
|
+
task_type: taskType,
|
|
877
|
+
model: routing.model,
|
|
878
|
+
retries_used: retriesUsed,
|
|
688
879
|
duration_ms: Date.now() - startTime
|
|
689
880
|
});
|
|
690
881
|
}
|
|
@@ -693,31 +884,31 @@ ${args.prompt}` : args.prompt;
|
|
|
693
884
|
|
|
694
885
|
// src/tools/repo-memory.ts
|
|
695
886
|
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
696
|
-
import { readFileSync as
|
|
697
|
-
import { join as
|
|
887
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
|
|
888
|
+
import { join as join7 } from "path";
|
|
698
889
|
var MEMORY_FILE = "MEMORY.json";
|
|
699
890
|
function memoryPath(directory) {
|
|
700
|
-
return
|
|
891
|
+
return join7(codebaseDir(directory), MEMORY_FILE);
|
|
701
892
|
}
|
|
702
893
|
function emptyMemory() {
|
|
703
894
|
return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
|
|
704
895
|
}
|
|
705
896
|
function readMemory(directory) {
|
|
706
897
|
const p = memoryPath(directory);
|
|
707
|
-
if (!
|
|
898
|
+
if (!existsSync7(p))
|
|
708
899
|
return emptyMemory();
|
|
709
900
|
try {
|
|
710
|
-
return JSON.parse(
|
|
901
|
+
return JSON.parse(readFileSync7(p, "utf-8"));
|
|
711
902
|
} catch {
|
|
712
903
|
return emptyMemory();
|
|
713
904
|
}
|
|
714
905
|
}
|
|
715
906
|
function writeMemory(directory, memory) {
|
|
716
907
|
const base = codebaseDir(directory);
|
|
717
|
-
if (!
|
|
718
|
-
|
|
908
|
+
if (!existsSync7(base))
|
|
909
|
+
mkdirSync3(base, { recursive: true });
|
|
719
910
|
memory.last_updated = new Date().toISOString();
|
|
720
|
-
|
|
911
|
+
writeFileSync6(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
|
|
721
912
|
}
|
|
722
913
|
var repoMemoryTool = tool6({
|
|
723
914
|
description: "Repo Memory Graph: read/write/query persistent architecture graph in .codebase/MEMORY.json (modules, dependencies, ownership, bug history, conventions)",
|
|
@@ -794,28 +985,28 @@ var repoMemoryTool = tool6({
|
|
|
794
985
|
|
|
795
986
|
// src/tools/failure-replay.ts
|
|
796
987
|
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
797
|
-
import { readFileSync as
|
|
798
|
-
import { join as
|
|
988
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
|
|
989
|
+
import { join as join8 } from "path";
|
|
799
990
|
var FAILURES_FILE = "FAILURES.json";
|
|
800
991
|
function failuresPath(directory) {
|
|
801
|
-
return
|
|
992
|
+
return join8(codebaseDir(directory), FAILURES_FILE);
|
|
802
993
|
}
|
|
803
994
|
function readStore(directory) {
|
|
804
995
|
const p = failuresPath(directory);
|
|
805
|
-
if (!
|
|
996
|
+
if (!existsSync8(p))
|
|
806
997
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
807
998
|
try {
|
|
808
|
-
return JSON.parse(
|
|
999
|
+
return JSON.parse(readFileSync8(p, "utf-8"));
|
|
809
1000
|
} catch {
|
|
810
1001
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
811
1002
|
}
|
|
812
1003
|
}
|
|
813
1004
|
function writeStore(directory, store) {
|
|
814
1005
|
const base = codebaseDir(directory);
|
|
815
|
-
if (!
|
|
816
|
-
|
|
1006
|
+
if (!existsSync8(base))
|
|
1007
|
+
mkdirSync4(base, { recursive: true });
|
|
817
1008
|
store.last_updated = new Date().toISOString();
|
|
818
|
-
|
|
1009
|
+
writeFileSync7(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
819
1010
|
}
|
|
820
1011
|
var failureReplayTool = tool7({
|
|
821
1012
|
description: "Failure Replay Engine: record and query past failures (reverted commits, failed deployments, flaky tests, bug fixes) in .codebase/FAILURES.json so the agent avoids repeating mistakes",
|
|
@@ -899,17 +1090,17 @@ var failureReplayTool = tool7({
|
|
|
899
1090
|
|
|
900
1091
|
// src/tools/decision-trace.ts
|
|
901
1092
|
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
902
|
-
import { readFileSync as
|
|
903
|
-
import { join as
|
|
1093
|
+
import { readFileSync as readFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync5, appendFileSync } from "fs";
|
|
1094
|
+
import { join as join9 } from "path";
|
|
904
1095
|
var DECISIONS_FILE = "DECISIONS.jsonl";
|
|
905
1096
|
function decisionsPath(directory) {
|
|
906
|
-
return
|
|
1097
|
+
return join9(codebaseDir(directory), DECISIONS_FILE);
|
|
907
1098
|
}
|
|
908
1099
|
function readDecisions(directory) {
|
|
909
1100
|
const p = decisionsPath(directory);
|
|
910
|
-
if (!
|
|
1101
|
+
if (!existsSync9(p))
|
|
911
1102
|
return [];
|
|
912
|
-
return
|
|
1103
|
+
return readFileSync9(p, "utf-8").split(`
|
|
913
1104
|
`).filter((l) => l.trim()).map((l) => {
|
|
914
1105
|
try {
|
|
915
1106
|
return JSON.parse(l);
|
|
@@ -949,8 +1140,8 @@ var decisionTraceTool = tool8({
|
|
|
949
1140
|
case "record": {
|
|
950
1141
|
if (!args.entry)
|
|
951
1142
|
return JSON.stringify({ error: "entry required" });
|
|
952
|
-
if (!
|
|
953
|
-
|
|
1143
|
+
if (!existsSync9(base))
|
|
1144
|
+
mkdirSync5(base, { recursive: true });
|
|
954
1145
|
const entry = { ...args.entry, timestamp: new Date().toISOString() };
|
|
955
1146
|
appendFileSync(decisionsPath(dir), JSON.stringify(entry) + `
|
|
956
1147
|
`, "utf-8");
|
|
@@ -984,28 +1175,28 @@ var decisionTraceTool = tool8({
|
|
|
984
1175
|
|
|
985
1176
|
// src/tools/volatility-map.ts
|
|
986
1177
|
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
987
|
-
import { readFileSync as
|
|
988
|
-
import { join as
|
|
1178
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync9, existsSync as existsSync10, mkdirSync as mkdirSync6 } from "fs";
|
|
1179
|
+
import { join as join10 } from "path";
|
|
989
1180
|
var VOLATILITY_FILE = "VOLATILITY.json";
|
|
990
1181
|
function volatilityPath(directory) {
|
|
991
|
-
return
|
|
1182
|
+
return join10(codebaseDir(directory), VOLATILITY_FILE);
|
|
992
1183
|
}
|
|
993
1184
|
function readStore2(directory) {
|
|
994
1185
|
const p = volatilityPath(directory);
|
|
995
|
-
if (!
|
|
1186
|
+
if (!existsSync10(p))
|
|
996
1187
|
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
997
1188
|
try {
|
|
998
|
-
return JSON.parse(
|
|
1189
|
+
return JSON.parse(readFileSync10(p, "utf-8"));
|
|
999
1190
|
} catch {
|
|
1000
1191
|
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
1001
1192
|
}
|
|
1002
1193
|
}
|
|
1003
1194
|
function writeStore2(directory, store) {
|
|
1004
1195
|
const base = codebaseDir(directory);
|
|
1005
|
-
if (!
|
|
1006
|
-
|
|
1196
|
+
if (!existsSync10(base))
|
|
1197
|
+
mkdirSync6(base, { recursive: true });
|
|
1007
1198
|
store.last_updated = new Date().toISOString();
|
|
1008
|
-
|
|
1199
|
+
writeFileSync9(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1009
1200
|
}
|
|
1010
1201
|
function stabilityLabel(churn, hotfixes, todos) {
|
|
1011
1202
|
const score = churn + hotfixes * 10 + todos * 2;
|
|
@@ -1092,28 +1283,28 @@ var volatilityMapTool = tool9({
|
|
|
1092
1283
|
|
|
1093
1284
|
// src/tools/policy-engine.ts
|
|
1094
1285
|
import { tool as tool10 } from "@opencode-ai/plugin";
|
|
1095
|
-
import { readFileSync as
|
|
1096
|
-
import { join as
|
|
1286
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync10, existsSync as existsSync11, mkdirSync as mkdirSync7 } from "fs";
|
|
1287
|
+
import { join as join11 } from "path";
|
|
1097
1288
|
var POLICIES_FILE = "POLICIES.json";
|
|
1098
1289
|
function policiesPath(directory) {
|
|
1099
|
-
return
|
|
1290
|
+
return join11(codebaseDir(directory), POLICIES_FILE);
|
|
1100
1291
|
}
|
|
1101
1292
|
function readStore3(directory) {
|
|
1102
1293
|
const p = policiesPath(directory);
|
|
1103
|
-
if (!
|
|
1294
|
+
if (!existsSync11(p))
|
|
1104
1295
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1105
1296
|
try {
|
|
1106
|
-
return JSON.parse(
|
|
1297
|
+
return JSON.parse(readFileSync11(p, "utf-8"));
|
|
1107
1298
|
} catch {
|
|
1108
1299
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1109
1300
|
}
|
|
1110
1301
|
}
|
|
1111
1302
|
function writeStore3(directory, store) {
|
|
1112
1303
|
const base = codebaseDir(directory);
|
|
1113
|
-
if (!
|
|
1114
|
-
|
|
1304
|
+
if (!existsSync11(base))
|
|
1305
|
+
mkdirSync7(base, { recursive: true });
|
|
1115
1306
|
store.last_updated = new Date().toISOString();
|
|
1116
|
-
|
|
1307
|
+
writeFileSync10(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1117
1308
|
}
|
|
1118
1309
|
var policyEngineTool = tool10({
|
|
1119
1310
|
description: "Self-Healing Policy Engine: manage .codebase/POLICIES.json — add, list, query, toggle, and record violations of editing policies learned from past failures",
|
|
@@ -1195,7 +1386,7 @@ var policyEngineTool = tool10({
|
|
|
1195
1386
|
|
|
1196
1387
|
// src/tools/hash-edit.ts
|
|
1197
1388
|
import { tool as tool11 } from "@opencode-ai/plugin";
|
|
1198
|
-
import { readFileSync as
|
|
1389
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
|
|
1199
1390
|
import { createHash } from "crypto";
|
|
1200
1391
|
var hashEditTool = tool11({
|
|
1201
1392
|
description: "Reliable file editing with content verification. Takes a target content, its expected MD5 hash, and replacement content. Only applies if the hash matches, preventing edits on stale file versions.",
|
|
@@ -1209,7 +1400,7 @@ var hashEditTool = tool11({
|
|
|
1209
1400
|
const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
|
|
1210
1401
|
let content;
|
|
1211
1402
|
try {
|
|
1212
|
-
content =
|
|
1403
|
+
content = readFileSync12(fullPath, "utf-8");
|
|
1213
1404
|
} catch (e) {
|
|
1214
1405
|
return `Error: Could not read file ${args.filePath}`;
|
|
1215
1406
|
}
|
|
@@ -1223,13 +1414,15 @@ var hashEditTool = tool11({
|
|
|
1223
1414
|
}
|
|
1224
1415
|
}
|
|
1225
1416
|
const newContent = content.replace(args.targetContent, args.replacementContent);
|
|
1226
|
-
|
|
1417
|
+
writeFileSync11(fullPath, newContent, "utf-8");
|
|
1227
1418
|
return `Successfully updated ${args.filePath} using hash-anchored edit.`;
|
|
1228
1419
|
}
|
|
1229
1420
|
});
|
|
1230
1421
|
|
|
1231
1422
|
// src/tools/council.ts
|
|
1232
1423
|
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
1424
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync8 } from "fs";
|
|
1425
|
+
import { join as join12 } from "path";
|
|
1233
1426
|
function createCouncilTool(client) {
|
|
1234
1427
|
return tool12({
|
|
1235
1428
|
description: "Run an ensemble of agents (Council) on the same task to reach consensus or compare approaches. Runs 3 specialized agents in parallel and returns their synthesized outputs.",
|
|
@@ -1284,16 +1477,34 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
|
|
|
1284
1477
|
},
|
|
1285
1478
|
query: { directory: context.directory }
|
|
1286
1479
|
});
|
|
1287
|
-
|
|
1480
|
+
const synthesis = (finalRes.data?.parts ?? []).filter((p) => p.type === "text").map((p) => p.text).join(`
|
|
1288
1481
|
`);
|
|
1482
|
+
persistCouncilResult(context.directory, {
|
|
1483
|
+
task: args.task,
|
|
1484
|
+
agents,
|
|
1485
|
+
results,
|
|
1486
|
+
synthesis,
|
|
1487
|
+
created_at: new Date().toISOString()
|
|
1488
|
+
});
|
|
1489
|
+
return synthesis;
|
|
1289
1490
|
}
|
|
1290
1491
|
});
|
|
1291
1492
|
}
|
|
1493
|
+
function persistCouncilResult(directory, payload) {
|
|
1494
|
+
try {
|
|
1495
|
+
const base = codebaseDir(directory);
|
|
1496
|
+
if (!existsSync12(base))
|
|
1497
|
+
mkdirSync8(base, { recursive: true });
|
|
1498
|
+
const path = join12(base, "COUNCILS.jsonl");
|
|
1499
|
+
appendFileSync2(path, JSON.stringify(payload) + `
|
|
1500
|
+
`, "utf-8");
|
|
1501
|
+
} catch {}
|
|
1502
|
+
}
|
|
1292
1503
|
|
|
1293
1504
|
// src/tools/context-generator.ts
|
|
1294
1505
|
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
1295
|
-
import { writeFileSync as
|
|
1296
|
-
import { join as
|
|
1506
|
+
import { writeFileSync as writeFileSync12, existsSync as existsSync13, readFileSync as readFileSync13, readdirSync as readdirSync2, statSync } from "fs";
|
|
1507
|
+
import { join as join13 } from "path";
|
|
1297
1508
|
var contextGeneratorTool = tool13({
|
|
1298
1509
|
description: "Auto-generate or update hierarchical context files (AGENTS.md, CLAUDE.md) throughout the project. These files provide critical grounding for AI agents.",
|
|
1299
1510
|
args: {
|
|
@@ -1302,20 +1513,20 @@ var contextGeneratorTool = tool13({
|
|
|
1302
1513
|
},
|
|
1303
1514
|
async execute(args, context) {
|
|
1304
1515
|
const root = context.directory;
|
|
1305
|
-
const target = args.targetDir ?
|
|
1306
|
-
if (!
|
|
1516
|
+
const target = args.targetDir ? join13(root, args.targetDir) : root;
|
|
1517
|
+
if (!existsSync13(target)) {
|
|
1307
1518
|
return `Error: Directory ${target} does not exist.`;
|
|
1308
1519
|
}
|
|
1309
|
-
const agentsMdPath =
|
|
1310
|
-
if (
|
|
1520
|
+
const agentsMdPath = join13(target, "AGENTS.md");
|
|
1521
|
+
if (existsSync13(agentsMdPath) && !args.force) {
|
|
1311
1522
|
return `AGENTS.md already exists in ${target}. Use force: true to overwrite.`;
|
|
1312
1523
|
}
|
|
1313
|
-
const pkgPath =
|
|
1524
|
+
const pkgPath = join13(root, "package.json");
|
|
1314
1525
|
let projectName = "Project";
|
|
1315
1526
|
let techStack = "Unknown";
|
|
1316
|
-
if (
|
|
1527
|
+
if (existsSync13(pkgPath)) {
|
|
1317
1528
|
try {
|
|
1318
|
-
const pkg = JSON.parse(
|
|
1529
|
+
const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
|
|
1319
1530
|
projectName = pkg.name || projectName;
|
|
1320
1531
|
techStack = Object.keys(pkg.dependencies || {}).slice(0, 5).join(", ");
|
|
1321
1532
|
} catch {}
|
|
@@ -1333,7 +1544,7 @@ var contextGeneratorTool = tool13({
|
|
|
1333
1544
|
|
|
1334
1545
|
## Directory Map
|
|
1335
1546
|
${readdirSync2(target).slice(0, 10).map((f) => {
|
|
1336
|
-
const s = statSync(
|
|
1547
|
+
const s = statSync(join13(target, f));
|
|
1337
1548
|
return `- \`${f}\`${s.isDirectory() ? "/" : ""} : [Description]`;
|
|
1338
1549
|
}).join(`
|
|
1339
1550
|
`)}
|
|
@@ -1341,17 +1552,17 @@ ${readdirSync2(target).slice(0, 10).map((f) => {
|
|
|
1341
1552
|
---
|
|
1342
1553
|
Generated by FlowDeck Context Generator.
|
|
1343
1554
|
`;
|
|
1344
|
-
|
|
1555
|
+
writeFileSync12(agentsMdPath, content, "utf-8");
|
|
1345
1556
|
return `Successfully generated AGENTS.md in ${target}.`;
|
|
1346
1557
|
}
|
|
1347
1558
|
});
|
|
1348
1559
|
|
|
1349
1560
|
// src/tools/create-skill.ts
|
|
1350
1561
|
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
1351
|
-
import { mkdirSync as
|
|
1352
|
-
import { join as
|
|
1562
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync13, existsSync as existsSync14 } from "fs";
|
|
1563
|
+
import { join as join14, dirname as dirname3 } from "path";
|
|
1353
1564
|
import { fileURLToPath } from "url";
|
|
1354
|
-
var SKILLS_DIR =
|
|
1565
|
+
var SKILLS_DIR = join14(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
|
|
1355
1566
|
var createSkillTool = tool14({
|
|
1356
1567
|
description: "Create a new reusable skill in the FlowDeck skill library (src/skills/). " + "Use this when you discover a repeatable pattern, solve a novel problem with human guidance, " + "or want to capture domain knowledge for future sessions.",
|
|
1357
1568
|
args: {
|
|
@@ -1361,9 +1572,9 @@ var createSkillTool = tool14({
|
|
|
1361
1572
|
tags: tool14.schema.array(tool14.schema.string()).optional().describe("Optional tags for categorisation, e.g. ['performance', 'typescript']")
|
|
1362
1573
|
},
|
|
1363
1574
|
async execute(args) {
|
|
1364
|
-
const skillDir =
|
|
1365
|
-
const skillFile =
|
|
1366
|
-
if (
|
|
1575
|
+
const skillDir = join14(SKILLS_DIR, args.name);
|
|
1576
|
+
const skillFile = join14(skillDir, "SKILL.md");
|
|
1577
|
+
if (existsSync14(skillFile)) {
|
|
1367
1578
|
return `Skill '${args.name}' already exists at ${skillFile}.
|
|
1368
1579
|
` + `Use a different name or delete the existing skill directory first.`;
|
|
1369
1580
|
}
|
|
@@ -1378,8 +1589,8 @@ origin: FlowDeck (self-learned)${tagLine}
|
|
|
1378
1589
|
`;
|
|
1379
1590
|
const fullContent = frontmatter + args.content.trimStart();
|
|
1380
1591
|
try {
|
|
1381
|
-
|
|
1382
|
-
|
|
1592
|
+
mkdirSync9(skillDir, { recursive: true });
|
|
1593
|
+
writeFileSync13(skillFile, fullContent, "utf-8");
|
|
1383
1594
|
return `✓ Skill '${args.name}' created at ${skillFile}
|
|
1384
1595
|
|
|
1385
1596
|
` + `The skill is now part of the FlowDeck library. Restart OpenCode to load it into the active session.`;
|
|
@@ -1391,8 +1602,8 @@ origin: FlowDeck (self-learned)${tagLine}
|
|
|
1391
1602
|
|
|
1392
1603
|
// src/tools/reflect.ts
|
|
1393
1604
|
import { tool as tool15 } from "@opencode-ai/plugin";
|
|
1394
|
-
import { existsSync as
|
|
1395
|
-
import { join as
|
|
1605
|
+
import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
|
|
1606
|
+
import { join as join15 } from "path";
|
|
1396
1607
|
var MAX_ARTIFACT_BYTES = 4000;
|
|
1397
1608
|
function tail(text, maxBytes) {
|
|
1398
1609
|
if (text.length <= maxBytes)
|
|
@@ -1421,11 +1632,11 @@ var reflectTool = tool15({
|
|
|
1421
1632
|
];
|
|
1422
1633
|
let found = 0;
|
|
1423
1634
|
for (const [rel, label] of ARTIFACT_PATHS) {
|
|
1424
|
-
const full =
|
|
1425
|
-
if (!
|
|
1635
|
+
const full = join15(root, rel);
|
|
1636
|
+
if (!existsSync15(full))
|
|
1426
1637
|
continue;
|
|
1427
1638
|
try {
|
|
1428
|
-
const raw =
|
|
1639
|
+
const raw = readFileSync14(full, "utf-8").trim();
|
|
1429
1640
|
if (!raw)
|
|
1430
1641
|
continue;
|
|
1431
1642
|
const count = raw.split(`
|
|
@@ -1449,14 +1660,14 @@ import { tool as tool16 } from "@opencode-ai/plugin";
|
|
|
1449
1660
|
|
|
1450
1661
|
// src/services/memory-store.ts
|
|
1451
1662
|
import { Database } from "bun:sqlite";
|
|
1452
|
-
import { existsSync as
|
|
1453
|
-
import { join as
|
|
1663
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync10 } from "fs";
|
|
1664
|
+
import { join as join16 } from "path";
|
|
1454
1665
|
import { homedir } from "os";
|
|
1455
|
-
var MEMORY_DIR =
|
|
1456
|
-
var DB_PATH =
|
|
1666
|
+
var MEMORY_DIR = join16(homedir(), ".flowdeck-memory");
|
|
1667
|
+
var DB_PATH = join16(MEMORY_DIR, "memory.db");
|
|
1457
1668
|
function ensureDir() {
|
|
1458
|
-
if (!
|
|
1459
|
-
|
|
1669
|
+
if (!existsSync16(MEMORY_DIR)) {
|
|
1670
|
+
mkdirSync10(MEMORY_DIR, { recursive: true });
|
|
1460
1671
|
}
|
|
1461
1672
|
}
|
|
1462
1673
|
var db = null;
|
|
@@ -1731,6 +1942,76 @@ var memorySearchTool = tool16({
|
|
|
1731
1942
|
}
|
|
1732
1943
|
});
|
|
1733
1944
|
|
|
1945
|
+
// src/tools/memory-status.ts
|
|
1946
|
+
import { tool as tool17 } from "@opencode-ai/plugin";
|
|
1947
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
1948
|
+
import { existsSync as existsSync17 } from "fs";
|
|
1949
|
+
import { join as join17 } from "path";
|
|
1950
|
+
import { homedir as homedir2 } from "os";
|
|
1951
|
+
var DB_PATH2 = join17(homedir2(), ".flowdeck-memory", "memory.db");
|
|
1952
|
+
var memoryStatusTool = tool17({
|
|
1953
|
+
description: "Check FlowDeck memory database status, statistics, and recent sessions",
|
|
1954
|
+
args: {},
|
|
1955
|
+
async execute(_args, _context) {
|
|
1956
|
+
try {
|
|
1957
|
+
const exists = existsSync17(DB_PATH2);
|
|
1958
|
+
const result = {
|
|
1959
|
+
database_exists: exists,
|
|
1960
|
+
path: DB_PATH2,
|
|
1961
|
+
status: exists ? "ACTIVE" : "NOT_INITIALIZED",
|
|
1962
|
+
statistics: null
|
|
1963
|
+
};
|
|
1964
|
+
if (exists) {
|
|
1965
|
+
try {
|
|
1966
|
+
const db2 = new Database2(DB_PATH2);
|
|
1967
|
+
const sessions = db2.prepare("SELECT COUNT(*) as count FROM sessions").get();
|
|
1968
|
+
const observations = db2.prepare("SELECT COUNT(*) as count FROM observations").get();
|
|
1969
|
+
const summaries = db2.prepare("SELECT COUNT(*) as count FROM summaries").get();
|
|
1970
|
+
const recentSessions = db2.prepare(`
|
|
1971
|
+
SELECT
|
|
1972
|
+
id,
|
|
1973
|
+
content_session_id,
|
|
1974
|
+
project,
|
|
1975
|
+
directory,
|
|
1976
|
+
created_at,
|
|
1977
|
+
last_active_at,
|
|
1978
|
+
prompt_count
|
|
1979
|
+
FROM sessions
|
|
1980
|
+
ORDER BY last_active_at DESC
|
|
1981
|
+
LIMIT 5
|
|
1982
|
+
`).all();
|
|
1983
|
+
result.statistics = {
|
|
1984
|
+
sessions: sessions.count,
|
|
1985
|
+
observations: observations.count,
|
|
1986
|
+
summaries: summaries.count,
|
|
1987
|
+
recent_sessions: recentSessions.map((s) => {
|
|
1988
|
+
const obsCount = db2.prepare("SELECT COUNT(*) as count FROM observations WHERE session_id = ?").get(s.id);
|
|
1989
|
+
return {
|
|
1990
|
+
project: s.project,
|
|
1991
|
+
directory: s.directory,
|
|
1992
|
+
observations_in_session: obsCount.count,
|
|
1993
|
+
last_active: s.last_active_at,
|
|
1994
|
+
prompt_count: s.prompt_count
|
|
1995
|
+
};
|
|
1996
|
+
})
|
|
1997
|
+
};
|
|
1998
|
+
db2.close();
|
|
1999
|
+
} catch (err) {
|
|
2000
|
+
result.status = "ERROR";
|
|
2001
|
+
result.statistics = { error: String(err) };
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
return JSON.stringify(result, null, 2);
|
|
2005
|
+
} catch (err) {
|
|
2006
|
+
return JSON.stringify({
|
|
2007
|
+
status: "ERROR",
|
|
2008
|
+
error: String(err),
|
|
2009
|
+
path: DB_PATH2
|
|
2010
|
+
}, null, 2);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
});
|
|
2014
|
+
|
|
1734
2015
|
// src/hooks/memory-hook.ts
|
|
1735
2016
|
var MAX_TOOL_RESPONSE = 1e4;
|
|
1736
2017
|
var MAX_PROMPT_LENGTH = 2000;
|
|
@@ -1830,15 +2111,15 @@ var memoryHook = {
|
|
|
1830
2111
|
};
|
|
1831
2112
|
|
|
1832
2113
|
// src/hooks/guard-rails.ts
|
|
1833
|
-
import { existsSync as
|
|
1834
|
-
import { join as
|
|
2114
|
+
import { existsSync as existsSync18, readFileSync as readFileSync15 } from "fs";
|
|
2115
|
+
import { join as join18 } from "path";
|
|
1835
2116
|
var PLANNING_DIR2 = ".planning";
|
|
1836
2117
|
var CONFIG_FILE = "config.json";
|
|
1837
2118
|
var STATE_FILE2 = "STATE.md";
|
|
1838
2119
|
function resolveExecutionMode(configPath, trustScore, volatility) {
|
|
1839
|
-
if (
|
|
2120
|
+
if (existsSync18(configPath)) {
|
|
1840
2121
|
try {
|
|
1841
|
-
const config = JSON.parse(
|
|
2122
|
+
const config = JSON.parse(readFileSync15(configPath, "utf-8"));
|
|
1842
2123
|
if (config.execution_mode === "review-only")
|
|
1843
2124
|
return "review-only";
|
|
1844
2125
|
if (config.execution_mode === "guarded")
|
|
@@ -1892,22 +2173,22 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
1892
2173
|
if (!ENABLED)
|
|
1893
2174
|
return;
|
|
1894
2175
|
const dir = ctx.directory;
|
|
1895
|
-
const planningDirPath =
|
|
2176
|
+
const planningDirPath = join18(dir, PLANNING_DIR2);
|
|
1896
2177
|
const codebaseDirectory = codebaseDir(dir);
|
|
1897
|
-
const configPath =
|
|
1898
|
-
const statePath2 =
|
|
2178
|
+
const configPath = join18(planningDirPath, CONFIG_FILE);
|
|
2179
|
+
const statePath2 = join18(planningDirPath, STATE_FILE2);
|
|
1899
2180
|
const workspaceRoot = findWorkspaceRoot(dir);
|
|
1900
2181
|
if (workspaceRoot && dir !== workspaceRoot) {
|
|
1901
2182
|
const config = getWorkspaceConfig(dir);
|
|
1902
|
-
if (config && config.workspace_mode === "shared" && !
|
|
2183
|
+
if (config && config.workspace_mode === "shared" && !existsSync18(planningDirPath)) {
|
|
1903
2184
|
const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
|
|
1904
2185
|
throw new Error(`[flowdeck] BLOCK: ${msg}`);
|
|
1905
2186
|
}
|
|
1906
2187
|
}
|
|
1907
2188
|
if (input.tool === "write" || input.tool === "edit") {
|
|
1908
|
-
if (!
|
|
2189
|
+
if (!existsSync18(planningDirPath))
|
|
1909
2190
|
return;
|
|
1910
|
-
if (!
|
|
2191
|
+
if (!existsSync18(codebaseDirectory)) {
|
|
1911
2192
|
throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /map-codebase to map the codebase.`);
|
|
1912
2193
|
}
|
|
1913
2194
|
const execMode = resolveExecutionMode(configPath, null);
|
|
@@ -1940,9 +2221,9 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
1940
2221
|
}
|
|
1941
2222
|
}
|
|
1942
2223
|
function effectiveSeverity(configPath, statePath2) {
|
|
1943
|
-
if (
|
|
2224
|
+
if (existsSync18(configPath)) {
|
|
1944
2225
|
try {
|
|
1945
|
-
const configContent =
|
|
2226
|
+
const configContent = readFileSync15(configPath, "utf-8");
|
|
1946
2227
|
const config = JSON.parse(configContent);
|
|
1947
2228
|
if (config.guard_enforcement === "warn")
|
|
1948
2229
|
return "warn";
|
|
@@ -1958,10 +2239,10 @@ function getEffectiveSeverity(configPath, statePath2) {
|
|
|
1958
2239
|
return effectiveSeverity(configPath, statePath2);
|
|
1959
2240
|
}
|
|
1960
2241
|
function getPlanConfirmed(statePath2) {
|
|
1961
|
-
if (!
|
|
2242
|
+
if (!existsSync18(statePath2))
|
|
1962
2243
|
return false;
|
|
1963
2244
|
try {
|
|
1964
|
-
const content =
|
|
2245
|
+
const content = readFileSync15(statePath2, "utf-8");
|
|
1965
2246
|
const match = content.match(/plan_confirmed:\s*(true|false)/i);
|
|
1966
2247
|
return match ? match[1].toLowerCase() === "true" : false;
|
|
1967
2248
|
} catch {
|
|
@@ -1969,32 +2250,32 @@ function getPlanConfirmed(statePath2) {
|
|
|
1969
2250
|
}
|
|
1970
2251
|
}
|
|
1971
2252
|
function getWarningMessage(planningDir2) {
|
|
1972
|
-
if (!
|
|
2253
|
+
if (!existsSync18(join18(planningDir2, STATE_FILE2))) {
|
|
1973
2254
|
return "No .planning/ found. Run /new-project first.";
|
|
1974
2255
|
}
|
|
1975
2256
|
return "Plan not confirmed. Run /plan and confirm to enable execution.";
|
|
1976
2257
|
}
|
|
1977
2258
|
function getBlockMessage(planningDir2) {
|
|
1978
|
-
if (!
|
|
2259
|
+
if (!existsSync18(join18(planningDir2, STATE_FILE2))) {
|
|
1979
2260
|
return "No .planning/ found. Run /new-project first.";
|
|
1980
2261
|
}
|
|
1981
2262
|
return "Plan not confirmed. Run /plan and confirm to enable execution.";
|
|
1982
2263
|
}
|
|
1983
2264
|
|
|
1984
2265
|
// src/hooks/tool-guard.ts
|
|
1985
|
-
import { existsSync as
|
|
1986
|
-
import { join as
|
|
2266
|
+
import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
|
|
2267
|
+
import { join as join19 } from "path";
|
|
1987
2268
|
var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
|
|
1988
2269
|
var BLOCKED_PATTERNS = {
|
|
1989
2270
|
read: [".env", ".pem", ".key", ".secret"],
|
|
1990
2271
|
write: ["node_modules"],
|
|
1991
2272
|
bash: ["rm -rf"]
|
|
1992
2273
|
};
|
|
1993
|
-
function isBlocked(
|
|
1994
|
-
const patterns = BLOCKED_PATTERNS[
|
|
2274
|
+
function isBlocked(tool18, args) {
|
|
2275
|
+
const patterns = BLOCKED_PATTERNS[tool18];
|
|
1995
2276
|
if (!patterns)
|
|
1996
2277
|
return null;
|
|
1997
|
-
if (
|
|
2278
|
+
if (tool18 === "bash") {
|
|
1998
2279
|
const cmd = args.command;
|
|
1999
2280
|
if (!cmd)
|
|
2000
2281
|
return null;
|
|
@@ -2005,7 +2286,7 @@ function isBlocked(tool17, args) {
|
|
|
2005
2286
|
}
|
|
2006
2287
|
return null;
|
|
2007
2288
|
}
|
|
2008
|
-
if (
|
|
2289
|
+
if (tool18 === "read") {
|
|
2009
2290
|
const filePath = args.filePath;
|
|
2010
2291
|
if (!filePath)
|
|
2011
2292
|
return null;
|
|
@@ -2016,7 +2297,7 @@ function isBlocked(tool17, args) {
|
|
|
2016
2297
|
}
|
|
2017
2298
|
return null;
|
|
2018
2299
|
}
|
|
2019
|
-
if (
|
|
2300
|
+
if (tool18 === "write") {
|
|
2020
2301
|
const filePath = args.filePath;
|
|
2021
2302
|
if (!filePath)
|
|
2022
2303
|
return null;
|
|
@@ -2030,11 +2311,11 @@ function isBlocked(tool17, args) {
|
|
|
2030
2311
|
return null;
|
|
2031
2312
|
}
|
|
2032
2313
|
function checkArchConstraint(directory, filePath) {
|
|
2033
|
-
const constraintsPath =
|
|
2034
|
-
if (!
|
|
2314
|
+
const constraintsPath = join19(codebaseDir(directory), "CONSTRAINTS.md");
|
|
2315
|
+
if (!existsSync19(constraintsPath))
|
|
2035
2316
|
return null;
|
|
2036
2317
|
try {
|
|
2037
|
-
const content =
|
|
2318
|
+
const content = readFileSync16(constraintsPath, "utf-8");
|
|
2038
2319
|
const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
|
|
2039
2320
|
if (!match)
|
|
2040
2321
|
return null;
|
|
@@ -2081,18 +2362,18 @@ async function toolGuardHook(ctx, input, output) {
|
|
|
2081
2362
|
}
|
|
2082
2363
|
|
|
2083
2364
|
// src/hooks/session-start.ts
|
|
2084
|
-
import { existsSync as
|
|
2365
|
+
import { existsSync as existsSync20, readFileSync as readFileSync17 } from "fs";
|
|
2085
2366
|
async function sessionStartHook(ctx) {
|
|
2086
2367
|
const planningDir2 = ctx.directory + "/.planning";
|
|
2087
2368
|
const codebaseDirectory = codebaseDir(ctx.directory);
|
|
2088
2369
|
const workspaceRoot = findWorkspaceRoot(ctx.directory);
|
|
2089
2370
|
const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
|
|
2090
|
-
if (!
|
|
2371
|
+
if (!existsSync20(planningDir2)) {
|
|
2091
2372
|
return {
|
|
2092
2373
|
flowdeck_phase: null,
|
|
2093
2374
|
flowdeck_status: "no_plan",
|
|
2094
2375
|
flowdeck_warning: "Run /new-project or /map-codebase to initialize.",
|
|
2095
|
-
flowdeck_has_codebase:
|
|
2376
|
+
flowdeck_has_codebase: existsSync20(codebaseDirectory),
|
|
2096
2377
|
...workspaceRoot && config?.sub_repos ? {
|
|
2097
2378
|
flowdeck_workspace_root: workspaceRoot,
|
|
2098
2379
|
flowdeck_sub_repos: config.sub_repos,
|
|
@@ -2103,7 +2384,7 @@ async function sessionStartHook(ctx) {
|
|
|
2103
2384
|
}
|
|
2104
2385
|
try {
|
|
2105
2386
|
const stateFilePath = statePath(ctx.directory);
|
|
2106
|
-
const content =
|
|
2387
|
+
const content = readFileSync17(stateFilePath, "utf-8");
|
|
2107
2388
|
const state = parseState(content);
|
|
2108
2389
|
const currentPhase = state["current_phase"] || {};
|
|
2109
2390
|
const result = {
|
|
@@ -2111,7 +2392,7 @@ async function sessionStartHook(ctx) {
|
|
|
2111
2392
|
flowdeck_status: currentPhase["status"] ?? null,
|
|
2112
2393
|
flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
|
|
2113
2394
|
flowdeck_last_action: currentPhase["last_action"] ?? null,
|
|
2114
|
-
flowdeck_has_codebase:
|
|
2395
|
+
flowdeck_has_codebase: existsSync20(codebaseDirectory)
|
|
2115
2396
|
};
|
|
2116
2397
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
2117
2398
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -2126,7 +2407,7 @@ async function sessionStartHook(ctx) {
|
|
|
2126
2407
|
flowdeck_phase: null,
|
|
2127
2408
|
flowdeck_status: "error",
|
|
2128
2409
|
flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
|
|
2129
|
-
flowdeck_has_codebase:
|
|
2410
|
+
flowdeck_has_codebase: existsSync20(codebaseDirectory)
|
|
2130
2411
|
};
|
|
2131
2412
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
2132
2413
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -2187,13 +2468,13 @@ function tryTerminalBell() {
|
|
|
2187
2468
|
function notifySessionIdle() {
|
|
2188
2469
|
notify("FlowDeck Task Completed", "Agent is idle and waiting for your next instruction", "info");
|
|
2189
2470
|
}
|
|
2190
|
-
function notifyPermissionNeeded(
|
|
2191
|
-
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${
|
|
2471
|
+
function notifyPermissionNeeded(tool18) {
|
|
2472
|
+
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool18}`, "critical");
|
|
2192
2473
|
}
|
|
2193
2474
|
|
|
2194
2475
|
// src/hooks/patch-trust.ts
|
|
2195
|
-
import { existsSync as
|
|
2196
|
-
import { join as
|
|
2476
|
+
import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
|
|
2477
|
+
import { join as join20 } from "path";
|
|
2197
2478
|
var HIGH_RISK_KEYWORDS = [
|
|
2198
2479
|
"password",
|
|
2199
2480
|
"secret",
|
|
@@ -2215,11 +2496,11 @@ var HIGH_RISK_KEYWORDS = [
|
|
|
2215
2496
|
"privilege"
|
|
2216
2497
|
];
|
|
2217
2498
|
function loadVolatility(directory) {
|
|
2218
|
-
const p =
|
|
2219
|
-
if (!
|
|
2499
|
+
const p = join20(codebaseDir(directory), "VOLATILITY.json");
|
|
2500
|
+
if (!existsSync21(p))
|
|
2220
2501
|
return {};
|
|
2221
2502
|
try {
|
|
2222
|
-
const data = JSON.parse(
|
|
2503
|
+
const data = JSON.parse(readFileSync18(p, "utf-8"));
|
|
2223
2504
|
const map = {};
|
|
2224
2505
|
for (const entry of data.entries ?? [])
|
|
2225
2506
|
map[entry.path] = entry.stability;
|
|
@@ -2229,11 +2510,11 @@ function loadVolatility(directory) {
|
|
|
2229
2510
|
}
|
|
2230
2511
|
}
|
|
2231
2512
|
function loadFailedPaths(directory) {
|
|
2232
|
-
const p =
|
|
2233
|
-
if (!
|
|
2513
|
+
const p = join20(codebaseDir(directory), "FAILURES.json");
|
|
2514
|
+
if (!existsSync21(p))
|
|
2234
2515
|
return [];
|
|
2235
2516
|
try {
|
|
2236
|
-
const data = JSON.parse(
|
|
2517
|
+
const data = JSON.parse(readFileSync18(p, "utf-8"));
|
|
2237
2518
|
return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
|
|
2238
2519
|
} catch {
|
|
2239
2520
|
return [];
|
|
@@ -2298,8 +2579,8 @@ async function patchTrustHook(ctx, input, output) {
|
|
|
2298
2579
|
}
|
|
2299
2580
|
|
|
2300
2581
|
// src/hooks/decision-trace-hook.ts
|
|
2301
|
-
import { existsSync as
|
|
2302
|
-
import { join as
|
|
2582
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync11, appendFileSync as appendFileSync3 } from "fs";
|
|
2583
|
+
import { join as join21 } from "path";
|
|
2303
2584
|
async function decisionTraceHook(ctx, input, output) {
|
|
2304
2585
|
if (input.tool !== "write" && input.tool !== "edit")
|
|
2305
2586
|
return;
|
|
@@ -2308,8 +2589,8 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
2308
2589
|
return;
|
|
2309
2590
|
const base = codebaseDir(ctx.directory);
|
|
2310
2591
|
try {
|
|
2311
|
-
if (!
|
|
2312
|
-
|
|
2592
|
+
if (!existsSync22(base))
|
|
2593
|
+
mkdirSync11(base, { recursive: true });
|
|
2313
2594
|
const entry = {
|
|
2314
2595
|
timestamp: new Date().toISOString(),
|
|
2315
2596
|
file_path: filePath,
|
|
@@ -2321,62 +2602,87 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
2321
2602
|
risk_level: "unknown",
|
|
2322
2603
|
auto_recorded: true
|
|
2323
2604
|
};
|
|
2324
|
-
|
|
2605
|
+
appendFileSync3(join21(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
|
|
2325
2606
|
`, "utf-8");
|
|
2326
2607
|
} catch {}
|
|
2327
2608
|
}
|
|
2328
2609
|
|
|
2329
2610
|
// src/services/telemetry.ts
|
|
2330
|
-
import { existsSync as
|
|
2331
|
-
import { join as
|
|
2611
|
+
import { existsSync as existsSync23, readFileSync as readFileSync19, appendFileSync as appendFileSync4, mkdirSync as mkdirSync12 } from "fs";
|
|
2612
|
+
import { join as join22 } from "path";
|
|
2332
2613
|
import { randomUUID } from "crypto";
|
|
2333
2614
|
function telemetryPath(dir) {
|
|
2334
|
-
return
|
|
2615
|
+
return join22(codebaseDir(dir), "TELEMETRY.jsonl");
|
|
2335
2616
|
}
|
|
2336
2617
|
function appendEvent(dir, partial) {
|
|
2337
2618
|
if (process.env.TELEMETRY_ENABLED !== "true")
|
|
2338
2619
|
return null;
|
|
2339
2620
|
const cd = codebaseDir(dir);
|
|
2340
|
-
if (!
|
|
2341
|
-
|
|
2621
|
+
if (!existsSync23(cd))
|
|
2622
|
+
mkdirSync12(cd, { recursive: true });
|
|
2342
2623
|
const event = {
|
|
2343
2624
|
id: randomUUID(),
|
|
2344
2625
|
ts: new Date().toISOString(),
|
|
2345
2626
|
...partial
|
|
2346
2627
|
};
|
|
2347
|
-
|
|
2628
|
+
appendFileSync4(telemetryPath(dir), JSON.stringify(event) + `
|
|
2348
2629
|
`, "utf-8");
|
|
2349
2630
|
return event;
|
|
2350
2631
|
}
|
|
2351
2632
|
|
|
2352
2633
|
// src/hooks/telemetry-hook.ts
|
|
2634
|
+
function resolveIds(toolInput) {
|
|
2635
|
+
const session_id = toolInput.sessionID ?? toolInput.sessionId ?? process.env.OPENCODE_SESSION_ID ?? "session-0";
|
|
2636
|
+
const run_id = toolInput.messageID ?? toolInput.messageId ?? toolInput.runID ?? toolInput.runId ?? process.env.OPENCODE_RUN_ID ?? "run-0";
|
|
2637
|
+
return { session_id, run_id };
|
|
2638
|
+
}
|
|
2639
|
+
function inferStatus(output) {
|
|
2640
|
+
if (output.error)
|
|
2641
|
+
return "error";
|
|
2642
|
+
if (typeof output.output !== "string")
|
|
2643
|
+
return "ok";
|
|
2644
|
+
const text = output.output.trim();
|
|
2645
|
+
if (!text)
|
|
2646
|
+
return "ok";
|
|
2647
|
+
try {
|
|
2648
|
+
const parsed = JSON.parse(text);
|
|
2649
|
+
if (parsed.success === false || parsed.error || parsed.status === "error")
|
|
2650
|
+
return "error";
|
|
2651
|
+
return "ok";
|
|
2652
|
+
} catch {
|
|
2653
|
+
return "ok";
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2353
2656
|
async function telemetryHook(context, toolInput, output) {
|
|
2354
2657
|
const dir = context.directory ?? process.cwd();
|
|
2355
|
-
const
|
|
2658
|
+
const tool18 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
2659
|
+
const ids = resolveIds(toolInput);
|
|
2356
2660
|
appendEvent(dir, {
|
|
2357
|
-
session_id:
|
|
2358
|
-
run_id:
|
|
2661
|
+
session_id: ids.session_id,
|
|
2662
|
+
run_id: ids.run_id,
|
|
2359
2663
|
event: "tool.call",
|
|
2360
|
-
tool:
|
|
2664
|
+
tool: tool18,
|
|
2361
2665
|
status: "ok",
|
|
2362
2666
|
meta: { parameters: output.args ?? {} }
|
|
2363
2667
|
});
|
|
2364
2668
|
}
|
|
2365
|
-
async function telemetryAfterHook(context, toolInput,
|
|
2669
|
+
async function telemetryAfterHook(context, toolInput, output) {
|
|
2366
2670
|
const dir = context.directory ?? process.cwd();
|
|
2367
|
-
const
|
|
2671
|
+
const tool18 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
2672
|
+
const ids = resolveIds(toolInput);
|
|
2673
|
+
const status = inferStatus(output);
|
|
2368
2674
|
appendEvent(dir, {
|
|
2369
|
-
session_id:
|
|
2370
|
-
run_id:
|
|
2675
|
+
session_id: ids.session_id,
|
|
2676
|
+
run_id: ids.run_id,
|
|
2371
2677
|
event: "tool.complete",
|
|
2372
|
-
tool:
|
|
2373
|
-
status
|
|
2678
|
+
tool: tool18,
|
|
2679
|
+
status
|
|
2374
2680
|
});
|
|
2375
2681
|
}
|
|
2376
2682
|
|
|
2377
2683
|
// src/services/approval-manager.ts
|
|
2378
|
-
import { existsSync as
|
|
2379
|
-
import { join as
|
|
2684
|
+
import { existsSync as existsSync24, readFileSync as readFileSync20, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13 } from "fs";
|
|
2685
|
+
import { join as join23 } from "path";
|
|
2380
2686
|
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
2381
2687
|
var SENSITIVE_PATTERNS = [
|
|
2382
2688
|
/auth/i,
|
|
@@ -2413,20 +2719,20 @@ function isSensitivePath(filePath) {
|
|
|
2413
2719
|
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
2414
2720
|
}
|
|
2415
2721
|
function approvalsPath(dir) {
|
|
2416
|
-
return
|
|
2722
|
+
return join23(codebaseDir(dir), "APPROVALS.json");
|
|
2417
2723
|
}
|
|
2418
|
-
function
|
|
2724
|
+
function loadStore2(dir) {
|
|
2419
2725
|
const p = approvalsPath(dir);
|
|
2420
|
-
if (!
|
|
2726
|
+
if (!existsSync24(p))
|
|
2421
2727
|
return { requests: [] };
|
|
2422
2728
|
try {
|
|
2423
|
-
return JSON.parse(
|
|
2729
|
+
return JSON.parse(readFileSync20(p, "utf-8"));
|
|
2424
2730
|
} catch {
|
|
2425
2731
|
return { requests: [] };
|
|
2426
2732
|
}
|
|
2427
2733
|
}
|
|
2428
2734
|
function checkApproval(dir, file_path, command) {
|
|
2429
|
-
const store =
|
|
2735
|
+
const store = loadStore2(dir);
|
|
2430
2736
|
const now = Date.now();
|
|
2431
2737
|
return store.requests.filter((r) => r.status === "approved" && r.resolved_at && (r.file_path === file_path || r.trigger === command) && now - new Date(r.resolved_at).getTime() < APPROVAL_TTL_MS).sort((a, b) => b.resolved_at.localeCompare(a.resolved_at)).at(0) ?? null;
|
|
2432
2738
|
}
|
|
@@ -2438,8 +2744,8 @@ async function approvalHook(context, toolInput, output) {
|
|
|
2438
2744
|
if (!ENABLED2)
|
|
2439
2745
|
return;
|
|
2440
2746
|
const dir = context.directory ?? process.cwd();
|
|
2441
|
-
const
|
|
2442
|
-
if (!WRITE_TOOLS.has(
|
|
2747
|
+
const tool18 = toolInput.name ?? toolInput.tool ?? "";
|
|
2748
|
+
if (!WRITE_TOOLS.has(tool18))
|
|
2443
2749
|
return;
|
|
2444
2750
|
const args = output.args ?? {};
|
|
2445
2751
|
const filePath = String(args.path ?? args.file_path ?? args.filename ?? "");
|
|
@@ -2454,7 +2760,7 @@ async function approvalHook(context, toolInput, output) {
|
|
|
2454
2760
|
session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
|
|
2455
2761
|
run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
|
|
2456
2762
|
event: "approval.request",
|
|
2457
|
-
tool:
|
|
2763
|
+
tool: tool18,
|
|
2458
2764
|
status: "blocked",
|
|
2459
2765
|
files: [filePath],
|
|
2460
2766
|
meta: { trigger: "sensitive_file", file: filePath }
|
|
@@ -2515,8 +2821,8 @@ function createContextWindowMonitorHook() {
|
|
|
2515
2821
|
}
|
|
2516
2822
|
|
|
2517
2823
|
// src/hooks/shell-env-hook.ts
|
|
2518
|
-
import { existsSync as
|
|
2519
|
-
import { join as
|
|
2824
|
+
import { existsSync as existsSync25, readFileSync as readFileSync21 } from "fs";
|
|
2825
|
+
import { join as join24 } from "path";
|
|
2520
2826
|
import { createRequire } from "module";
|
|
2521
2827
|
var _version;
|
|
2522
2828
|
function getVersion() {
|
|
@@ -2552,7 +2858,7 @@ var MARKER_TO_LANG = {
|
|
|
2552
2858
|
};
|
|
2553
2859
|
function detectPackageManager(root) {
|
|
2554
2860
|
for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
|
|
2555
|
-
if (
|
|
2861
|
+
if (existsSync25(join24(root, lockfile)))
|
|
2556
2862
|
return pm;
|
|
2557
2863
|
}
|
|
2558
2864
|
return;
|
|
@@ -2561,7 +2867,7 @@ function detectLanguages(root) {
|
|
|
2561
2867
|
const langs = [];
|
|
2562
2868
|
const seen = new Set;
|
|
2563
2869
|
for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
|
|
2564
|
-
if (!seen.has(lang) &&
|
|
2870
|
+
if (!seen.has(lang) && existsSync25(join24(root, marker))) {
|
|
2565
2871
|
langs.push(lang);
|
|
2566
2872
|
seen.add(lang);
|
|
2567
2873
|
}
|
|
@@ -2569,11 +2875,11 @@ function detectLanguages(root) {
|
|
|
2569
2875
|
return langs;
|
|
2570
2876
|
}
|
|
2571
2877
|
function readCurrentPhase(root) {
|
|
2572
|
-
const statePath2 =
|
|
2573
|
-
if (!
|
|
2878
|
+
const statePath2 = join24(root, ".planning", "STATE.md");
|
|
2879
|
+
if (!existsSync25(statePath2))
|
|
2574
2880
|
return;
|
|
2575
2881
|
try {
|
|
2576
|
-
const content =
|
|
2882
|
+
const content = readFileSync21(statePath2, "utf-8");
|
|
2577
2883
|
const match = content.match(/phase:\s*(\S+)/i);
|
|
2578
2884
|
return match?.[1];
|
|
2579
2885
|
} catch {
|
|
@@ -2667,14 +2973,13 @@ function createSessionIdleHook(client, tracker) {
|
|
|
2667
2973
|
if (edited.length > 10) {
|
|
2668
2974
|
await client.app.log({ body: { service: "flowdeck", level: "info", message: ` … and ${edited.length - 10} more` } }).catch(() => {});
|
|
2669
2975
|
}
|
|
2670
|
-
tracker.clear();
|
|
2671
2976
|
} catch {}
|
|
2672
2977
|
};
|
|
2673
2978
|
}
|
|
2674
2979
|
|
|
2675
2980
|
// src/hooks/compaction-hook.ts
|
|
2676
|
-
import { existsSync as
|
|
2677
|
-
import { join as
|
|
2981
|
+
import { existsSync as existsSync26, readFileSync as readFileSync22 } from "fs";
|
|
2982
|
+
import { join as join25 } from "path";
|
|
2678
2983
|
var STRUCTURED_SUMMARY_PROMPT = `
|
|
2679
2984
|
When summarizing this session, you MUST include the following sections:
|
|
2680
2985
|
|
|
@@ -2713,11 +3018,11 @@ For each: agent name, status, description, session_id.
|
|
|
2713
3018
|
**RESUME, DON'T RESTART.** Use session_id to continue existing sessions.
|
|
2714
3019
|
`;
|
|
2715
3020
|
function readPlanningState2(directory) {
|
|
2716
|
-
const statePath2 =
|
|
2717
|
-
if (!
|
|
3021
|
+
const statePath2 = join25(directory, ".planning", "STATE.md");
|
|
3022
|
+
if (!existsSync26(statePath2))
|
|
2718
3023
|
return null;
|
|
2719
3024
|
try {
|
|
2720
|
-
const content =
|
|
3025
|
+
const content = readFileSync22(statePath2, "utf-8");
|
|
2721
3026
|
return content.slice(0, 1500);
|
|
2722
3027
|
} catch {
|
|
2723
3028
|
return null;
|
|
@@ -2812,13 +3117,24 @@ function blockMessage(toolName) {
|
|
|
2812
3117
|
class OrchestratorGuard {
|
|
2813
3118
|
primarySessionId = null;
|
|
2814
3119
|
onEvent(event) {
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
const
|
|
2818
|
-
if (
|
|
2819
|
-
this.primarySessionId =
|
|
3120
|
+
const eventType = event.type ?? "";
|
|
3121
|
+
if (eventType === "session.deleted") {
|
|
3122
|
+
const deletedId = extractSessionId(event);
|
|
3123
|
+
if (deletedId && deletedId === this.primarySessionId) {
|
|
3124
|
+
this.primarySessionId = null;
|
|
2820
3125
|
}
|
|
3126
|
+
return;
|
|
2821
3127
|
}
|
|
3128
|
+
if (eventType !== "session.created" && eventType !== "session.started")
|
|
3129
|
+
return;
|
|
3130
|
+
if (this.primarySessionId !== null)
|
|
3131
|
+
return;
|
|
3132
|
+
const id = extractSessionId(event);
|
|
3133
|
+
if (!id)
|
|
3134
|
+
return;
|
|
3135
|
+
if (extractParentSessionId(event))
|
|
3136
|
+
return;
|
|
3137
|
+
this.primarySessionId = id;
|
|
2822
3138
|
}
|
|
2823
3139
|
check(sessionId, toolName) {
|
|
2824
3140
|
if (DISABLED)
|
|
@@ -2834,6 +3150,20 @@ class OrchestratorGuard {
|
|
|
2834
3150
|
}
|
|
2835
3151
|
}
|
|
2836
3152
|
}
|
|
3153
|
+
function extractSessionId(event) {
|
|
3154
|
+
const props = event.properties;
|
|
3155
|
+
const inner = event.event;
|
|
3156
|
+
const info = props?.info;
|
|
3157
|
+
const id = event.sessionID ?? event.sessionId ?? inner?.sessionID ?? inner?.sessionId ?? info?.id;
|
|
3158
|
+
return id ?? null;
|
|
3159
|
+
}
|
|
3160
|
+
function extractParentSessionId(event) {
|
|
3161
|
+
const props = event.properties;
|
|
3162
|
+
const inner = event.event;
|
|
3163
|
+
const info = props?.info;
|
|
3164
|
+
const parentId = inner?.parentID ?? inner?.parentId ?? info?.parentID ?? info?.parentId;
|
|
3165
|
+
return parentId ?? null;
|
|
3166
|
+
}
|
|
2837
3167
|
|
|
2838
3168
|
// src/hooks/auto-learn-hook.ts
|
|
2839
3169
|
var MIN_EDITS = 1;
|
|
@@ -5810,23 +6140,23 @@ function getAgentConfigs(agentModels) {
|
|
|
5810
6140
|
}
|
|
5811
6141
|
|
|
5812
6142
|
// src/config/loader.ts
|
|
5813
|
-
import { existsSync as
|
|
5814
|
-
import { join as
|
|
5815
|
-
import { homedir as
|
|
6143
|
+
import { existsSync as existsSync27, readFileSync as readFileSync23 } from "fs";
|
|
6144
|
+
import { join as join26 } from "path";
|
|
6145
|
+
import { homedir as homedir3 } from "os";
|
|
5816
6146
|
var CONFIG_FILENAME = "flowdeck.json";
|
|
5817
6147
|
function getGlobalConfigDir() {
|
|
5818
|
-
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ?
|
|
6148
|
+
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join26(process.env.XDG_CONFIG_HOME, "opencode") : join26(homedir3(), ".config", "opencode"));
|
|
5819
6149
|
}
|
|
5820
6150
|
function loadFlowDeckConfig(directory) {
|
|
5821
6151
|
const candidates = [];
|
|
5822
6152
|
if (directory) {
|
|
5823
|
-
candidates.push(
|
|
6153
|
+
candidates.push(join26(directory, ".opencode", CONFIG_FILENAME));
|
|
5824
6154
|
}
|
|
5825
|
-
candidates.push(
|
|
6155
|
+
candidates.push(join26(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
5826
6156
|
for (const configPath of candidates) {
|
|
5827
|
-
if (
|
|
6157
|
+
if (existsSync27(configPath)) {
|
|
5828
6158
|
try {
|
|
5829
|
-
const content =
|
|
6159
|
+
const content = readFileSync23(configPath, "utf-8");
|
|
5830
6160
|
return JSON.parse(content);
|
|
5831
6161
|
} catch {
|
|
5832
6162
|
console.warn(`[flowdeck] Failed to load config from ${configPath}`);
|
|
@@ -5838,13 +6168,13 @@ function loadFlowDeckConfig(directory) {
|
|
|
5838
6168
|
// src/index.ts
|
|
5839
6169
|
function loadRulePaths() {
|
|
5840
6170
|
const __dir = dirname4(fileURLToPath2(import.meta.url));
|
|
5841
|
-
const rulesDir =
|
|
5842
|
-
if (!
|
|
6171
|
+
const rulesDir = join27(__dir, "..", "src", "rules");
|
|
6172
|
+
if (!existsSync28(rulesDir))
|
|
5843
6173
|
return [];
|
|
5844
6174
|
const paths = [];
|
|
5845
6175
|
function walk(dir) {
|
|
5846
6176
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
5847
|
-
const full =
|
|
6177
|
+
const full = join27(dir, entry.name);
|
|
5848
6178
|
if (entry.isDirectory()) {
|
|
5849
6179
|
walk(full);
|
|
5850
6180
|
} else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
|
|
@@ -5857,8 +6187,8 @@ function loadRulePaths() {
|
|
|
5857
6187
|
}
|
|
5858
6188
|
function loadCommands() {
|
|
5859
6189
|
const __dir = dirname4(fileURLToPath2(import.meta.url));
|
|
5860
|
-
const commandsDir =
|
|
5861
|
-
if (!
|
|
6190
|
+
const commandsDir = join27(__dir, "..", "src", "commands");
|
|
6191
|
+
if (!existsSync28(commandsDir))
|
|
5862
6192
|
return {};
|
|
5863
6193
|
const commands = {};
|
|
5864
6194
|
try {
|
|
@@ -5866,7 +6196,7 @@ function loadCommands() {
|
|
|
5866
6196
|
if (!file.endsWith(".md"))
|
|
5867
6197
|
continue;
|
|
5868
6198
|
const name = basename(file, ".md");
|
|
5869
|
-
const raw =
|
|
6199
|
+
const raw = readFileSync24(join27(commandsDir, file), "utf-8");
|
|
5870
6200
|
let description;
|
|
5871
6201
|
let template = raw;
|
|
5872
6202
|
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
@@ -5943,8 +6273,8 @@ var plugin = async (input, _options) => {
|
|
|
5943
6273
|
}
|
|
5944
6274
|
}
|
|
5945
6275
|
}
|
|
5946
|
-
const skillsDir =
|
|
5947
|
-
if (
|
|
6276
|
+
const skillsDir = join27(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
|
|
6277
|
+
if (existsSync28(skillsDir)) {
|
|
5948
6278
|
const cfgAny = cfg;
|
|
5949
6279
|
if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
|
|
5950
6280
|
cfgAny.skills = { paths: [] };
|
|
@@ -5985,7 +6315,8 @@ var plugin = async (input, _options) => {
|
|
|
5985
6315
|
"context-generator": contextGeneratorTool,
|
|
5986
6316
|
"create-skill": createSkillTool,
|
|
5987
6317
|
reflect: reflectTool,
|
|
5988
|
-
"memory-search": memorySearchTool
|
|
6318
|
+
"memory-search": memorySearchTool,
|
|
6319
|
+
"memory-status": memoryStatusTool
|
|
5989
6320
|
},
|
|
5990
6321
|
"shell.env": shellEnvHook,
|
|
5991
6322
|
"todo.updated": todoHook,
|
|
@@ -5997,36 +6328,44 @@ var plugin = async (input, _options) => {
|
|
|
5997
6328
|
},
|
|
5998
6329
|
event: async ({ event }) => {
|
|
5999
6330
|
const type = event?.type ?? "";
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6331
|
+
try {
|
|
6332
|
+
if (type === "session.created" || type === "session.started") {
|
|
6333
|
+
const sessionId = event?.sessionID ?? event?.sessionId ?? "";
|
|
6334
|
+
if (sessionId) {
|
|
6335
|
+
memoryHook.onSessionCreated(directory, sessionId, event?.prompt);
|
|
6336
|
+
}
|
|
6337
|
+
await sessionStartHook({ directory });
|
|
6338
|
+
} else if (type === "message.updated") {
|
|
6339
|
+
const msgEvent = event?.event ?? event;
|
|
6340
|
+
const sessionId = msgEvent?.sessionID ?? msgEvent?.sessionId ?? "";
|
|
6341
|
+
if (sessionId) {
|
|
6342
|
+
memoryHook.onMessageUpdated(sessionId, msgEvent.role, msgEvent.content, directory);
|
|
6343
|
+
}
|
|
6344
|
+
} else if (type === "session.compacted") {
|
|
6345
|
+
const compactEvent = event?.event ?? event;
|
|
6346
|
+
const sessionId = compactEvent?.sessionID ?? compactEvent?.sessionId ?? "";
|
|
6347
|
+
if (sessionId) {
|
|
6348
|
+
memoryHook.onSessionCompact(sessionId, compactEvent.summary ?? "");
|
|
6349
|
+
}
|
|
6350
|
+
} else if (type === "session.deleted") {
|
|
6351
|
+
const delEvent = event?.event ?? event;
|
|
6352
|
+
const sessionId = delEvent?.sessionID ?? delEvent?.sessionId ?? "";
|
|
6353
|
+
if (sessionId) {
|
|
6354
|
+
memoryHook.clearSession(sessionId);
|
|
6355
|
+
}
|
|
6023
6356
|
}
|
|
6357
|
+
} catch (err) {
|
|
6358
|
+
console.error("[FlowDeck Memory] Event handler error:", err);
|
|
6024
6359
|
}
|
|
6025
6360
|
await contextMonitor.event({ event });
|
|
6026
6361
|
orchestratorGuard.onEvent(event);
|
|
6027
6362
|
if (type === "session.idle") {
|
|
6028
|
-
|
|
6029
|
-
|
|
6363
|
+
try {
|
|
6364
|
+
await sessionIdleHook();
|
|
6365
|
+
await autoLearnHook();
|
|
6366
|
+
} finally {
|
|
6367
|
+
fileTracker.clear();
|
|
6368
|
+
}
|
|
6030
6369
|
}
|
|
6031
6370
|
},
|
|
6032
6371
|
"tool.execute.before": async (toolInput, toolOutput) => {
|
|
@@ -6050,9 +6389,13 @@ var plugin = async (input, _options) => {
|
|
|
6050
6389
|
},
|
|
6051
6390
|
"tool.execute.after": async (toolInput, toolOutput) => {
|
|
6052
6391
|
await telemetryAfterHook({ directory }, toolInput, toolOutput);
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6392
|
+
try {
|
|
6393
|
+
const sessionId = toolInput?.sessionID ?? toolInput?.sessionId ?? "";
|
|
6394
|
+
if (sessionId && toolInput?.tool) {
|
|
6395
|
+
memoryHook.onToolExecuted(sessionId, toolInput.tool, toolInput, toolOutput?.output ?? null, directory);
|
|
6396
|
+
}
|
|
6397
|
+
} catch (err) {
|
|
6398
|
+
console.error("[FlowDeck Memory] Tool execution error:", err);
|
|
6056
6399
|
}
|
|
6057
6400
|
await contextMonitor["tool.execute.after"](toolInput, toolOutput);
|
|
6058
6401
|
}
|