deepclause-pi 0.1.5 → 0.2.0

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/src/index.ts CHANGED
@@ -1,11 +1,28 @@
1
- import { readdir } from "node:fs/promises";
1
+ import { readFile, readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
5
  import { StringEnum } from "@earendil-works/pi-ai";
5
6
  import type { DMLEvent } from "deepclause-sdk";
6
7
  import { Type } from "typebox";
7
8
  import { buildInitialMessages } from "./context.js";
8
9
  import { loadConfig, setModelToolEnabled, type ContextMode } from "./config.js";
10
+ import { renderDml, renderSequence } from "./diagram/extract.js";
11
+ import { polishDiagram, resolveGrade, type DiagramGrade } from "./diagram/grade.js";
12
+ import { findChrome, validateMermaid, type MermaidView } from "./diagram/validate.js";
13
+ import {
14
+ buildViewer,
15
+ openViewerInBrowser,
16
+ writeSidecar,
17
+ } from "./diagram/viewer.js";
18
+ import {
19
+ collectDiagramTargets,
20
+ diagramNameFor,
21
+ displayPath,
22
+ ensureDiagramDir,
23
+ resolveDiagramSource,
24
+ } from "./diagram/workspace.js";
25
+ import { completeTextWithPiModel } from "./model.js";
9
26
  import { executeDml } from "./runtime.js";
10
27
  import { getPaths, initializeWorkspace, resolveDmlPath } from "./workspace.js";
11
28
  import {
@@ -22,7 +39,8 @@ import {
22
39
  } from "./planner.js";
23
40
 
24
41
  const DC_RUN_TOOL = "dc_run";
25
- const AUTHORING_INSTRUCTION = `DeepClause programs live in .pi/deepclause/skills/ and executable generated plans live in .pi/deepclause/plans/. You may create and edit DML skills directly after consulting .pi/deepclause/AGENTS.md and DML_REFERENCE.md. Use /dc-plan when the user asks pi to design a contextual executable plan; finish that planning turn with dc_plan_commit. DeepClause compilation is unavailable, so generated content must already be valid DML. Users execute programs through /dc-run. If the opt-in dc_run tool is active, you may execute an ordinary skill with it, but contextual plans requiring pi_agent_step must be started by the user. Never invoke a compiler or create .deepclause/.`;
42
+ const DC_DIAGRAM_TOOL = "dc_diagram";
43
+ const AUTHORING_INSTRUCTION = `DeepClause programs live in .pi/deepclause/skills/ and executable generated plans live in .pi/deepclause/plans/. You may create and edit DML skills directly after consulting .pi/deepclause/AGENTS.md and DML_REFERENCE.md. Use /dc-plan when the user asks pi to design a contextual executable plan; finish that planning turn with dc_plan_commit. When the user asks for a diagram, flowchart, or visual of a .dml file, call the dc_diagram tool with the exact path and the requested grade (presentation or specification); it writes the viewer under .pi/deepclause/diagrams/ and opens it, so do not hand-write Mermaid. DeepClause compilation is unavailable, so generated content must already be valid DML. Users execute programs through /dc-run. If the opt-in dc_run tool is active, you may execute an ordinary skill with it, but contextual plans requiring pi_agent_step must be started by the user. Never invoke a compiler or create .deepclause/.`;
26
44
  const STATUS_KEY = "deepclause";
27
45
  const WIDGET_KEY = "deepclause-stream";
28
46
 
@@ -197,6 +215,14 @@ function modelLabel(ctx: ExtensionCommandContext): string {
197
215
  return ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "none selected";
198
216
  }
199
217
 
218
+ async function bundledViewerTemplate(): Promise<string> {
219
+ return readFile(fileURLToPath(new URL("./assets/viewer.template.html", import.meta.url)), "utf8");
220
+ }
221
+
222
+ function viewerVendorAssetPath(): string {
223
+ return fileURLToPath(new URL("./assets/vendor/mermaid.min.js", import.meta.url));
224
+ }
225
+
200
226
  function publishResult(pi: ExtensionAPI, content: string, details: Record<string, unknown>): void {
201
227
  pi.sendMessage({ customType: "deepclause-result", content, display: true, details });
202
228
  }
@@ -205,6 +231,7 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
205
231
  let activeController: AbortController | undefined;
206
232
  let activeDescription: string | undefined;
207
233
  let modelToolRegistered = false;
234
+ let diagramToolRegistered = false;
208
235
  let planCommitRegistered = false;
209
236
  let planningTransaction: PlanningTransaction | undefined;
210
237
  let pendingAgentStep: PendingAgentStep | undefined;
@@ -507,9 +534,135 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
507
534
  }
508
535
  };
509
536
 
537
+ const setDiagramToolActive = () => {
538
+ if (!diagramToolRegistered) {
539
+ pi.registerTool({
540
+ name: DC_DIAGRAM_TOOL,
541
+ label: "Create DeepClause Diagram",
542
+ description: "Create a presentation-grade or specification-grade Mermaid diagram from any .dml file, write a self-contained offline viewer under .pi/deepclause/diagrams/, and open it.",
543
+ promptSnippet: "Create a presentation- or specification-grade diagram from a DML file",
544
+ promptGuidelines: [
545
+ "Use dc_diagram whenever the user asks for a diagram, flowchart, or visual of a .dml file; pass the exact path the user named.",
546
+ "Choose grade=presentation for slides and overviews and grade=specification for engineering detail; use grade=both only when the user asks for both.",
547
+ "Do not hand-write Mermaid or run diagram tools yourself; call dc_diagram and report the viewer result.",
548
+ ],
549
+ parameters: Type.Object({
550
+ dml: Type.String({ description: "Path to a .dml file, relative to the workspace or absolute. A leading @ is ignored." }),
551
+ grade: Type.Optional(Type.String({ description: "presentation (default), specification, or both. Synonyms such as detailed or technical map to specification." })),
552
+ view: Type.Optional(StringEnum(["flow", "sequence"] as const, { description: "Base layout used to seed the grade; default flow." })),
553
+ }),
554
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
555
+ if (activeController) {
556
+ return {
557
+ content: [{ type: "text", text: "Another DeepClause operation is already active; wait for it to finish." }],
558
+ details: { success: false, error: "execution_already_active" },
559
+ };
560
+ }
561
+
562
+ const requested = resolveGrade(String(params.grade ?? "")) ?? "presentation";
563
+ const grades: DiagramGrade[] = requested === "both" ? ["presentation", "specification"] : [requested];
564
+ const view: MermaidView = params.view === "sequence" ? "sequence" : "flow";
565
+
566
+ let sourcePath: string;
567
+ try {
568
+ sourcePath = await resolveDiagramSource(ctx.cwd, params.dml);
569
+ } catch (error) {
570
+ const message = error instanceof Error ? error.message : String(error);
571
+ return { content: [{ type: "text", text: `dc_diagram failed: ${message}` }], details: { success: false, error: message } };
572
+ }
573
+ if (!ctx.model) {
574
+ return {
575
+ content: [{ type: "text", text: "dc_diagram requires an active pi model. Select one and try again." }],
576
+ details: { success: false, error: "no_model" },
577
+ };
578
+ }
579
+
580
+ const config = await loadConfig(getPaths(ctx.cwd).config);
581
+ const source = await readFile(sourcePath, "utf8");
582
+ const display = displayPath(ctx.cwd, sourcePath);
583
+ const seed = view === "sequence"
584
+ ? renderSequence(display, source)
585
+ : renderDml(display, source, { hideOutput: true });
586
+ const targets = await collectDiagramTargets(ctx.cwd, [sourcePath]);
587
+ const name = diagramNameFor(sourcePath, targets, ctx.cwd);
588
+ const { diagrams, vendor } = await ensureDiagramDir(ctx.cwd, viewerVendorAssetPath());
589
+ const templateText = await bundledViewerTemplate();
590
+
591
+ const controller = new AbortController();
592
+ const cancel = () => controller.abort(signal?.reason ?? new Error("dc_diagram cancelled"));
593
+ if (signal?.aborted) cancel();
594
+ else signal?.addEventListener("abort", cancel, { once: true });
595
+ activeController = controller;
596
+ activeDescription = `diagram ${name} (${grades.join("+")})`;
597
+
598
+ const run = (command: string, args: string[], options?: { timeout?: number }) => pi.exec(command, args, options);
599
+ let chrome: string | undefined;
600
+ let chromeResolved = false;
601
+
602
+ try {
603
+ for (const grade of grades) {
604
+ const result = await polishDiagram({
605
+ grade,
606
+ view,
607
+ source,
608
+ seed,
609
+ maxTokens: config.maxTokens,
610
+ signal: controller.signal,
611
+ complete: (options) => completeTextWithPiModel(ctx, options),
612
+ validate: async (code) => {
613
+ if (!chromeResolved) {
614
+ chrome = await findChrome(run);
615
+ chromeResolved = true;
616
+ }
617
+ const outcome = await validateMermaid(code, view, { run, vendorDir: vendor, chrome: chrome ?? null });
618
+ return outcome.result;
619
+ },
620
+ onProgress: (message) => onUpdate?.({
621
+ content: [{ type: "text", text: message }],
622
+ details: { dml: display, grades, name },
623
+ }),
624
+ });
625
+ await writeSidecar(diagrams, name, grade, result.code);
626
+ }
627
+
628
+ const build = await buildViewer({
629
+ cwd: ctx.cwd,
630
+ templateText,
631
+ vendorAssetPath: viewerVendorAssetPath(),
632
+ extraPaths: [sourcePath],
633
+ });
634
+ const opened = ctx.hasUI
635
+ ? await openViewerInBrowser(pi, build.viewerPath, name, grades[0] ?? "presentation")
636
+ : false;
637
+ const viewer = displayPath(ctx.cwd, build.viewerPath);
638
+ const text = `Created ${grades.join(" + ")}-grade diagram for ${display}. Viewer: ${viewer}${opened ? " (opened in your browser)" : ""}`;
639
+ return {
640
+ content: [{ type: "text", text }],
641
+ details: { success: true, dml: display, grades, name, viewer, opened, chrome: Boolean(chrome) },
642
+ };
643
+ } catch (error) {
644
+ const message = error instanceof Error ? error.message : String(error);
645
+ return { content: [{ type: "text", text: `dc_diagram failed: ${message}` }], details: { success: false, error: message } };
646
+ } finally {
647
+ signal?.removeEventListener("abort", cancel);
648
+ activeController = undefined;
649
+ activeDescription = undefined;
650
+ }
651
+ },
652
+ });
653
+ diagramToolRegistered = true;
654
+ }
655
+
656
+ const activeTools = pi.getActiveTools();
657
+ if (!activeTools.includes(DC_DIAGRAM_TOOL)) {
658
+ pi.setActiveTools([...activeTools, DC_DIAGRAM_TOOL]);
659
+ }
660
+ };
661
+
510
662
  pi.on("session_start", async (_event, ctx) => {
511
663
  const config = await loadConfig(getPaths(ctx.cwd).config);
512
664
  setModelToolActive(config.modelToolEnabled);
665
+ setDiagramToolActive();
513
666
  });
514
667
 
515
668
  pi.on("tool_execution_start", (event) => {
@@ -590,6 +743,9 @@ export default function deepClauseExtension(pi: ExtensionAPI) {
590
743
  `Plans: ${path.relative(ctx.cwd, paths.plans)}`,
591
744
  `Context: ${config.contextMode} (verbose default: ${config.verbose})`,
592
745
  `Model tool (${DC_RUN_TOOL}): ${config.modelToolEnabled && pi.getActiveTools().includes(DC_RUN_TOOL) ? "enabled" : "disabled"}`,
746
+ `Model tool (${DC_DIAGRAM_TOOL}): ${pi.getActiveTools().includes(DC_DIAGRAM_TOOL) ? "enabled" : "disabled"}`,
747
+ "Ask pi for a presentation-grade or specification-grade diagram of any .dml file;",
748
+ "it writes the viewer under .pi/deepclause/diagrams/ and opens it.",
593
749
  "Commands:",
594
750
  " /dc-list",
595
751
  " /dc-plan <request> [--name=slug] create an executable contextual DML plan",
package/src/model.ts ADDED
@@ -0,0 +1,46 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { Usage } from "@earendil-works/pi-ai";
3
+
4
+ export interface TextCompletion {
5
+ text: string;
6
+ usage?: Usage;
7
+ }
8
+
9
+ /**
10
+ * One-shot text completion through pi's active model and credentials. Used by
11
+ * diagram grading; it never requests API keys or mutates provider state.
12
+ */
13
+ export async function completeTextWithPiModel(
14
+ ctx: ExtensionContext,
15
+ options: { systemPrompt?: string; prompt: string; maxTokens: number; signal?: AbortSignal },
16
+ ): Promise<TextCompletion> {
17
+ const model = ctx.model;
18
+ if (!model) throw new Error("Select a pi model before generating a diagram grade");
19
+ if (!ctx.modelRegistry.hasConfiguredAuth(model)) {
20
+ throw new Error(`Pi has no configured authentication for ${model.provider}/${model.id}`);
21
+ }
22
+
23
+ const response = await ctx.modelRegistry.complete(
24
+ model,
25
+ {
26
+ systemPrompt: options.systemPrompt,
27
+ messages: [{ role: "user", content: options.prompt, timestamp: Date.now() }],
28
+ },
29
+ {
30
+ signal: options.signal,
31
+ maxTokens: options.maxTokens,
32
+ cacheRetention: "none",
33
+ },
34
+ );
35
+
36
+ if (response.stopReason === "error" || response.stopReason === "aborted") {
37
+ throw new Error(response.errorMessage || `Pi model request ${response.stopReason}`);
38
+ }
39
+
40
+ const text = response.content
41
+ .filter((content): content is Extract<typeof response.content[number], { type: "text" }> => content.type === "text")
42
+ .map((content) => content.text)
43
+ .join("");
44
+
45
+ return { text, usage: response.usage };
46
+ }