@miadi/stateloom-mcp 0.2.1 → 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
@@ -787,7 +787,7 @@ function buildServer() {
787
787
  return { content: [{ type: "text", text: "No events defined yet." }] };
788
788
  return { content: [{ type: "text", text: events.join("\n") }] };
789
789
  });
790
- 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 }) => {
791
791
  try {
792
792
  const r = resolveProjectSwitch(path, PROJECT_FILE);
793
793
  const denial = denyProjectSwitch(r.path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miadi/stateloom-mcp",
3
- "version": "0.2.1",
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",