@medicine-wheel/app 0.2.8 → 0.4.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.
@@ -0,0 +1,74 @@
1
+ import { NextResponse } from "next/server";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+
5
+ function getChartsFile(): string {
6
+ const dataDir = process.env.MW_DATA_DIR ?? path.join(process.cwd(), ".mw", "store");
7
+ return path.join(dataDir, "charts.jsonl");
8
+ }
9
+
10
+ function readJsonl<T>(filePath: string): T[] {
11
+ if (!fs.existsSync(filePath)) return [];
12
+ const content = fs.readFileSync(filePath, "utf-8");
13
+ return content
14
+ .split("\n")
15
+ .filter((line) => line.trim())
16
+ .map((line) => JSON.parse(line) as T);
17
+ }
18
+
19
+ function appendJsonl<T>(filePath: string, item: T): void {
20
+ const dir = path.dirname(filePath);
21
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
22
+ fs.appendFileSync(filePath, JSON.stringify(item) + "\n", "utf-8");
23
+ }
24
+
25
+ export async function GET(request: Request) {
26
+ try {
27
+ const { searchParams } = new URL(request.url);
28
+ const direction = searchParams.get("direction");
29
+
30
+ let charts = readJsonl<any>(getChartsFile());
31
+
32
+ if (direction) {
33
+ charts = charts.filter((c) => c.direction === direction);
34
+ }
35
+
36
+ charts.sort(
37
+ (a: any, b: any) =>
38
+ Date.parse(b.updated_at ?? b.created_at) -
39
+ Date.parse(a.updated_at ?? a.created_at)
40
+ );
41
+
42
+ return NextResponse.json(charts);
43
+ } catch (error: unknown) {
44
+ const message = error instanceof Error ? error.message : String(error);
45
+ return NextResponse.json({ error: message }, { status: 500 });
46
+ }
47
+ }
48
+
49
+ export async function POST(request: Request) {
50
+ try {
51
+ const body = await request.json();
52
+
53
+ const chart = {
54
+ id: body.id || crypto.randomUUID(),
55
+ desired_outcome: body.desired_outcome,
56
+ current_reality: body.current_reality,
57
+ direction: body.direction || "east",
58
+ action_steps: body.action_steps ?? [],
59
+ due_date: body.due_date,
60
+ created_at: body.created_at || new Date().toISOString(),
61
+ updated_at: new Date().toISOString(),
62
+ phase: body.phase || "current_reality",
63
+ ceremonies_linked: body.ceremonies_linked ?? [],
64
+ wilson_alignment: body.wilson_alignment,
65
+ cycle_id: body.cycle_id,
66
+ };
67
+
68
+ appendJsonl(getChartsFile(), chart);
69
+ return NextResponse.json({ success: true, chart }, { status: 201 });
70
+ } catch (error: unknown) {
71
+ const message = error instanceof Error ? error.message : String(error);
72
+ return NextResponse.json({ error: message }, { status: 500 });
73
+ }
74
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * StreamableHTTP MCP endpoint
3
+ *
4
+ * Enables MCP clients to connect to the Medicine Wheel server
5
+ * via HTTP POST instead of stdio. Accepts JSON-RPC MCP messages
6
+ * and dispatches them to the same tool/resource/prompt registry
7
+ * that the stdio transport uses.
8
+ *
9
+ * Supported methods:
10
+ * - tools/list → lists all registered MCP tools
11
+ * - tools/call → invokes a tool by name with arguments
12
+ *
13
+ * @see https://github.com/jgwill/medicine-wheel/issues/69
14
+ */
15
+ import { NextRequest, NextResponse } from "next/server";
16
+ import { allTools } from "@medicine-wheel/mcp/all-tools";
17
+
18
+ export async function POST(request: NextRequest) {
19
+ try {
20
+ const body = await request.json();
21
+
22
+ if (!body.jsonrpc || body.jsonrpc !== "2.0") {
23
+ return NextResponse.json(
24
+ {
25
+ jsonrpc: "2.0",
26
+ error: {
27
+ code: -32600,
28
+ message: "Invalid Request: expected JSON-RPC 2.0",
29
+ },
30
+ id: body.id ?? null,
31
+ },
32
+ { status: 400 }
33
+ );
34
+ }
35
+
36
+ const { method, params, id } = body;
37
+
38
+ // ── tools/list ──
39
+ if (method === "tools/list") {
40
+ return NextResponse.json({
41
+ jsonrpc: "2.0",
42
+ result: {
43
+ tools: allTools.map((t) => ({
44
+ name: t.name,
45
+ description: t.description,
46
+ inputSchema: t.inputSchema,
47
+ })),
48
+ },
49
+ id: id ?? null,
50
+ });
51
+ }
52
+
53
+ // ── tools/call ──
54
+ if (method === "tools/call") {
55
+ const toolName = params?.name;
56
+ const tool = allTools.find((t) => t.name === toolName);
57
+ if (!tool) {
58
+ return NextResponse.json({
59
+ jsonrpc: "2.0",
60
+ error: {
61
+ code: -32602,
62
+ message: `Unknown tool: ${toolName}`,
63
+ },
64
+ id: id ?? null,
65
+ });
66
+ }
67
+
68
+ try {
69
+ const result = await tool.handler(params?.arguments || {});
70
+ return NextResponse.json({
71
+ jsonrpc: "2.0",
72
+ result: {
73
+ content: [
74
+ { type: "text", text: JSON.stringify(result, null, 2) },
75
+ ],
76
+ },
77
+ id: id ?? null,
78
+ });
79
+ } catch (error) {
80
+ const errorMessage =
81
+ error instanceof Error ? error.message : String(error);
82
+ return NextResponse.json({
83
+ jsonrpc: "2.0",
84
+ result: {
85
+ content: [
86
+ {
87
+ type: "text",
88
+ text: JSON.stringify(
89
+ {
90
+ error: errorMessage,
91
+ direction:
92
+ "Please ensure all required parameters are provided",
93
+ },
94
+ null,
95
+ 2
96
+ ),
97
+ },
98
+ ],
99
+ isError: true,
100
+ },
101
+ id: id ?? null,
102
+ });
103
+ }
104
+ }
105
+
106
+ // ── Unsupported method ──
107
+ return NextResponse.json(
108
+ {
109
+ jsonrpc: "2.0",
110
+ error: {
111
+ code: -32601,
112
+ message: `Method '${method}' is not supported. Available: tools/list, tools/call`,
113
+ },
114
+ id: id ?? null,
115
+ },
116
+ { status: 200 }
117
+ );
118
+ } catch {
119
+ return NextResponse.json(
120
+ {
121
+ jsonrpc: "2.0",
122
+ error: { code: -32700, message: "Parse error" },
123
+ id: null,
124
+ },
125
+ { status: 400 }
126
+ );
127
+ }
128
+ }
129
+
130
+ export async function GET() {
131
+ return NextResponse.json({
132
+ status: "available",
133
+ transport: "StreamableHTTP",
134
+ description:
135
+ "Medicine Wheel MCP StreamableHTTP endpoint. Send JSON-RPC 2.0 POST requests to interact.",
136
+ capabilities: ["tools", "resources", "prompts"],
137
+ });
138
+ }
@@ -0,0 +1,64 @@
1
+ import { NextResponse } from "next/server";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+
5
+ function getMmotsFile(): string {
6
+ const dataDir = process.env.MW_DATA_DIR ?? path.join(process.cwd(), ".mw", "store");
7
+ return path.join(dataDir, "mmots.jsonl");
8
+ }
9
+
10
+ function readJsonl<T>(filePath: string): T[] {
11
+ if (!fs.existsSync(filePath)) return [];
12
+ const content = fs.readFileSync(filePath, "utf-8");
13
+ return content
14
+ .split("\n")
15
+ .filter((line) => line.trim())
16
+ .map((line) => JSON.parse(line) as T);
17
+ }
18
+
19
+ function appendJsonl<T>(filePath: string, item: T): void {
20
+ const dir = path.dirname(filePath);
21
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
22
+ fs.appendFileSync(filePath, JSON.stringify(item) + "\n", "utf-8");
23
+ }
24
+
25
+ export async function GET(request: Request) {
26
+ try {
27
+ const { searchParams } = new URL(request.url);
28
+ const chartId = searchParams.get("chart_id");
29
+
30
+ let mmots = readJsonl<any>(getMmotsFile());
31
+
32
+ if (chartId) {
33
+ mmots = mmots.filter((m) => m.chart_id === chartId);
34
+ }
35
+
36
+ return NextResponse.json(mmots);
37
+ } catch (error: unknown) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ return NextResponse.json({ error: message }, { status: 500 });
40
+ }
41
+ }
42
+
43
+ export async function POST(request: Request) {
44
+ try {
45
+ const body = await request.json();
46
+
47
+ const mmot = {
48
+ id: body.id || crypto.randomUUID(),
49
+ chart_id: body.chart_id,
50
+ timestamp: new Date().toISOString(),
51
+ step1_expected: body.step1_expected,
52
+ step1_actual: body.step1_actual,
53
+ step2_analysis: body.step2_analysis,
54
+ step3_adjustments: body.step3_adjustments ?? [],
55
+ step4_feedback: body.step4_feedback,
56
+ };
57
+
58
+ appendJsonl(getMmotsFile(), mmot);
59
+ return NextResponse.json({ success: true, mmot }, { status: 201 });
60
+ } catch (error: unknown) {
61
+ const message = error instanceof Error ? error.message : String(error);
62
+ return NextResponse.json({ error: message }, { status: 500 });
63
+ }
64
+ }
@@ -1,30 +1,37 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useState, useCallback } from "react";
3
+ import "@xyflow/react/dist/style.css";
4
+
5
+ import { useEffect, useState, useCallback, useMemo } from "react";
6
+ import dynamic from "next/dynamic";
4
7
  import { type RelationalNode, type RelationalEdge, DIRECTION_COLORS } from "@/lib/types";
8
+ import {
9
+ buildGraphData,
10
+ type MWGraphData,
11
+ type MWGraphNode,
12
+ } from "@medicine-wheel/graph-viz";
5
13
  import { toast } from "sonner";
6
14
 
7
- interface GraphNode {
8
- id: string;
9
- label: string;
10
- type: string;
11
- direction?: string;
12
- x: number;
13
- y: number;
14
- }
15
-
16
- interface GraphLink {
17
- source: string;
18
- target: string;
19
- label: string;
20
- ceremonyHonored: boolean;
21
- strength: number;
22
- }
15
+ // React Flow touches `window`/`document`, so the interactive renderer is
16
+ // loaded client-side only via the `./interactive` subpath export.
17
+ const MedicineWheelFlowGraph = dynamic(
18
+ () =>
19
+ import("@medicine-wheel/graph-viz/interactive").then(
20
+ (m) => m.MedicineWheelFlowGraph,
21
+ ),
22
+ {
23
+ ssr: false,
24
+ loading: () => (
25
+ <div className="flex items-center justify-center h-[600px] text-gray-500">
26
+ Loading interactive graph…
27
+ </div>
28
+ ),
29
+ },
30
+ );
23
31
 
24
32
  export default function GraphPage() {
25
- const [nodes, setNodes] = useState<GraphNode[]>([]);
26
- const [links, setLinks] = useState<GraphLink[]>([]);
27
- const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
33
+ const [graph, setGraph] = useState<MWGraphData>({ nodes: [], links: [] });
34
+ const [selectedNode, setSelectedNode] = useState<MWGraphNode | null>(null);
28
35
  const [loading, setLoading] = useState(true);
29
36
  const [showLabels, setShowLabels] = useState(true);
30
37
 
@@ -33,39 +40,13 @@ export default function GraphPage() {
33
40
  const [nodesRes, edgesRes] = await Promise.all([fetch("/api/nodes"), fetch("/api/edges")]);
34
41
  const nodesResponse = await nodesRes.json();
35
42
  const edgesData: RelationalEdge[] = await edgesRes.json();
36
-
37
- // API returns { nodes: [...], provider: '...', count: N }
38
- const nodesData: RelationalNode[] = Array.isArray(nodesResponse) ? nodesResponse : (nodesResponse.nodes || []);
39
-
40
- // Position nodes by direction on a circular layout
41
- const CX = 350, CY = 300, R = 220;
42
- const dirAngles: Record<string, number> = { east: 0, south: 90, west: 180, north: 270 };
43
43
 
44
- const graphNodes = nodesData.map((n, i) => {
45
- const baseAngle = n.direction ? dirAngles[n.direction] ?? 0 : (360 * i) / nodesData.length;
46
- const jitter = (Math.random() - 0.5) * 60;
47
- const angle = ((baseAngle + jitter) * Math.PI) / 180;
48
- const r = R * (0.5 + Math.random() * 0.4);
49
- return {
50
- id: n.id,
51
- label: n.name,
52
- type: n.type,
53
- direction: n.direction,
54
- x: CX + r * Math.cos(angle),
55
- y: CY + r * Math.sin(angle),
56
- };
57
- });
58
-
59
- const graphLinks = edgesData.map((e) => ({
60
- source: e.from_id,
61
- target: e.to_id,
62
- label: e.relationship_type,
63
- ceremonyHonored: e.ceremony_honored,
64
- strength: e.strength,
65
- }));
44
+ // API returns { nodes: [...], provider: '...', count: N }
45
+ const nodesData: RelationalNode[] = Array.isArray(nodesResponse)
46
+ ? nodesResponse
47
+ : nodesResponse.nodes || [];
66
48
 
67
- setNodes(graphNodes);
68
- setLinks(graphLinks);
49
+ setGraph(buildGraphData(nodesData, edgesData));
69
50
  } catch {
70
51
  toast.error("Failed to load graph data");
71
52
  } finally {
@@ -73,9 +54,18 @@ export default function GraphPage() {
73
54
  }
74
55
  }, []);
75
56
 
76
- useEffect(() => { loadData(); }, [loadData]);
57
+ useEffect(() => {
58
+ loadData();
59
+ }, [loadData]);
77
60
 
78
- const nodeMap = new Map(nodes.map((n) => [n.id, n]));
61
+ const ceremoniedCount = useMemo(
62
+ () => graph.links.filter((l) => l.ceremonyHonored).length,
63
+ [graph.links],
64
+ );
65
+ const directionCount = useMemo(
66
+ () => new Set(graph.nodes.map((n) => n.direction).filter(Boolean)).size,
67
+ [graph.nodes],
68
+ );
79
69
 
80
70
  return (
81
71
  <div className="min-h-screen bg-[#0a0a1a] text-white p-6">
@@ -85,7 +75,9 @@ export default function GraphPage() {
85
75
  <h1 className="text-2xl font-bold flex items-center gap-2">
86
76
  <span className="text-3xl">🔮</span> Medicine Wheel Graph
87
77
  </h1>
88
- <p className="text-gray-400 text-sm mt-1">Relational visualization across four directions</p>
78
+ <p className="text-gray-400 text-sm mt-1">
79
+ Interactive relational web — drag, pan, zoom across the four directions
80
+ </p>
89
81
  </div>
90
82
  <div className="flex gap-2">
91
83
  <button
@@ -94,7 +86,9 @@ export default function GraphPage() {
94
86
  >
95
87
  Labels {showLabels ? "ON" : "OFF"}
96
88
  </button>
97
- <button onClick={loadData} className="px-3 py-1.5 rounded text-sm bg-white/5 hover:bg-white/10">↻ Refresh</button>
89
+ <button onClick={loadData} className="px-3 py-1.5 rounded text-sm bg-white/5 hover:bg-white/10">
90
+ ↻ Refresh
91
+ </button>
98
92
  </div>
99
93
  </div>
100
94
 
@@ -102,64 +96,20 @@ export default function GraphPage() {
102
96
  <div className="flex-1 rounded-xl border border-white/10 overflow-hidden">
103
97
  {loading ? (
104
98
  <div className="flex items-center justify-center h-[600px] text-gray-500">Loading graph data...</div>
105
- ) : nodes.length === 0 ? (
99
+ ) : graph.nodes.length === 0 ? (
106
100
  <div className="flex flex-col items-center justify-center h-[600px] text-gray-500">
107
101
  <span className="text-4xl mb-3">🌀</span>
108
102
  <p>No relational nodes yet.</p>
109
103
  <p className="text-sm mt-1">Create nodes via the Nodes page.</p>
110
104
  </div>
111
105
  ) : (
112
- <svg viewBox="0 0 700 600" className="w-full h-[600px]">
113
- {/* Quadrant backgrounds */}
114
- {[
115
- { dir: "east", cx: 525, cy: 300 },
116
- { dir: "south", cx: 350, cy: 475 },
117
- { dir: "west", cx: 175, cy: 300 },
118
- { dir: "north", cx: 350, cy: 125 },
119
- ].map(({ dir, cx, cy }) => (
120
- <g key={dir}>
121
- <circle cx={cx} cy={cy} r={40} fill={(DIRECTION_COLORS as any)[dir]} opacity={0.1} />
122
- <text x={cx} y={cy + 4} textAnchor="middle" fill={(DIRECTION_COLORS as any)[dir]} className="text-xs font-bold capitalize" opacity={0.6}>
123
- {dir}
124
- </text>
125
- </g>
126
- ))}
127
-
128
- {/* Links */}
129
- {links.map((link, i) => {
130
- const source = nodeMap.get(link.source);
131
- const target = nodeMap.get(link.target);
132
- if (!source || !target) return null;
133
- return (
134
- <line
135
- key={i}
136
- x1={source.x} y1={source.y} x2={target.x} y2={target.y}
137
- stroke={link.ceremonyHonored ? "#FFD700" : "#555"}
138
- strokeWidth={1 + link.strength * 2}
139
- strokeDasharray={link.ceremonyHonored ? "none" : "6,4"}
140
- opacity={0.5}
141
- />
142
- );
143
- })}
144
-
145
- {/* Nodes */}
146
- {nodes.map((node) => (
147
- <g key={node.id} className="cursor-pointer" onClick={() => setSelectedNode(node)}>
148
- <circle
149
- cx={node.x} cy={node.y} r={selectedNode?.id === node.id ? 18 : 14}
150
- fill={node.direction ? (DIRECTION_COLORS as any)[node.direction] || "#888" : "#888"}
151
- stroke={selectedNode?.id === node.id ? "#FFD700" : "#333"}
152
- strokeWidth={selectedNode?.id === node.id ? 3 : 1}
153
- opacity={0.9}
154
- />
155
- {showLabels && (
156
- <text x={node.x} y={node.y + 26} textAnchor="middle" fill="#ccc" className="text-[10px]">
157
- {node.label.length > 15 ? node.label.slice(0, 14) + "…" : node.label}
158
- </text>
159
- )}
160
- </g>
161
- ))}
162
- </svg>
106
+ <MedicineWheelFlowGraph
107
+ data={graph}
108
+ height={600}
109
+ darkMode
110
+ showNodeLabels={showLabels}
111
+ onNodeClick={(node) => setSelectedNode(node)}
112
+ />
163
113
  )}
164
114
  </div>
165
115
 
@@ -189,10 +139,10 @@ export default function GraphPage() {
189
139
  <div className="rounded-xl border border-white/10 p-4">
190
140
  <h3 className="text-sm font-semibold text-gray-400 mb-3">Graph Stats</h3>
191
141
  <div className="grid grid-cols-2 gap-3 text-center">
192
- <div><p className="text-2xl font-bold">{nodes.length}</p><p className="text-xs text-gray-500">Nodes</p></div>
193
- <div><p className="text-2xl font-bold">{links.length}</p><p className="text-xs text-gray-500">Relations</p></div>
194
- <div><p className="text-2xl font-bold">{links.filter((l) => l.ceremonyHonored).length}</p><p className="text-xs text-gray-500">Ceremonied</p></div>
195
- <div><p className="text-2xl font-bold">{new Set(nodes.map((n) => n.direction).filter(Boolean)).size}</p><p className="text-xs text-gray-500">Directions</p></div>
142
+ <div><p className="text-2xl font-bold">{graph.nodes.length}</p><p className="text-xs text-gray-500">Nodes</p></div>
143
+ <div><p className="text-2xl font-bold">{graph.links.length}</p><p className="text-xs text-gray-500">Relations</p></div>
144
+ <div><p className="text-2xl font-bold">{ceremoniedCount}</p><p className="text-xs text-gray-500">Ceremonied</p></div>
145
+ <div><p className="text-2xl font-bold">{directionCount}</p><p className="text-xs text-gray-500">Directions</p></div>
196
146
  </div>
197
147
  </div>
198
148
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medicine-wheel/app",
3
- "version": "0.2.8",
3
+ "version": "0.4.0",
4
4
  "description": "Medicine Wheel — Interactive visual layer for Indigenous relational research with Four Directions, ceremonies, and narrative arcs",
5
5
  "bin": {
6
6
  "mw": "dist/cli/mw.js",
@@ -52,6 +52,8 @@
52
52
  "build": "next build",
53
53
  "start": "next start -p 3940",
54
54
  "lint": "next lint",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest",
55
57
  "version:patch": "node scripts/bump-versions.mjs patch && node scripts/sync-versions.mjs && shx rm -rf node_modules src/*/node_modules package-lock.json && npm install --legacy-peer-deps",
56
58
  "version:minor": "node scripts/bump-versions.mjs minor && node scripts/sync-versions.mjs && shx rm -rf node_modules src/*/node_modules package-lock.json && npm install --legacy-peer-deps",
57
59
  "version:major": "node scripts/bump-versions.mjs major && node scripts/sync-versions.mjs && shx rm -rf node_modules src/*/node_modules package-lock.json && npm install --legacy-peer-deps",
@@ -72,33 +74,35 @@
72
74
  "release:major": "npm run version:major && npm run publish:all && npm run release:commit"
73
75
  },
74
76
  "dependencies": {
77
+ "@medicine-wheel/ceremony-protocol": "^0.4.0",
78
+ "@medicine-wheel/community-review": "^0.4.0",
79
+ "@medicine-wheel/consent-lifecycle": "^0.4.0",
80
+ "@medicine-wheel/data-store": "^0.4.0",
81
+ "@medicine-wheel/data-store-postgres": "^0.4.0",
82
+ "@medicine-wheel/fire-keeper": "^0.4.0",
83
+ "@medicine-wheel/graph-viz": "^0.4.0",
84
+ "@medicine-wheel/importance-unit": "^0.4.0",
85
+ "@medicine-wheel/mcp": "^4.3.0",
86
+ "@medicine-wheel/narrative-engine": "^0.4.0",
87
+ "@medicine-wheel/ontology-core": "^0.4.0",
88
+ "@medicine-wheel/prompt-decomposition": "^0.4.0",
89
+ "@medicine-wheel/relational-index": "^0.4.0",
90
+ "@medicine-wheel/relational-query": "^0.4.0",
91
+ "@medicine-wheel/session-reader": "^0.4.0",
92
+ "@medicine-wheel/storage-provider": "^0.4.0",
93
+ "@medicine-wheel/transformation-tracker": "^0.4.0",
94
+ "@medicine-wheel/ui-components": "^0.4.0",
95
+ "@neondatabase/serverless": "^0.10.0",
96
+ "clsx": "^2.1.1",
97
+ "lucide-react": "^0.475.0",
75
98
  "next": "^15.3.0",
99
+ "next-themes": "^0.4.6",
76
100
  "react": "^19.0.0",
77
101
  "react-dom": "^19.0.0",
78
- "next-themes": "^0.4.6",
102
+ "recharts": "^2.15.4",
79
103
  "sonner": "^1.7.0",
80
- "lucide-react": "^0.475.0",
81
- "clsx": "^2.1.1",
82
104
  "tailwind-merge": "^3.0.2",
83
- "recharts": "^2.15.4",
84
- "@medicine-wheel/ontology-core": "^0.2.8",
85
- "@medicine-wheel/ceremony-protocol": "^0.2.8",
86
- "@medicine-wheel/narrative-engine": "^0.2.8",
87
- "@medicine-wheel/graph-viz": "^0.2.8",
88
- "@medicine-wheel/relational-query": "^0.2.8",
89
- "@medicine-wheel/prompt-decomposition": "^0.2.8",
90
- "@medicine-wheel/ui-components": "^0.2.8",
91
- "@medicine-wheel/data-store": "^0.2.8",
92
- "@medicine-wheel/session-reader": "^0.2.8",
93
- "@medicine-wheel/fire-keeper": "^0.2.8",
94
- "@medicine-wheel/importance-unit": "^0.2.8",
95
- "@medicine-wheel/relational-index": "^0.2.8",
96
- "@medicine-wheel/transformation-tracker": "^0.2.8",
97
- "@medicine-wheel/storage-provider": "^0.2.8",
98
- "@medicine-wheel/community-review": "^0.2.8",
99
- "@medicine-wheel/consent-lifecycle": "^0.2.8",
100
- "@medicine-wheel/data-store-postgres": "^0.2.8",
101
- "@neondatabase/serverless": "^0.10.0"
105
+ "@xyflow/react": "^12.3.0"
102
106
  },
103
107
  "devDependencies": {
104
108
  "@tailwindcss/postcss": "^4.1.0",
@@ -107,6 +111,7 @@
107
111
  "@types/react-dom": "^19.0.0",
108
112
  "shx": "^0.4.0",
109
113
  "tailwindcss": "^4.1.0",
110
- "typescript": "^5.7.0"
114
+ "typescript": "^5.7.0",
115
+ "vitest": "^4.1.8"
111
116
  }
112
117
  }