stackcanvas 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.
package/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  # stackcanvas
2
2
 
3
- [Telemetry: opt-in, anonymous, no infra data — see TELEMETRY.md](TELEMETRY.md)
4
-
5
3
  Live infrastructure canvas for [Claude Code](https://claude.com/claude-code).
6
4
  The agent writes and plans your Terraform — stackcanvas shows it as a living
7
5
  diagram. Drag new resources onto the canvas; the agent turns them into
@@ -16,7 +14,7 @@ localhost, reading your local state and plan.
16
14
  2. The graph re-renders live whenever `*.tfstate` or `.stackcanvas/plan.json`
17
15
  change — you watch the agent work.
18
16
  3. You drag resources from the palette (or right-click existing ones to
19
- request changes / removal) and hit **Apply**. Connections between two
17
+ request changes / removal) and hit **Send to agent**. Connections between two
20
18
  not-yet-created (draft) resources aren't included in the intent yet —
21
19
  connect drafts to existing resources, or describe the relation in the
22
20
  draft's wishes field.
@@ -30,10 +28,6 @@ localhost, reading your local state and plan.
30
28
  claude plugin marketplace add pshenok/stackcanvas
31
29
  claude plugin install stackcanvas@stackcanvas
32
30
 
33
- or without the plugin system:
34
-
35
- claude mcp add stackcanvas -- npx -y stackcanvas
36
-
37
31
  Then, inside a repo with Terraform:
38
32
 
39
33
  /stackcanvas
@@ -41,6 +35,13 @@ Then, inside a repo with Terraform:
41
35
  This is the verified path — the CI `check-plugin` job validates the plugin
42
36
  and marketplace manifests on every push.
43
37
 
38
+ Or without the plugin system:
39
+
40
+ claude mcp add stackcanvas -- npx -y stackcanvas
41
+
42
+ Then, inside a repo with Terraform, just ask: *open the stackcanvas canvas
43
+ for this repo*.
44
+
44
45
  ## Other MCP clients
45
46
 
46
47
  stackcanvas is a standard [MCP](https://modelcontextprotocol.io) server, so
@@ -77,11 +78,14 @@ What's provider-specific is only the curation layer, shipped as **provider packs
77
78
 
78
79
  - a palette pack (`packages/ui/src/resource-palette.ts`) — curated drag-and-drop types
79
80
  - containment rules (`DEFAULT_CONTAINMENT_RULES` in `@stackcanvas/core`) — which
80
- resources render as visual containers (AWS VPC/subnet today)
81
+ resources render as visual containers (AWS VPC/subnet, GCP network/subnetwork,
82
+ Azure subnet, Cloudflare zone today)
81
83
  - icon patterns (`packages/ui/src/icons.tsx`)
82
84
 
83
- AWS ships first. Adding a GCP/Azure/other pack is pure data and makes a great
84
- first PR.
85
+ Four packs ship today: **AWS** (complete the reference pack) and **GCP /
86
+ Azure / Cloudflare starter packs** covering the common resource types per
87
+ provider. Rounding out a starter pack, or adding a new provider entirely, is
88
+ pure data and makes a great first PR.
85
89
 
86
90
  ## OpenTofu
87
91
 
@@ -98,7 +102,7 @@ to pin it — both take precedence over auto-detection.
98
102
  | `open_canvas` | Start the canvas for a Terraform root, open the browser |
99
103
  | `load_plan` | Register a plan (JSON or binary) for diff highlighting |
100
104
  | `get_graph_summary` | Text summary of the graph for the agent |
101
- | `await_canvas_intent` | Block until the user clicks Apply; returns their edits |
105
+ | `await_canvas_intent` | Block until the user clicks Send to agent; returns their edits |
102
106
 
103
107
  ## Demo
104
108
 
@@ -106,13 +110,14 @@ to pin it — both take precedence over auto-detection.
106
110
 
107
111
  ## Telemetry
108
112
 
109
- stackcanvas can send a handful of anonymous usage counters (installs,
110
- canvases opened, edits sent, scans run) **opt-in only**, nothing is sent
111
- until you click **Allow** on the one-time canvas banner, and `DO_NOT_TRACK=1`
112
- / `STACKCANVAS_TELEMETRY=0` always turn it off regardless of that decision.
113
- No resource names, infrastructure data, or file paths ever leave your
114
- machine. Full payload, consent model, and how to verify it yourself:
115
- [TELEMETRY.md](TELEMETRY.md).
113
+ stackcanvas can send a handful of anonymous (pseudonymous install id — see
114
+ TELEMETRY.md) usage counters (installs, canvases opened, intents sent;
115
+ scan/drift counters reserved five ever, see TELEMETRY.md) **opt-in
116
+ only**, nothing is sent until you click **Allow** on the one-time canvas
117
+ banner, and `DO_NOT_TRACK=1` / `STACKCANVAS_TELEMETRY=0` always turn it off
118
+ regardless of that decision. No resource names, infrastructure data, or file
119
+ paths ever leave your machine. Full payload, consent model, and how to
120
+ verify it yourself: [TELEMETRY.md](TELEMETRY.md).
116
121
 
117
122
  ## Development
118
123
 
package/dist/cli.js CHANGED
@@ -40,7 +40,15 @@ function deriveEdges(nodes) {
40
40
  }
41
41
  var DEFAULT_CONTAINMENT_RULES = [
42
42
  { containerType: "aws_vpc", memberAttr: "vpc_id", kind: "vpc" },
43
- { containerType: "aws_subnet", memberAttr: "subnet_id", kind: "subnet" }
43
+ { containerType: "aws_subnet", memberAttr: "subnet_id", kind: "subnet" },
44
+ // GCP networks/subnetworks are referenced by `self_link`, not `id`.
45
+ { containerType: "google_compute_network", memberAttr: "network", kind: "vpc", containerIdAttr: "self_link" },
46
+ { containerType: "google_compute_subnetwork", memberAttr: "subnetwork", kind: "subnet", containerIdAttr: "self_link" },
47
+ { containerType: "azurerm_subnet", memberAttr: "subnet_id", kind: "subnet" },
48
+ // Cloudflare zones aren't a network container, but they group the
49
+ // resources scoped to them the same way a VPC groups its members — reuse
50
+ // the 'vpc' kind rather than introducing a one-off styling kind.
51
+ { containerType: "cloudflare_zone", memberAttr: "zone_id", kind: "vpc" }
44
52
  ];
45
53
  function deriveContainment(g, rules = DEFAULT_CONTAINMENT_RULES) {
46
54
  const groups = [...g.groups];
@@ -48,7 +56,7 @@ function deriveContainment(g, rules = DEFAULT_CONTAINMENT_RULES) {
48
56
  for (const rule of rules) {
49
57
  const groupByPhysicalId = /* @__PURE__ */ new Map();
50
58
  for (const n of nodes.filter((n2) => n2.type === rule.containerType)) {
51
- const pid = n.attributes["id"];
59
+ const pid = n.attributes[rule.containerIdAttr ?? "id"];
52
60
  if (typeof pid !== "string") continue;
53
61
  const gid = `${rule.kind}:${n.id}`;
54
62
  groups.push({ id: gid, label: n.name, kind: rule.kind, parent: n.group });
@@ -926,7 +934,7 @@ function openBrowser(url) {
926
934
  }
927
935
 
928
936
  // src/version.ts
929
- var VERSION = "0.2.0";
937
+ var VERSION = "0.2.2";
930
938
 
931
939
  // src/server.ts
932
940
  function looksLikeTerraformRoot(dir) {
@@ -966,7 +974,11 @@ function createMcpServer(deps = {}) {
966
974
  canvas.setAgentStatus("idle");
967
975
  open(url);
968
976
  }
969
- return ok(`Canvas running at ${url}. The graph live-updates as tfstate changes. Run \`terraform plan -out=tfplan && terraform show -json tfplan > .stackcanvas/plan.json\` (or \`tofu plan \u2026\` with OpenTofu) to show the plan diff, then call await_canvas_intent to receive user edits.`);
977
+ const stale = canvas.getStale();
978
+ const warning = stale === null ? "" : `
979
+
980
+ WARNING: the graph may be incomplete \u2014 ${stale}. Tell the user.`;
981
+ return ok(`Canvas running at ${url}. The graph live-updates as tfstate changes. Run \`terraform plan -out=tfplan && terraform show -json tfplan > .stackcanvas/plan.json\` (or \`tofu plan \u2026\` with OpenTofu) to show the plan diff, then call await_canvas_intent to receive user edits.` + warning);
970
982
  });
971
983
  mcp.registerTool("load_plan", {
972
984
  description: "Register a Terraform plan for diff highlighting on the canvas. Accepts a JSON plan (terraform show -json output) or a binary plan file.",
@@ -988,10 +1000,14 @@ function createMcpServer(deps = {}) {
988
1000
  inputSchema: {}
989
1001
  }, async () => {
990
1002
  if (!canvas) return fail("No canvas open. Call open_canvas first.");
991
- return ok(summarizeGraph(canvas.getGraph()));
1003
+ const stale = canvas.getStale();
1004
+ const warning = stale === null ? "" : `
1005
+
1006
+ WARNING: the graph may be incomplete \u2014 ${stale}. Tell the user.`;
1007
+ return ok(summarizeGraph(canvas.getGraph()) + warning);
992
1008
  });
993
1009
  mcp.registerTool("await_canvas_intent", {
994
- description: "Block until the user clicks Apply on the canvas, then return their requested changes as intent JSON: {intent: {add, modify, remove} | null}. null = timeout, call again in a loop to keep waiting. After receiving an intent, write the Terraform code for it.",
1010
+ description: "Block until the user clicks Send-to-agent on the canvas, then return their requested changes as intent JSON: {intent: {add, modify, remove} | null}. null = timeout, call again in a loop to keep waiting. After receiving an intent, write the Terraform code for it. After implementing an intent, run terraform plan (write the JSON to .stackcanvas/plan.json) and call this tool again to keep the loop alive. Never run terraform apply unless the human explicitly asks.",
995
1011
  inputSchema: {
996
1012
  timeoutSeconds: z2.number().positive().max(3600).default(45).describe("How long to wait before returning {intent: null}. Default 45s: MCP clients commonly abort requests after 60s, so keep this under your client's request timeout and call the tool in a loop instead of passing large values.")
997
1013
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "stackcanvas",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
- "main": "src/server.ts",
5
+ "main": "dist/cli.js",
6
6
  "bin": {
7
7
  "stackcanvas": "dist/cli.js"
8
8
  },
@@ -28,5 +28,24 @@
28
28
  "@stackcanvas/server": "workspace:*",
29
29
  "@types/node": "^22.0.0",
30
30
  "tsup": "^8.0.0"
31
- }
31
+ },
32
+ "description": "Live infrastructure canvas for Claude Code: see what your AI agent is about to do to your Terraform \u2014 and approve it \u2014 before it happens. Local-first, credentials never leave your machine.",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/pshenok/stackcanvas.git"
37
+ },
38
+ "homepage": "https://stackcanvas.dev",
39
+ "bugs": "https://github.com/pshenok/stackcanvas/issues",
40
+ "author": "Kostyantyn Pshenychnyy",
41
+ "keywords": [
42
+ "terraform",
43
+ "opentofu",
44
+ "mcp",
45
+ "claude-code",
46
+ "infrastructure-as-code",
47
+ "devtools",
48
+ "visualization",
49
+ "ai-agent"
50
+ ]
32
51
  }