@miadi/stateloom-mcp 0.2.0 → 0.2.2

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.
@@ -8,4 +8,17 @@ export interface SwitchResult {
8
8
  /** True when the new path equals the previous one. */
9
9
  unchanged: boolean;
10
10
  }
11
+ export declare function chronicleRoot(env?: Record<string, string | undefined>): string;
12
+ /**
13
+ * Resolve `miadi-chronicle://<episode>/[diagrams/]<name>[.smdf.json]` to the
14
+ * absolute path `<root>/<episode-dir>/diagrams/<name>.smdf.json`.
15
+ *
16
+ * Episode matching: exact folder name first; else a bare number matches
17
+ * `-episode-NNN-` folders. Episode numbers are NOT unique in the chronicle
18
+ * (011, 016, 043, 078, 083, 098, 120 are doubled) — ambiguity is a HARD
19
+ * ERROR naming every candidate, never a silent first match: a resolver that
20
+ * guesses writes the wrong episode into the record. A missing diagram file
21
+ * stays legitimate — naming it is how an episode's FIRST diagram is created.
22
+ */
23
+ export declare function resolveChronicleUri(uri: string, env?: Record<string, string | undefined>): string;
11
24
  export declare function resolveProjectSwitch(next: string, previous: string): SwitchResult;
@@ -1,11 +1,74 @@
1
1
  /**
2
2
  * Pure logic behind the `set_project_file` MCP tool (path power — the agent
3
3
  * chooses which `.smdf.json` document the loom weaves).
4
+ *
5
+ * Two modalities, by William's orientation (2026-08-07): STANDALONE — a plain
6
+ * path, no episode, just designing — and RELATIONAL — the document lives
7
+ * inside a chronicle episode and is addressed as
8
+ * `miadi-chronicle://<episode>/<diagram>`, resolving over
9
+ * $MIADI_CHRONICLE_ROOT to `<episode-dir>/diagrams/<diagram>.smdf.json`.
10
+ * The relationship is carried by WHERE the file lives (the scalar truth of
11
+ * jgwill/Miadi#593); wheel edges stay derived/ceremonial, never minted here.
4
12
  */
5
- import { existsSync } from "fs";
6
- import { resolve } from "path";
13
+ import { existsSync, readdirSync, statSync } from "fs";
14
+ import { join, resolve } from "path";
15
+ const CHRONICLE_SCHEME = /^miadi-chronicle:\/\//i;
16
+ export function chronicleRoot(env = process.env) {
17
+ return env.MIADI_CHRONICLE_ROOT ?? "/srv/miadi/episodes/miadi-chronicle";
18
+ }
19
+ /**
20
+ * Resolve `miadi-chronicle://<episode>/[diagrams/]<name>[.smdf.json]` to the
21
+ * absolute path `<root>/<episode-dir>/diagrams/<name>.smdf.json`.
22
+ *
23
+ * Episode matching: exact folder name first; else a bare number matches
24
+ * `-episode-NNN-` folders. Episode numbers are NOT unique in the chronicle
25
+ * (011, 016, 043, 078, 083, 098, 120 are doubled) — ambiguity is a HARD
26
+ * ERROR naming every candidate, never a silent first match: a resolver that
27
+ * guesses writes the wrong episode into the record. A missing diagram file
28
+ * stays legitimate — naming it is how an episode's FIRST diagram is created.
29
+ */
30
+ export function resolveChronicleUri(uri, env = process.env) {
31
+ const rest = uri.replace(CHRONICLE_SCHEME, "").replace(/[?#].*$/, "");
32
+ const segments = rest.split("/").filter(Boolean);
33
+ if (segments.length < 2) {
34
+ throw new Error(`chronicle address needs <episode>/<diagram> (got '${uri}') — e.g. miadi-chronicle://103/film-preprod`);
35
+ }
36
+ const episodeRef = segments[0];
37
+ const name = segments[segments.length - 1];
38
+ const root = chronicleRoot(env);
39
+ let dirs;
40
+ try {
41
+ dirs = readdirSync(root).filter((d) => {
42
+ try {
43
+ return statSync(join(root, d)).isDirectory();
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ });
49
+ }
50
+ catch {
51
+ throw new Error(`chronicle root not readable: ${root} (MIADI_CHRONICLE_ROOT)`);
52
+ }
53
+ let matches = dirs.filter((d) => d === episodeRef);
54
+ if (matches.length === 0 && /^\d+$/.test(episodeRef)) {
55
+ const num = String(parseInt(episodeRef, 10));
56
+ matches = dirs.filter((d) => {
57
+ const m = d.match(/-episode-0*(\d+)-/);
58
+ return m !== null && m[1] === num;
59
+ });
60
+ }
61
+ if (matches.length === 0) {
62
+ throw new Error(`no episode matches '${episodeRef}' under ${root}`);
63
+ }
64
+ if (matches.length > 1) {
65
+ throw new Error(`episode '${episodeRef}' is ambiguous — candidates:\n ${matches.sort().join("\n ")}\nName the full folder.`);
66
+ }
67
+ const file = name.endsWith(".json") ? name : `${name}.smdf.json`;
68
+ return join(root, matches[0], "diagrams", file);
69
+ }
7
70
  export function resolveProjectSwitch(next, previous) {
8
- const path = resolve(next);
71
+ const path = CHRONICLE_SCHEME.test(next) ? resolveChronicleUri(next) : resolve(next);
9
72
  if (!path.endsWith(".json")) {
10
73
  throw new Error(`project file must be a .json document (got '${path}') — the loom weaves .smdf.json files`);
11
74
  }
package/dist/server.js CHANGED
@@ -47,6 +47,11 @@ import { resolveProjectSwitch } from "./projectSwitch.js";
47
47
  const BRIDGE_URL = envAlias("BRIDGE_URL");
48
48
  const AGENT_NAME = envAlias("AGENT_NAME");
49
49
  const BRIDGE_TOKEN = envAlias("BRIDGE_TOKEN");
50
+ // The canvas address for a document (chart_1785683055671): when the agent
51
+ // re-points the loom, the human gets a link to LOOK at the same document —
52
+ // the `?doc=` parameter is resolved and allowlist-guarded by the web side.
53
+ const CANVAS_URL = envAlias("CANVAS_URL") ?? `http://127.0.0.1:${envAlias("WEB_PORT") ?? "4598"}`;
54
+ const canvasLink = (path) => `${CANVAS_URL}/?doc=${encodeURIComponent(path)}`;
50
55
  // ─── File-backed shared store ────────────────────────────────────────
51
56
  // Both web/ and mcp/ resolve the same path from STATELOOM_PROJECT_FILE
52
57
  // (legacy twin SMCRAFT_PROJECT_FILE still honored).
@@ -782,7 +787,7 @@ function buildServer() {
782
787
  return { content: [{ type: "text", text: "No events defined yet." }] };
783
788
  return { content: [{ type: "text", text: events.join("\n") }] };
784
789
  });
785
- server.tool("set_project_file", "Choose which .smdf.json path is the active document the loom weaves. Disk persistence AND the live bridge room (the hub keys rooms by this path) both re-point to it. A missing file is legitimate — create_state_machine or load_definition writes it next. Path must end in .json; convention is *.smdf.json.", { path: z.string() }, async ({ path }) => {
790
+ server.tool("set_project_file", "Choose the active document the loom weaves — two modalities. STANDALONE: any .smdf.json path. RELATIONAL: a chronicle address `miadi-chronicle://<episode>/<diagram>` resolving over $MIADI_CHRONICLE_ROOT to <episode-dir>/diagrams/<diagram>.smdf.json — an ambiguous episode number is a hard error naming the candidates. Disk persistence AND the live bridge room both re-point. A missing file is legitimate — naming it is how an episode's first diagram is created.", { path: z.string() }, async ({ path }) => {
786
791
  try {
787
792
  const r = resolveProjectSwitch(path, PROJECT_FILE);
788
793
  const denial = denyProjectSwitch(r.path);
@@ -808,7 +813,7 @@ function buildServer() {
808
813
  content: [
809
814
  {
810
815
  type: "text",
811
- text: `Active document: ${r.path}\nPrevious: ${r.previous}\n${summary}\n${bridgeNote}`,
816
+ text: `Active document: ${r.path}\nPrevious: ${r.previous}\n${summary}\n${bridgeNote}\nCanvas: ${canvasLink(r.path)}`,
812
817
  },
813
818
  ],
814
819
  };
@@ -832,7 +837,7 @@ function buildServer() {
832
837
  : "bridge not configured (STATELOOM_BRIDGE_URL unset)";
833
838
  return {
834
839
  content: [
835
- { type: "text", text: `Active document: ${PROJECT_FILE}\n${summary}\n${bridgeNote}` },
840
+ { type: "text", text: `Active document: ${PROJECT_FILE}\n${summary}\n${bridgeNote}\nCanvas: ${canvasLink(PROJECT_FILE)}` },
836
841
  ],
837
842
  };
838
843
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miadi/stateloom-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "MCP server for miadi-stateloom (formerly smcraft-mcp) — lets LLM agents design state machines through conversation",
5
5
  "repository": {
6
6
  "type": "git",