@miadi/stateloom-mcp 0.3.3 → 0.3.5
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/atomicWrite.d.ts +12 -0
- package/dist/atomicWrite.js +21 -1
- package/dist/erd.js +2 -1
- package/dist/server.js +43 -2
- package/package.json +2 -2
package/dist/atomicWrite.d.ts
CHANGED
|
@@ -1 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create the folder a document is about to be written into, one level only.
|
|
3
|
+
*
|
|
4
|
+
* Earned 2026-10-01 (Episode 550, L1): `miadi-chronicle://550/screenwalk`
|
|
5
|
+
* resolves to `<episode>/diagrams/screenwalk.smdf.json`, and naming a missing
|
|
6
|
+
* file is how an episode's first diagram is created — but a fresh episode has
|
|
7
|
+
* no `diagrams/` yet, so the first write failed with ENOENT. The folder is
|
|
8
|
+
* created only when ITS parent exists (an episode's `diagrams/` room, a new
|
|
9
|
+
* folder beside existing work); a path whose ancestors are missing is a typo,
|
|
10
|
+
* and it still fails loudly instead of growing a tree.
|
|
11
|
+
*/
|
|
12
|
+
export declare function ensureParentFolder(path: string): void;
|
|
1
13
|
export declare function writeFileAtomic(path: string, text: string): void;
|
package/dist/atomicWrite.js
CHANGED
|
@@ -6,9 +6,29 @@
|
|
|
6
6
|
* replace it regardless of its mode, and a read-only document is a person's
|
|
7
7
|
* word that it should not change — and the new file keeps the old one's mode.
|
|
8
8
|
*/
|
|
9
|
-
import { accessSync, chmodSync, constants, existsSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
|
|
9
|
+
import { accessSync, chmodSync, constants, existsSync, mkdirSync, renameSync, rmSync, statSync, writeFileSync } from "fs";
|
|
10
10
|
import { basename, dirname, join } from "path";
|
|
11
|
+
/**
|
|
12
|
+
* Create the folder a document is about to be written into, one level only.
|
|
13
|
+
*
|
|
14
|
+
* Earned 2026-10-01 (Episode 550, L1): `miadi-chronicle://550/screenwalk`
|
|
15
|
+
* resolves to `<episode>/diagrams/screenwalk.smdf.json`, and naming a missing
|
|
16
|
+
* file is how an episode's first diagram is created — but a fresh episode has
|
|
17
|
+
* no `diagrams/` yet, so the first write failed with ENOENT. The folder is
|
|
18
|
+
* created only when ITS parent exists (an episode's `diagrams/` room, a new
|
|
19
|
+
* folder beside existing work); a path whose ancestors are missing is a typo,
|
|
20
|
+
* and it still fails loudly instead of growing a tree.
|
|
21
|
+
*/
|
|
22
|
+
export function ensureParentFolder(path) {
|
|
23
|
+
const dir = dirname(path);
|
|
24
|
+
if (existsSync(dir))
|
|
25
|
+
return;
|
|
26
|
+
if (!existsSync(dirname(dir)))
|
|
27
|
+
return;
|
|
28
|
+
mkdirSync(dir);
|
|
29
|
+
}
|
|
11
30
|
export function writeFileAtomic(path, text) {
|
|
31
|
+
ensureParentFolder(path);
|
|
12
32
|
let mode;
|
|
13
33
|
if (existsSync(path)) {
|
|
14
34
|
accessSync(path, constants.W_OK);
|
package/dist/erd.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, readdirSync, writeFileSync } from "fs";
|
|
|
3
3
|
import { dirname, join, resolve } from "path";
|
|
4
4
|
import { addAttribute, addEntity, addRelationship, checkLinks, checkStateOf, collectNotes, emptyErd, ERD_CARDINALITIES, isErdDefinition, isErdfPath, isSqdfPath, isSysdfPath, removeAttribute, removeEntity, removeRelationship, renderMermaidEr, updateEntity, updateErdSettings, summarizeErd, validateErd, normalizeNotes } from "@miadi/stateloom-protocol";
|
|
5
5
|
import { stampedOutputPath } from "@miadi/stateloom-cli/render";
|
|
6
|
+
import { writeFileAtomic } from "./atomicWrite.js";
|
|
6
7
|
const ok = (text) => ({ content: [{ type: "text", text }] });
|
|
7
8
|
const fail = (text) => ({ content: [{ type: "text", text }], isError: true });
|
|
8
9
|
export function readErd(path) {
|
|
@@ -17,7 +18,7 @@ export function readErd(path) {
|
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
function writeErd(path, def) {
|
|
20
|
-
|
|
21
|
+
writeFileAtomic(path, JSON.stringify(def, null, 2) + "\n");
|
|
21
22
|
}
|
|
22
23
|
/** One line for set_project_file / get_project_file when the active document is an ERDF. */
|
|
23
24
|
export function erdSummary(path) {
|
package/dist/server.js
CHANGED
|
@@ -72,6 +72,7 @@ import { registerErdTools, erdSummary, erdGetDefinition, erdLoadDefinition, erdR
|
|
|
72
72
|
import { registerSequenceTools, sequenceSummary, sequenceGetDefinition, sequenceLoadDefinition, sequenceRender, sequenceGetNotes, sequenceSetNotes, } from "./sequence.js";
|
|
73
73
|
import { registerSystemTools, checkSystemReport, reconcileAfterEdit, reconcileModeLine, systemSummary, systemGetDefinition, systemLoadDefinition, systemRender, systemGetNotes, systemSetNotes, systemRispec, } from "./system.js";
|
|
74
74
|
import { liveBridge } from "./live.js";
|
|
75
|
+
import { writeFileAtomic } from "./atomicWrite.js";
|
|
75
76
|
// STATELOOM_* read first, SMCRAFT_* legacy twin honored — the live MCP
|
|
76
77
|
// registration bakes SMCRAFT_PROJECT_FILE and must keep working (2026-07-27
|
|
77
78
|
// rename brief). Resolved once at boot, like the direct reads they replace.
|
|
@@ -130,7 +131,7 @@ function writeDef(def) {
|
|
|
130
131
|
catch {
|
|
131
132
|
// A new or unreadable file is written in the loom's own shape.
|
|
132
133
|
}
|
|
133
|
-
|
|
134
|
+
writeFileAtomic(PROJECT_FILE, JSON.stringify(bare ? def : { stateMachine: def }, null, 2) + (bare ? "\n" : ""));
|
|
134
135
|
}
|
|
135
136
|
// ─── Optional real-time design bridge (env-gated, best-effort) ───────
|
|
136
137
|
// When STATELOOM_BRIDGE_URL (or legacy SMCRAFT_BRIDGE_URL) is set, each
|
|
@@ -784,6 +785,42 @@ function buildServer() {
|
|
|
784
785
|
content: [{ type: "text", text: `Added state '${name}' under '${parentName}' (${kind ?? "normal"}).` }],
|
|
785
786
|
};
|
|
786
787
|
});
|
|
788
|
+
server.tool("remove_transition", "Remove a state's transitions on an event — only those to `nextState` when it is given (use \"\" for a transition that stays). Everything else on the state is kept. Says how many were removed.", { state: z.string(), event: z.string(), nextState: z.string().optional() }, async ({ state, event, nextState }) => {
|
|
789
|
+
const def = readDef();
|
|
790
|
+
if (!def)
|
|
791
|
+
return { content: [{ type: "text", text: noMachine() }], isError: true };
|
|
792
|
+
const target = findState(def.state, state);
|
|
793
|
+
if (!target)
|
|
794
|
+
return { content: [{ type: "text", text: `State '${state}' not found.` }], isError: true };
|
|
795
|
+
const all = target.transitions ?? [];
|
|
796
|
+
const matches = (t) => t.event === event && (nextState === undefined || (nextState === "" ? !t.nextState : t.nextState === nextState));
|
|
797
|
+
const indices = all.map((t, i) => (matches(t) ? i : -1)).filter((i) => i >= 0);
|
|
798
|
+
if (!indices.length) {
|
|
799
|
+
const have = all.map((t) => `${t.event}${t.nextState ? ` → ${t.nextState}` : " (stays)"}`).join(", ") || "none";
|
|
800
|
+
return { content: [{ type: "text", text: `No transition on '${event}'${nextState === undefined ? "" : ` to '${nextState || "(stays)"}'`} from '${state}'. It has: ${have}.` }], isError: true };
|
|
801
|
+
}
|
|
802
|
+
target.transitions = all.filter((t) => !matches(t));
|
|
803
|
+
writeDef(def);
|
|
804
|
+
// Positional ops, highest index first, so each index still names the right one.
|
|
805
|
+
bridgeEmitPatch([...indices].reverse().map((index) => ({ op: "transition.remove", state, index })));
|
|
806
|
+
return { content: [{ type: "text", text: `Removed ${indices.length} transition(s) on '${event}' from '${state}'.` }] };
|
|
807
|
+
});
|
|
808
|
+
server.tool("add_object", "Declare an object the machine is constructed with: `instance` is the name guards use (`walk` in `walk.state`), `class` is its type — an ERD entity when it is data. Fixes L001/L003 without rewriting the machine. Refused when the instance already exists.", { instance: z.string(), class: z.string() }, async ({ instance, class: className }) => {
|
|
809
|
+
const def = readDef();
|
|
810
|
+
if (!def)
|
|
811
|
+
return { content: [{ type: "text", text: noMachine() }], isError: true };
|
|
812
|
+
if (!instance.trim() || !className.trim())
|
|
813
|
+
return { content: [{ type: "text", text: "An object needs an instance and a class." }], isError: true };
|
|
814
|
+
const objects = [...(def.settings.objects ?? [])];
|
|
815
|
+
if (objects.some((o) => o.instance === instance)) {
|
|
816
|
+
return { content: [{ type: "text", text: `The machine already has an object '${instance}'.` }], isError: true };
|
|
817
|
+
}
|
|
818
|
+
objects.push({ instance, class: className });
|
|
819
|
+
def.settings.objects = objects;
|
|
820
|
+
writeDef(def);
|
|
821
|
+
bridgeEmitPatch([{ op: "settings.update", patch: { objects } }]);
|
|
822
|
+
return { content: [{ type: "text", text: `Object '${instance}' : ${className} declared on '${def.settings.name}'.` }] };
|
|
823
|
+
});
|
|
787
824
|
server.tool("update_state", "Change what a state says about itself without rewriting the machine: its description, its kind (normal, final, history) or its prompt. Omitted fields stay as they are; an empty description or prompt clears it. Use it after a scenario built states in auto mode, to say what each one is.", {
|
|
788
825
|
name: z.string(),
|
|
789
826
|
description: z.string().optional(),
|
|
@@ -1081,7 +1118,11 @@ function buildServer() {
|
|
|
1081
1118
|
: "bridge not configured (STATELOOM_BRIDGE_URL unset)";
|
|
1082
1119
|
return {
|
|
1083
1120
|
content: [
|
|
1084
|
-
{
|
|
1121
|
+
{
|
|
1122
|
+
type: "text",
|
|
1123
|
+
// The version is said so an agent can tell when its pin is behind (Episode 550, L4).
|
|
1124
|
+
text: `Active document: ${PROJECT_FILE}\n${summary}\n${bridgeNote}\nCanvas: ${canvasLink(PROJECT_FILE)}\nLoom: @miadi/stateloom-mcp ${PACKAGE_VERSION} (\`npm view @miadi/stateloom-mcp version\` names the latest)`,
|
|
1125
|
+
},
|
|
1085
1126
|
],
|
|
1086
1127
|
};
|
|
1087
1128
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miadi/stateloom-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
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",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@miadi/stateloom-cli": "^0.1.4",
|
|
42
42
|
"@miadi/stateloom-client": "^0.1.3",
|
|
43
|
-
"@miadi/stateloom-protocol": "^0.1.
|
|
43
|
+
"@miadi/stateloom-protocol": "^0.1.14",
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
45
45
|
"zod": "^4.3.6"
|
|
46
46
|
},
|