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/dist/index.js CHANGED
@@ -1,14 +1,22 @@
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 { StringEnum } from "@earendil-works/pi-ai";
4
5
  import { Type } from "typebox";
5
6
  import { buildInitialMessages } from "./context.js";
6
7
  import { loadConfig, setModelToolEnabled } from "./config.js";
8
+ import { renderDml, renderSequence } from "./diagram/extract.js";
9
+ import { polishDiagram, resolveGrade } from "./diagram/grade.js";
10
+ import { findChrome, validateMermaid } from "./diagram/validate.js";
11
+ import { buildViewer, openViewerInBrowser, writeSidecar, } from "./diagram/viewer.js";
12
+ import { collectDiagramTargets, diagramNameFor, displayPath, ensureDiagramDir, resolveDiagramSource, } from "./diagram/workspace.js";
13
+ import { completeTextWithPiModel } from "./model.js";
7
14
  import { executeDml } from "./runtime.js";
8
15
  import { getPaths, initializeWorkspace, resolveDmlPath } from "./workspace.js";
9
16
  import { assemblePlanDml, buildPlanningPrompt, DC_PLAN_COMMIT_TOOL, isContextualPlan, PI_AGENT_STEP_TOOL, readPlanRequiredTools, validateGeneratedPlan, validatePlanSpec, writePlanNonDestructively, } from "./planner.js";
10
17
  const DC_RUN_TOOL = "dc_run";
11
- 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/.`;
18
+ const DC_DIAGRAM_TOOL = "dc_diagram";
19
+ 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/.`;
12
20
  const STATUS_KEY = "deepclause";
13
21
  const WIDGET_KEY = "deepclause-stream";
14
22
  export function splitArguments(input) {
@@ -170,6 +178,12 @@ async function listDmlFiles(directory, prefix = "") {
170
178
  function modelLabel(ctx) {
171
179
  return ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "none selected";
172
180
  }
181
+ async function bundledViewerTemplate() {
182
+ return readFile(fileURLToPath(new URL("./assets/viewer.template.html", import.meta.url)), "utf8");
183
+ }
184
+ function viewerVendorAssetPath() {
185
+ return fileURLToPath(new URL("./assets/vendor/mermaid.min.js", import.meta.url));
186
+ }
173
187
  function publishResult(pi, content, details) {
174
188
  pi.sendMessage({ customType: "deepclause-result", content, display: true, details });
175
189
  }
@@ -177,6 +191,7 @@ export default function deepClauseExtension(pi) {
177
191
  let activeController;
178
192
  let activeDescription;
179
193
  let modelToolRegistered = false;
194
+ let diagramToolRegistered = false;
180
195
  let planCommitRegistered = false;
181
196
  let planningTransaction;
182
197
  let pendingAgentStep;
@@ -468,9 +483,131 @@ export default function deepClauseExtension(pi) {
468
483
  : activeTools.filter((name) => name !== DC_RUN_TOOL));
469
484
  }
470
485
  };
486
+ const setDiagramToolActive = () => {
487
+ if (!diagramToolRegistered) {
488
+ pi.registerTool({
489
+ name: DC_DIAGRAM_TOOL,
490
+ label: "Create DeepClause Diagram",
491
+ 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.",
492
+ promptSnippet: "Create a presentation- or specification-grade diagram from a DML file",
493
+ promptGuidelines: [
494
+ "Use dc_diagram whenever the user asks for a diagram, flowchart, or visual of a .dml file; pass the exact path the user named.",
495
+ "Choose grade=presentation for slides and overviews and grade=specification for engineering detail; use grade=both only when the user asks for both.",
496
+ "Do not hand-write Mermaid or run diagram tools yourself; call dc_diagram and report the viewer result.",
497
+ ],
498
+ parameters: Type.Object({
499
+ dml: Type.String({ description: "Path to a .dml file, relative to the workspace or absolute. A leading @ is ignored." }),
500
+ grade: Type.Optional(Type.String({ description: "presentation (default), specification, or both. Synonyms such as detailed or technical map to specification." })),
501
+ view: Type.Optional(StringEnum(["flow", "sequence"], { description: "Base layout used to seed the grade; default flow." })),
502
+ }),
503
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
504
+ if (activeController) {
505
+ return {
506
+ content: [{ type: "text", text: "Another DeepClause operation is already active; wait for it to finish." }],
507
+ details: { success: false, error: "execution_already_active" },
508
+ };
509
+ }
510
+ const requested = resolveGrade(String(params.grade ?? "")) ?? "presentation";
511
+ const grades = requested === "both" ? ["presentation", "specification"] : [requested];
512
+ const view = params.view === "sequence" ? "sequence" : "flow";
513
+ let sourcePath;
514
+ try {
515
+ sourcePath = await resolveDiagramSource(ctx.cwd, params.dml);
516
+ }
517
+ catch (error) {
518
+ const message = error instanceof Error ? error.message : String(error);
519
+ return { content: [{ type: "text", text: `dc_diagram failed: ${message}` }], details: { success: false, error: message } };
520
+ }
521
+ if (!ctx.model) {
522
+ return {
523
+ content: [{ type: "text", text: "dc_diagram requires an active pi model. Select one and try again." }],
524
+ details: { success: false, error: "no_model" },
525
+ };
526
+ }
527
+ const config = await loadConfig(getPaths(ctx.cwd).config);
528
+ const source = await readFile(sourcePath, "utf8");
529
+ const display = displayPath(ctx.cwd, sourcePath);
530
+ const seed = view === "sequence"
531
+ ? renderSequence(display, source)
532
+ : renderDml(display, source, { hideOutput: true });
533
+ const targets = await collectDiagramTargets(ctx.cwd, [sourcePath]);
534
+ const name = diagramNameFor(sourcePath, targets, ctx.cwd);
535
+ const { diagrams, vendor } = await ensureDiagramDir(ctx.cwd, viewerVendorAssetPath());
536
+ const templateText = await bundledViewerTemplate();
537
+ const controller = new AbortController();
538
+ const cancel = () => controller.abort(signal?.reason ?? new Error("dc_diagram cancelled"));
539
+ if (signal?.aborted)
540
+ cancel();
541
+ else
542
+ signal?.addEventListener("abort", cancel, { once: true });
543
+ activeController = controller;
544
+ activeDescription = `diagram ${name} (${grades.join("+")})`;
545
+ const run = (command, args, options) => pi.exec(command, args, options);
546
+ let chrome;
547
+ let chromeResolved = false;
548
+ try {
549
+ for (const grade of grades) {
550
+ const result = await polishDiagram({
551
+ grade,
552
+ view,
553
+ source,
554
+ seed,
555
+ maxTokens: config.maxTokens,
556
+ signal: controller.signal,
557
+ complete: (options) => completeTextWithPiModel(ctx, options),
558
+ validate: async (code) => {
559
+ if (!chromeResolved) {
560
+ chrome = await findChrome(run);
561
+ chromeResolved = true;
562
+ }
563
+ const outcome = await validateMermaid(code, view, { run, vendorDir: vendor, chrome: chrome ?? null });
564
+ return outcome.result;
565
+ },
566
+ onProgress: (message) => onUpdate?.({
567
+ content: [{ type: "text", text: message }],
568
+ details: { dml: display, grades, name },
569
+ }),
570
+ });
571
+ await writeSidecar(diagrams, name, grade, result.code);
572
+ }
573
+ const build = await buildViewer({
574
+ cwd: ctx.cwd,
575
+ templateText,
576
+ vendorAssetPath: viewerVendorAssetPath(),
577
+ extraPaths: [sourcePath],
578
+ });
579
+ const opened = ctx.hasUI
580
+ ? await openViewerInBrowser(pi, build.viewerPath, name, grades[0] ?? "presentation")
581
+ : false;
582
+ const viewer = displayPath(ctx.cwd, build.viewerPath);
583
+ const text = `Created ${grades.join(" + ")}-grade diagram for ${display}. Viewer: ${viewer}${opened ? " (opened in your browser)" : ""}`;
584
+ return {
585
+ content: [{ type: "text", text }],
586
+ details: { success: true, dml: display, grades, name, viewer, opened, chrome: Boolean(chrome) },
587
+ };
588
+ }
589
+ catch (error) {
590
+ const message = error instanceof Error ? error.message : String(error);
591
+ return { content: [{ type: "text", text: `dc_diagram failed: ${message}` }], details: { success: false, error: message } };
592
+ }
593
+ finally {
594
+ signal?.removeEventListener("abort", cancel);
595
+ activeController = undefined;
596
+ activeDescription = undefined;
597
+ }
598
+ },
599
+ });
600
+ diagramToolRegistered = true;
601
+ }
602
+ const activeTools = pi.getActiveTools();
603
+ if (!activeTools.includes(DC_DIAGRAM_TOOL)) {
604
+ pi.setActiveTools([...activeTools, DC_DIAGRAM_TOOL]);
605
+ }
606
+ };
471
607
  pi.on("session_start", async (_event, ctx) => {
472
608
  const config = await loadConfig(getPaths(ctx.cwd).config);
473
609
  setModelToolActive(config.modelToolEnabled);
610
+ setDiagramToolActive();
474
611
  });
475
612
  pi.on("tool_execution_start", (event) => {
476
613
  if (pendingAgentStep && !pendingAgentStep.toolsUsed.includes(event.toolName)) {
@@ -547,6 +684,9 @@ export default function deepClauseExtension(pi) {
547
684
  `Plans: ${path.relative(ctx.cwd, paths.plans)}`,
548
685
  `Context: ${config.contextMode} (verbose default: ${config.verbose})`,
549
686
  `Model tool (${DC_RUN_TOOL}): ${config.modelToolEnabled && pi.getActiveTools().includes(DC_RUN_TOOL) ? "enabled" : "disabled"}`,
687
+ `Model tool (${DC_DIAGRAM_TOOL}): ${pi.getActiveTools().includes(DC_DIAGRAM_TOOL) ? "enabled" : "disabled"}`,
688
+ "Ask pi for a presentation-grade or specification-grade diagram of any .dml file;",
689
+ "it writes the viewer under .pi/deepclause/diagrams/ and opens it.",
550
690
  "Commands:",
551
691
  " /dc-list",
552
692
  " /dc-plan <request> [--name=slug] create an executable contextual DML plan",
@@ -0,0 +1,16 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { Usage } from "@earendil-works/pi-ai";
3
+ export interface TextCompletion {
4
+ text: string;
5
+ usage?: Usage;
6
+ }
7
+ /**
8
+ * One-shot text completion through pi's active model and credentials. Used by
9
+ * diagram grading; it never requests API keys or mutates provider state.
10
+ */
11
+ export declare function completeTextWithPiModel(ctx: ExtensionContext, options: {
12
+ systemPrompt?: string;
13
+ prompt: string;
14
+ maxTokens: number;
15
+ signal?: AbortSignal;
16
+ }): Promise<TextCompletion>;
package/dist/model.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * One-shot text completion through pi's active model and credentials. Used by
3
+ * diagram grading; it never requests API keys or mutates provider state.
4
+ */
5
+ export async function completeTextWithPiModel(ctx, options) {
6
+ const model = ctx.model;
7
+ if (!model)
8
+ throw new Error("Select a pi model before generating a diagram grade");
9
+ if (!ctx.modelRegistry.hasConfiguredAuth(model)) {
10
+ throw new Error(`Pi has no configured authentication for ${model.provider}/${model.id}`);
11
+ }
12
+ const response = await ctx.modelRegistry.complete(model, {
13
+ systemPrompt: options.systemPrompt,
14
+ messages: [{ role: "user", content: options.prompt, timestamp: Date.now() }],
15
+ }, {
16
+ signal: options.signal,
17
+ maxTokens: options.maxTokens,
18
+ cacheRetention: "none",
19
+ });
20
+ if (response.stopReason === "error" || response.stopReason === "aborted") {
21
+ throw new Error(response.errorMessage || `Pi model request ${response.stopReason}`);
22
+ }
23
+ const text = response.content
24
+ .filter((content) => content.type === "text")
25
+ .map((content) => content.text)
26
+ .join("");
27
+ return { text, usage: response.usage };
28
+ }
@@ -0,0 +1,154 @@
1
+ # DML diagrams in `deepclause-pi` — minimal design
2
+
3
+ ## Goal
4
+
5
+ > The user asks pi to turn a DML file into a diagram — **presentation grade** or
6
+ > **specification grade**. When it's done, a viewer opens.
7
+
8
+ That's the whole feature. No command suite, no manual build/edit/check/clean
9
+ ritual. Pi does the work; the user just asks.
10
+
11
+ ## How the user experiences it
12
+
13
+ ```text
14
+ User: “Make me a presentation-grade diagram of
15
+ skills/deep_research.dml”
16
+
17
+ pi: (calls the dc_diagram tool)
18
+ Creating a presentation-grade diagram for deep_research.dml…
19
+ (viewer opens in the browser at the Presentation view)
20
+
21
+ User: “Now a specification-grade one for the same file”
22
+
23
+ pi: (calls dc_diagram with grade=specification)
24
+ (viewer opens at the Specification view)
25
+ ```
26
+
27
+ The DML file can live **anywhere** — a relative path, an absolute path, or a
28
+ path outside `.pi/deepclause/`. Pi points at it; the tool reads it.
29
+
30
+ Grade is inferred from the request, or passed explicitly:
31
+ `presentation` / `presentation-grade` → Presentation; `specification` /
32
+ `spec` / `detailed` / `technical` → Specification. If the user says "both",
33
+ produce both views.
34
+
35
+ ## What the extension exposes
36
+
37
+ Exactly **one model-callable tool**:
38
+
39
+ ```text
40
+ dc_diagram
41
+ dml : string // path to any .dml file (relative or absolute)
42
+ grade : "presentation" | "specification" | "both" // default: presentation
43
+ view : "flow" | "sequence" // optional, default: flow
44
+ ```
45
+
46
+ - Registered on startup (it is a safe, read-and-generate operation — it does not
47
+ execute the DML, unlike `dc_run`, so it does not need the `/dc-tool` opt-in).
48
+ - `promptGuidelines` tell pi to call it whenever the user asks for a diagram /
49
+ flowchart / visual of a DML file, to infer the grade from the wording, and not
50
+ to hand-write Mermaid itself.
51
+ - A short line in `AUTHORING_INSTRUCTION` / `.pi/deepclause/AGENTS.md` mentions
52
+ the tool and where diagrams live.
53
+
54
+ Optional, only if we want manual triggering: a single `/dc-diagram <path>
55
+ [--grade=...]` command that does exactly the same thing. Not required.
56
+
57
+ ## What the tool does (one call, end to end)
58
+
59
+ 1. Resolve the input path: relative to the workspace or absolute; require an
60
+ existing `.dml` file (`realpath` + extension check). No `.pi/deepclause/`
61
+ restriction on input.
62
+ 2. Read the DML and compute a deterministic Mermaid **seed** in-process
63
+ (`renderDml` / `renderSequence`, ported from `dml_flow.mjs`). Always valid.
64
+ 3. If a grade is requested, ask the active pi model to rewrite the seed in that
65
+ grade (reusing pi's model backend — no `pi_bash`, no API keys), then validate
66
+ and retry up to ~3 rounds:
67
+ - **Presentation**: ~8–12 nodes, plain language, headline numbers, 1–2
68
+ callouts, no function names or framework jargon.
69
+ - **Specification**: keep function names, `task`/`tool` roles,
70
+ post-conditions, seed data; precise for an engineer.
71
+ - Validation: structural checks always; real Mermaid parser via headless
72
+ Chrome only if Chrome is available. A broken result never reaches the
73
+ viewer.
74
+ 4. Write the sidecar(s) under the active workspace's
75
+ `.pi/deepclause/diagrams/`: `<name>.presentation.mmd` and/or
76
+ `<name>.specification.mmd`.
77
+ 5. (Re)build `viewer.html` (embedded manifest + vendored Mermaid).
78
+ 6. **Open the viewer** at that diagram and view:
79
+ `xdg-open .pi/deepclause/diagrams/viewer.html?view=presentation#<name>`
80
+ (fixed argv via `pi.exec`, TUI/interactive only; otherwise just return the
81
+ path).
82
+ 7. Return a one-line result: file, grade, and viewer path.
83
+
84
+ Progress and cancellation reuse the existing `/dc-run` UI plumbing
85
+ (`setWidget`/`setStatus`, a local `AbortController` wired to the active
86
+ execution so `/dc-cancel` still works).
87
+
88
+ ## Names and collisions
89
+
90
+ The viewer is keyed by the DML **file name** (without `.dml`). If two DML files
91
+ with the same base name are diagrammed, disambiguate with a short path hash
92
+ (e.g. `deep_research-3f9a`) so sidecars and viewer entries never collide. The
93
+ full source path is always shown in the viewer.
94
+
95
+ ## Artifacts
96
+
97
+ ```text
98
+ .pi/deepclause/diagrams/ # in the active workspace, regardless
99
+ ├── viewer.html # of where the DML source lives
100
+ ├── index.json # manifest (also handy for tests/scripts)
101
+ ├── <name>.md # Mermaid fences for editors
102
+ ├── <name>.presentation.mmd # presentation-grade sidecar
103
+ └── <name>.specification.mmd # specification-grade sidecar
104
+ ```
105
+
106
+ - Created lazily on first use; `initializeWorkspace()` stays untouched.
107
+ - Output always stays under `.pi/deepclause/`; no `.deepclause/`, no `docs/`.
108
+ - Regenerating a grade overwrites only that sidecar (the user asked for it);
109
+ the other grade and other diagrams are preserved.
110
+
111
+ ## Viewer
112
+
113
+ Reuse the handbook `viewer.template.html` nearly verbatim, with the sidebar
114
+ list, theme picker, split pane (editable Mermaid + read-only DML), SVG/PNG
115
+ export and hash deep-links. The view dropdown becomes:
116
+
117
+ `Presentation → Specification → Flow → Sequence`
118
+
119
+ Fallback order for the default view: Presentation → Specification → Flow.
120
+
121
+ ## Code changes
122
+
123
+ ```text
124
+ src/diagram/extract.ts # TS port of dml_flow.mjs (renderDml, renderSequence)
125
+ src/diagram/grade.ts # model rewrite + validation/retry loop
126
+ src/diagram/viewer.ts # write sidecars, build viewer.html, open browser
127
+ src/assets/viewer.template.html # adapted; Presentation/Specification labels
128
+ src/assets/vendor/mermaid.min.js # vendored, written on first build
129
+ src/runtime.ts # export a small completeWithPiModel() helper
130
+ src/index.ts # register dc_diagram + authoring note
131
+ ```
132
+
133
+ ## Policy
134
+
135
+ - No compiler, no `.deepclause/`, no DML execution.
136
+ - Input may be any readable `.dml` path; **output** is confined to the active
137
+ workspace's `.pi/deepclause/diagrams/`.
138
+ - Does not register runtime tools and does not widen `pi_bash`/approval scope.
139
+ - Browser opening uses a fixed command, not a shell string.
140
+
141
+ ## Validation
142
+
143
+ - Asking pi for a diagram in natural language results in a `dc_diagram` call and
144
+ an opened viewer.
145
+ - A DML file outside `.pi/deepclause/` (including an absolute path) works.
146
+ - Presentation vs specification wording selects the right grade; "both"
147
+ produces both.
148
+ - Chrome present → real-parser gate; Chrome absent → structural gate, viewer
149
+ still opens.
150
+ - Invalid/partial model output is retried and never written as a final sidecar.
151
+ - Missing file / non-`.dml` input is rejected.
152
+ - Same-name DML files do not collide in the viewer.
153
+ - No approval prompts are raised.
154
+ - No files appear outside `.pi/deepclause/diagrams/`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepclause-pi",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "Pi-hosted runtime for DeepClause DML programs",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -375,6 +375,22 @@ DML is most valuable when an application needs more structure than a prompt and
375
375
 
376
376
  Poor fits include long-running background services, high-frequency shell automation that would require many approval prompts, workflows needing unrestricted pi tools, secret handling, or durable state without an explicit workspace storage design.
377
377
 
378
+ ## Diagrams
379
+
380
+ Pi can turn any DML file into a self-contained, offline Mermaid viewer. Ask for one in plain language:
381
+
382
+ > "Make a presentation-grade diagram of .pi/deepclause/skills/my_skill.dml"
383
+ > "Give me a specification-grade diagram of src/report.dml"
384
+
385
+ Pi calls the `dc_diagram` model tool with the DML path and a grade:
386
+
387
+ - **presentation** — about 8-12 nodes, plain language, headline numbers (slides and overviews).
388
+ - **specification** — function names, task/tool roles, post-conditions (engineers).
389
+
390
+ The tool extracts a deterministic Mermaid seed, has pi rewrite it in the chosen grade, validates the result, writes the viewer under `.pi/deepclause/diagrams/`, and opens it. The DML file may live anywhere (workspace-relative or absolute); only the generated viewer stays under `.pi/deepclause/`.
391
+
392
+ Do not hand-write Mermaid for the user, and do not copy diagram tooling into the workspace. Regenerating a grade replaces only that grade's sidecar (`<name>.presentation.mmd` / `<name>.specification.mmd`).
393
+
378
394
  ## Conservative editing rules
379
395
 
380
396
  When modifying an existing skill: