@insitue/claude-plugin 0.4.5 → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insitue",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "Drive a Claude Code session from the InSitue browser overlay. Pick an element in your app, claude reads the file and proposes the edit.",
5
5
  "mcpServers": {
6
6
  "insitue": {
package/CHANGELOG.md ADDED
@@ -0,0 +1,33 @@
1
+ # @insitue/claude-plugin
2
+
3
+ ## 0.4.6
4
+
5
+ - **Fix (Windows):** `isInsideProject` now uses `path.sep` instead of a
6
+ hardcoded `/`, so the project-dir sandbox check actually holds on
7
+ Windows. Previously every `apply_edit` / `write_file` call would be
8
+ rejected on Windows because `C:\proj/` never matches `C:\proj\file`.
9
+ - **Fix:** the MCP server's reported version (`McpServer({ version })`)
10
+ is now read from `package.json` at startup, eliminating the stale
11
+ `"0.3.0"` literal that drifted from the actual 0.4.x releases.
12
+ - **Fix:** stderr message on spawn-timeout now correctly reports "8 s"
13
+ instead of "5 s" (the loop ceiling is 8 s).
14
+ - **Docs:** rewrote the README's Architecture section to match the
15
+ current tool surface (next_pick, list_recent_picks, start_session,
16
+ end_session, diagnose, read_file, apply_edit, write_file) — the
17
+ previous text claimed only two tools and said "the bridge never
18
+ writes files" (no longer true on Desktop).
19
+ - **Docs:** added Stability, Versioning, and Security sections to the
20
+ README.
21
+ - **Docs:** swapped a leaked internal CMS-slug example
22
+ (`briefings:hairspray-chipping:body`) for a generic one in
23
+ `mcp-server.ts` JSDoc.
24
+ - **Docs:** clarified `project-dir.ts` JSDoc — said "three forms",
25
+ listed six.
26
+ - **Metadata:** added `keywords`, `homepage`, `bugs` to `package.json`;
27
+ included `CHANGELOG.md` in the published tarball.
28
+ - Bumped `.claude-plugin/plugin.json` version in lockstep.
29
+
30
+ ## 0.4.x and earlier
31
+
32
+ Pre-launch versions — see [git history](https://github.com/InSitue/insitue/commits/main/packages/claude-plugin)
33
+ for changes. Real entries start at the next minor bump.
package/README.md CHANGED
@@ -237,32 +237,83 @@ extensively to stderr; claude surfaces them in the transcript.
237
237
 
238
238
  The plugin is a stdio MCP server that:
239
239
 
240
- 1. On startup, reads `${CLAUDE_PROJECT_DIR}/.insitue/session.json`
241
- to find a running companion. If one's alive, reuse it.
242
- 2. Otherwise spawns `npx -y @insitue/companion@latest dev` as a
243
- child process, polls for the new `session.json` to appear,
244
- then connects.
245
- 3. Subscribes to the companion's WS broadcast channel. Every
246
- pick the browser sends arrives here.
247
- 4. Exposes two MCP tools:
248
- - `insitue__next_pick` long-polls until a pick lands
249
- (default 5 min). Returns target + source + screenshot +
250
- userNote.
251
- - `insitue__list_recent_picks` buffered picks since the
252
- server started.
240
+ 1. On startup, resolves the project dir (`--project-dir` argv →
241
+ `INSITUE_PROJECT_DIR` env `CLAUDE_PROJECT_DIR` env
242
+ walk-up for `.insitue/session.json` walk-up for
243
+ `package.json` cwd).
244
+ 2. Reads `.insitue/session.json` to find a running companion.
245
+ If one's alive (PID up + port responsive), reuse it.
246
+ 3. Otherwise spawns `npx -y @insitue/companion@latest dev` as a
247
+ child process, polls for the new `session.json` to appear
248
+ (8 s ceiling), then connects.
249
+ 4. Subscribes to the companion's WS broadcast channel. Every
250
+ pick the browser sends arrives here, gets summarised, and
251
+ buffers (cap: 32) for the polling loop to pull.
253
252
  5. Auto-reconnects if the companion restarts (HMR, manual
254
253
  stop). The widget reconnects too.
255
254
  6. Cleans up on `process.exit` / `SIGTERM` — kills only the
256
255
  companion it spawned, leaves user-started companions
257
256
  untouched.
258
257
 
259
- The bridge **never writes files**. Claude does, via its native
260
- Edit tool. This keeps the InSitue trust boundary clean: the
261
- companion is the only thing that touches fs, and only after
262
- the user has approved a proposal in the terminal.
258
+ ### MCP tools exposed
259
+
260
+ | Tool | Purpose |
261
+ |---|---|
262
+ | `next_pick` | Long-poll for the next browser pick (default 25 s; max 30 min). |
263
+ | `list_recent_picks` | Up to N buffered picks since the MCP server started. |
264
+ | `start_session` | Returns the operating instructions + current state. Desktop entry point. |
265
+ | `end_session` | Cleanly disconnect: close WS, suppress reconnect, kill spawned companion, drop session file. |
266
+ | `diagnose` | Health check — companion reachability, SDK + SWC-plugin versions + wiring, recommendations. |
267
+ | `read_file` | Project-scoped file read. Desktop fallback (Code has native Read). |
268
+ | `apply_edit` | Project-scoped string-replacement edit. Desktop fallback (Code has native Edit). |
269
+ | `write_file` | Project-scoped full-file write. Desktop fallback. |
270
+
271
+ All file tools resolve paths against the project dir and refuse
272
+ anything that resolves outside it (realpath-checked, so `..` games
273
+ are blocked). On Claude Code the agent prefers its native tools;
274
+ the project-scoped ones exist primarily for Claude Desktop, which
275
+ has no built-in file tools.
276
+
277
+ ### Trust boundary
278
+
279
+ The companion is the only process that holds the user's edit
280
+ authority — it spawns from the project dir, writes only inside it,
281
+ and is gated by the WS handshake's loopback bind + per-session
282
+ token. The MCP bridge here is a read-side subscriber that
283
+ broadcasts picks into the chat. Writes still require the human
284
+ in the terminal to say "yes" before the agent calls `apply_edit`
285
+ (or its native equivalent).
263
286
 
264
287
  ---
265
288
 
289
+ ## Stability
290
+
291
+ The plugin's MCP tool surface is what consumers depend on. We
292
+ treat tool name/argument changes as breaking; descriptions and
293
+ internal behaviour can evolve in patch releases.
294
+
295
+ ## Versioning
296
+
297
+ - **Major** — backwards-incompatible changes to the MCP tool
298
+ surface (renames, removed tools, argument shape changes), or
299
+ changes that require the user to re-run `setup`.
300
+ - **Minor** — additive tools, optional new arguments, new env
301
+ vars, new CLI subcommands.
302
+ - **Patch** — bug fixes, docs, internal refactors, version-pin
303
+ bumps for transitive deps.
304
+
305
+ The plugin pins the InSitue WS protocol version (`5` at the time
306
+ of writing) against the companion. A mismatch is rejected at the
307
+ handshake rather than silently degraded.
308
+
309
+ ## Security
310
+
311
+ Report vulnerabilities privately — see [SECURITY.md](../../SECURITY.md)
312
+ in the repo root. Especially relevant for this package: anything
313
+ that lets the bridge accept frames over a non-loopback bind, that
314
+ lets `apply_edit` / `write_file` escape the resolved project dir,
315
+ or that leaks the session token outside the local machine.
316
+
266
317
  ## License
267
318
 
268
319
  MIT. Same as the rest of InSitue.
@@ -0,0 +1,31 @@
1
+ // src/cloud/config.ts
2
+ import { homedir } from "os";
3
+ import { join } from "path";
4
+ import { existsSync, readFileSync } from "fs";
5
+ function loadAuth() {
6
+ const p = join(homedir(), ".insitue", "auth.json");
7
+ if (!existsSync(p)) return {};
8
+ try {
9
+ return JSON.parse(readFileSync(p, "utf8"));
10
+ } catch {
11
+ return {};
12
+ }
13
+ }
14
+ function loadProjectId(projectDir) {
15
+ const p = join(projectDir, ".insitue", "project.json");
16
+ if (!existsSync(p)) return null;
17
+ try {
18
+ return JSON.parse(readFileSync(p, "utf8")).projectId ?? null;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ function resolveHost(cfg) {
24
+ return process.env["INSITUE_API_HOST"] ?? cfg.host ?? "https://app.insitue.com";
25
+ }
26
+
27
+ export {
28
+ loadAuth,
29
+ loadProjectId,
30
+ resolveHost
31
+ };
@@ -0,0 +1,66 @@
1
+ // src/cloud/api.ts
2
+ var CloudApiError = class extends Error {
3
+ constructor(status, code, message) {
4
+ super(message);
5
+ this.status = status;
6
+ this.code = code;
7
+ this.name = "CloudApiError";
8
+ }
9
+ status;
10
+ code;
11
+ };
12
+ async function call(host, token, method, path, body) {
13
+ const res = await fetch(`${host}${path}`, {
14
+ method,
15
+ headers: {
16
+ authorization: `Bearer ${token}`,
17
+ ...body ? { "content-type": "application/json" } : {}
18
+ },
19
+ ...body ? { body: JSON.stringify(body) } : {}
20
+ });
21
+ const text = await res.text();
22
+ let json = {};
23
+ try {
24
+ json = text ? JSON.parse(text) : {};
25
+ } catch {
26
+ }
27
+ const errMsg = json?.error;
28
+ if (!res.ok) {
29
+ const code = res.status === 401 ? "unauthorized" : res.status === 402 ? "not_paid" : res.status === 404 ? "not_found" : res.status === 409 ? "conflict" : "error";
30
+ throw new CloudApiError(res.status, code, errMsg ?? `HTTP ${res.status}`);
31
+ }
32
+ return json;
33
+ }
34
+ var listIssues = (host, token, projectId) => call(
35
+ host,
36
+ token,
37
+ "GET",
38
+ `/api/v1/dev/issues?projectId=${encodeURIComponent(projectId)}`
39
+ );
40
+ var claimIssue = (host, token, id) => call(
41
+ host,
42
+ token,
43
+ "POST",
44
+ `/api/v1/dev/issues/${encodeURIComponent(id)}/claim`
45
+ );
46
+ var resolveIssue = (host, token, id, prUrl, branch) => call(
47
+ host,
48
+ token,
49
+ "POST",
50
+ `/api/v1/dev/issues/${encodeURIComponent(id)}/resolve`,
51
+ { prUrl, branch }
52
+ );
53
+ var releaseIssue = (host, token, id) => call(
54
+ host,
55
+ token,
56
+ "POST",
57
+ `/api/v1/dev/issues/${encodeURIComponent(id)}/release`
58
+ );
59
+
60
+ export {
61
+ CloudApiError,
62
+ listIssues,
63
+ claimIssue,
64
+ resolveIssue,
65
+ releaseIssue
66
+ };
@@ -1,9 +1,9 @@
1
1
  // src/diagnose.ts
2
- import { existsSync, readFileSync } from "fs";
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
3
3
  import { request as httpRequest } from "http";
4
4
  import { join } from "path";
5
- function readPkgVersion(projectDir, pkgName) {
6
- const pkgJson = join(projectDir, "node_modules", pkgName, "package.json");
5
+ function readPkgVersionAt(dir, pkgName) {
6
+ const pkgJson = join(dir, "node_modules", pkgName, "package.json");
7
7
  if (!existsSync(pkgJson)) return null;
8
8
  try {
9
9
  return JSON.parse(readFileSync(pkgJson, "utf8")).version ?? null;
@@ -11,6 +11,38 @@ function readPkgVersion(projectDir, pkgName) {
11
11
  return null;
12
12
  }
13
13
  }
14
+ function listWorkspaceMembers(projectDir) {
15
+ const out = [];
16
+ for (const parent of ["apps", "packages"]) {
17
+ const parentDir = join(projectDir, parent);
18
+ if (!existsSync(parentDir)) continue;
19
+ let entries;
20
+ try {
21
+ entries = readdirSync(parentDir);
22
+ } catch {
23
+ continue;
24
+ }
25
+ for (const name of entries) {
26
+ const child = join(parentDir, name);
27
+ try {
28
+ if (statSync(child).isDirectory() && existsSync(join(child, "package.json"))) {
29
+ out.push(child);
30
+ }
31
+ } catch {
32
+ }
33
+ }
34
+ }
35
+ return out;
36
+ }
37
+ function readPkgVersion(projectDir, pkgName) {
38
+ const v = readPkgVersionAt(projectDir, pkgName);
39
+ if (v) return v;
40
+ for (const ws of listWorkspaceMembers(projectDir)) {
41
+ const v2 = readPkgVersionAt(ws, pkgName);
42
+ if (v2) return v2;
43
+ }
44
+ return null;
45
+ }
14
46
  function readSession(projectDir) {
15
47
  const file = join(projectDir, ".insitue", "session.json");
16
48
  if (!existsSync(file)) return null;
@@ -21,7 +53,7 @@ function readSession(projectDir) {
21
53
  }
22
54
  }
23
55
  async function pokeCompanion(port) {
24
- return new Promise((resolve2) => {
56
+ return new Promise((resolve) => {
25
57
  const req = httpRequest(
26
58
  {
27
59
  host: "127.0.0.1",
@@ -32,26 +64,27 @@ async function pokeCompanion(port) {
32
64
  },
33
65
  (res) => {
34
66
  res.resume();
35
- resolve2({ alive: true, subscribers: null });
67
+ resolve({ alive: true, subscribers: null });
36
68
  }
37
69
  );
38
- req.on("error", () => resolve2({ alive: false, subscribers: null }));
70
+ req.on("error", () => resolve({ alive: false, subscribers: null }));
39
71
  req.on("timeout", () => {
40
72
  req.destroy();
41
- resolve2({ alive: false, subscribers: null });
73
+ resolve({ alive: false, subscribers: null });
42
74
  });
43
75
  req.end();
44
76
  });
45
77
  }
46
- function detectSwcPluginConfigured(projectDir) {
47
- for (const f of [
48
- "next.config.mjs",
49
- "next.config.js",
50
- "next.config.ts",
51
- "vite.config.ts",
52
- "vite.config.js"
53
- ]) {
54
- const p = join(projectDir, f);
78
+ var FRAMEWORK_CONFIGS = [
79
+ "next.config.mjs",
80
+ "next.config.js",
81
+ "next.config.ts",
82
+ "vite.config.ts",
83
+ "vite.config.js"
84
+ ];
85
+ function detectSwcPluginConfiguredAt(dir) {
86
+ for (const f of FRAMEWORK_CONFIGS) {
87
+ const p = join(dir, f);
55
88
  if (existsSync(p)) {
56
89
  try {
57
90
  const c = readFileSync(p, "utf8");
@@ -63,6 +96,18 @@ function detectSwcPluginConfigured(projectDir) {
63
96
  }
64
97
  return null;
65
98
  }
99
+ function detectSwcPluginConfigured(projectDir) {
100
+ let foundAnyConfig = false;
101
+ const direct = detectSwcPluginConfiguredAt(projectDir);
102
+ if (direct === true) return true;
103
+ if (direct === false) foundAnyConfig = true;
104
+ for (const ws of listWorkspaceMembers(projectDir)) {
105
+ const r = detectSwcPluginConfiguredAt(ws);
106
+ if (r === true) return true;
107
+ if (r === false) foundAnyConfig = true;
108
+ }
109
+ return foundAnyConfig ? false : null;
110
+ }
66
111
  async function diagnose(projectDir) {
67
112
  const session = readSession(projectDir.dir);
68
113
  const hasSessionFile = session !== null;
@@ -119,76 +164,6 @@ async function diagnose(projectDir) {
119
164
  };
120
165
  }
121
166
 
122
- // src/project-dir.ts
123
- import { existsSync as existsSync2, realpathSync } from "fs";
124
- import { dirname, isAbsolute, join as join2, resolve } from "path";
125
- function readProjectDirArg(argv) {
126
- for (let i = 0; i < argv.length; i++) {
127
- const a = argv[i];
128
- if (a === "--project-dir" && i + 1 < argv.length) return argv[i + 1];
129
- if (a.startsWith("--project-dir=")) return a.slice("--project-dir=".length);
130
- }
131
- return null;
132
- }
133
- function walkUpFor(start, marker) {
134
- let dir = resolve(start);
135
- while (true) {
136
- if (existsSync2(join2(dir, marker))) return dir;
137
- const parent = dirname(dir);
138
- if (parent === dir) return null;
139
- dir = parent;
140
- }
141
- }
142
- function realpathSafe(p) {
143
- try {
144
- return realpathSync(p);
145
- } catch {
146
- return resolve(p);
147
- }
148
- }
149
- function resolveProjectDir(argv = process.argv.slice(2), env = process.env) {
150
- const fromArg = readProjectDirArg(argv);
151
- if (fromArg) {
152
- return { dir: realpathSafe(fromArg), source: "argv" };
153
- }
154
- if (env.INSITUE_PROJECT_DIR) {
155
- return {
156
- dir: realpathSafe(env.INSITUE_PROJECT_DIR),
157
- source: "INSITUE_PROJECT_DIR"
158
- };
159
- }
160
- if (env.CLAUDE_PROJECT_DIR) {
161
- return {
162
- dir: realpathSafe(env.CLAUDE_PROJECT_DIR),
163
- source: "CLAUDE_PROJECT_DIR"
164
- };
165
- }
166
- const cwd = process.cwd();
167
- const sessionDir = walkUpFor(cwd, ".insitue");
168
- if (sessionDir && existsSync2(join2(sessionDir, ".insitue", "session.json"))) {
169
- return { dir: realpathSafe(sessionDir), source: "session-walk-up" };
170
- }
171
- const pkgDir = walkUpFor(cwd, "package.json");
172
- if (pkgDir) {
173
- return { dir: realpathSafe(pkgDir), source: "package-walk-up" };
174
- }
175
- return { dir: realpathSafe(cwd), source: "cwd" };
176
- }
177
- function isInsideProject(root, target) {
178
- const r = realpathSafe(root);
179
- let t;
180
- try {
181
- t = realpathSync(target);
182
- } catch {
183
- t = resolve(target);
184
- }
185
- if (!isAbsolute(r) || !isAbsolute(t)) return false;
186
- const rWithSep = r.endsWith("/") ? r : r + "/";
187
- return t === r || t.startsWith(rWithSep);
188
- }
189
-
190
167
  export {
191
- diagnose,
192
- resolveProjectDir,
193
- isInsideProject
168
+ diagnose
194
169
  };
@@ -0,0 +1,72 @@
1
+ // src/project-dir.ts
2
+ import { existsSync, realpathSync } from "fs";
3
+ import { dirname, isAbsolute, join, resolve, sep } from "path";
4
+ function readProjectDirArg(argv) {
5
+ for (let i = 0; i < argv.length; i++) {
6
+ const a = argv[i];
7
+ if (a === "--project-dir" && i + 1 < argv.length) return argv[i + 1];
8
+ if (a.startsWith("--project-dir=")) return a.slice("--project-dir=".length);
9
+ }
10
+ return null;
11
+ }
12
+ function walkUpFor(start, marker) {
13
+ let dir = resolve(start);
14
+ while (true) {
15
+ if (existsSync(join(dir, marker))) return dir;
16
+ const parent = dirname(dir);
17
+ if (parent === dir) return null;
18
+ dir = parent;
19
+ }
20
+ }
21
+ function realpathSafe(p) {
22
+ try {
23
+ return realpathSync(p);
24
+ } catch {
25
+ return resolve(p);
26
+ }
27
+ }
28
+ function resolveProjectDir(argv = process.argv.slice(2), env = process.env) {
29
+ const fromArg = readProjectDirArg(argv);
30
+ if (fromArg) {
31
+ return { dir: realpathSafe(fromArg), source: "argv" };
32
+ }
33
+ if (env.INSITUE_PROJECT_DIR) {
34
+ return {
35
+ dir: realpathSafe(env.INSITUE_PROJECT_DIR),
36
+ source: "INSITUE_PROJECT_DIR"
37
+ };
38
+ }
39
+ if (env.CLAUDE_PROJECT_DIR) {
40
+ return {
41
+ dir: realpathSafe(env.CLAUDE_PROJECT_DIR),
42
+ source: "CLAUDE_PROJECT_DIR"
43
+ };
44
+ }
45
+ const cwd = process.cwd();
46
+ const sessionDir = walkUpFor(cwd, ".insitue");
47
+ if (sessionDir && existsSync(join(sessionDir, ".insitue", "session.json"))) {
48
+ return { dir: realpathSafe(sessionDir), source: "session-walk-up" };
49
+ }
50
+ const pkgDir = walkUpFor(cwd, "package.json");
51
+ if (pkgDir) {
52
+ return { dir: realpathSafe(pkgDir), source: "package-walk-up" };
53
+ }
54
+ return { dir: realpathSafe(cwd), source: "cwd" };
55
+ }
56
+ function isInsideProject(root, target) {
57
+ const r = realpathSafe(root);
58
+ let t;
59
+ try {
60
+ t = realpathSync(target);
61
+ } catch {
62
+ t = resolve(target);
63
+ }
64
+ if (!isAbsolute(r) || !isAbsolute(t)) return false;
65
+ const rWithSep = r.endsWith(sep) ? r : r + sep;
66
+ return t === r || t.startsWith(rWithSep);
67
+ }
68
+
69
+ export {
70
+ resolveProjectDir,
71
+ isInsideProject
72
+ };
@@ -0,0 +1,14 @@
1
+ import {
2
+ CloudApiError,
3
+ claimIssue,
4
+ listIssues,
5
+ releaseIssue,
6
+ resolveIssue
7
+ } from "../chunk-IRPBZWNQ.js";
8
+ export {
9
+ CloudApiError,
10
+ claimIssue,
11
+ listIssues,
12
+ releaseIssue,
13
+ resolveIssue
14
+ };
@@ -0,0 +1,10 @@
1
+ import {
2
+ loadAuth,
3
+ loadProjectId,
4
+ resolveHost
5
+ } from "../chunk-5APYM634.js";
6
+ export {
7
+ loadAuth,
8
+ loadProjectId,
9
+ resolveHost
10
+ };
@@ -0,0 +1,6 @@
1
+ import {
2
+ diagnose
3
+ } from "./chunk-SGLSPTHD.js";
4
+ export {
5
+ diagnose
6
+ };
@@ -1,9 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- diagnose,
4
3
  isInsideProject,
5
4
  resolveProjectDir
6
- } from "./chunk-RF5Q55CG.js";
5
+ } from "./chunk-UNMH2DN4.js";
6
+ import {
7
+ diagnose
8
+ } from "./chunk-SGLSPTHD.js";
9
+ import {
10
+ CloudApiError,
11
+ claimIssue,
12
+ listIssues,
13
+ releaseIssue,
14
+ resolveIssue
15
+ } from "./chunk-IRPBZWNQ.js";
16
+ import {
17
+ loadAuth,
18
+ loadProjectId,
19
+ resolveHost
20
+ } from "./chunk-5APYM634.js";
7
21
 
8
22
  // src/mcp-server.ts
9
23
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -166,6 +180,20 @@ function loadInstructions() {
166
180
  }
167
181
 
168
182
  // src/mcp-server.ts
183
+ function readPackageVersion() {
184
+ const here = dirname3(fileURLToPath2(import.meta.url));
185
+ for (const base of [join3(here, ".."), here, join3(here, "..", "..")]) {
186
+ const p = join3(base, "package.json");
187
+ if (existsSync2(p)) {
188
+ try {
189
+ const v = JSON.parse(readFileSync3(p, "utf8")).version;
190
+ if (v) return v;
191
+ } catch {
192
+ }
193
+ }
194
+ }
195
+ return "unknown";
196
+ }
169
197
  var MAX_BUFFERED_PICKS = 32;
170
198
  var NEXT_PICK_DEFAULT_TIMEOUT_MS = 25 * 1e3;
171
199
  var NEXT_PICK_MAX_TIMEOUT_MS = 30 * 60 * 1e3;
@@ -327,7 +355,7 @@ async function ensureCompanion(projectDir2) {
327
355
  await new Promise((r) => setTimeout(r, 200));
328
356
  }
329
357
  process.stderr.write(
330
- "[insitue-mcp] companion didn't come up in 5s \u2014 see [companion] / [companion err] above\n"
358
+ "[insitue-mcp] companion didn't come up in 8s \u2014 see [companion] / [companion err] above\n"
331
359
  );
332
360
  return null;
333
361
  }
@@ -484,7 +512,7 @@ function endSession() {
484
512
  }
485
513
  var server = new McpServer({
486
514
  name: "insitue",
487
- version: "0.3.0"
515
+ version: readPackageVersion()
488
516
  });
489
517
  server.registerTool(
490
518
  "next_pick",
@@ -685,6 +713,199 @@ server.registerTool(
685
713
  };
686
714
  }
687
715
  );
716
+ function cloudSetup() {
717
+ const auth = loadAuth();
718
+ if (!auth.token) {
719
+ return {
720
+ error: "not_logged_in",
721
+ message: "Run `insitue login` (create a token at https://app.insitue.com/app/settings/developer) so InSitue can read this account's issues."
722
+ };
723
+ }
724
+ const projectId = loadProjectId(projectDir.dir);
725
+ if (!projectId) {
726
+ return {
727
+ error: "not_linked",
728
+ message: "Link this repo to a cloud project: `insitue link <projectId>` (find the id in your InSitue dashboard project settings)."
729
+ };
730
+ }
731
+ return { token: auth.token, host: resolveHost(auth), projectId };
732
+ }
733
+ function cloudErrPayload(e) {
734
+ if (e instanceof CloudApiError) {
735
+ return {
736
+ error: e.code,
737
+ message: e.code === "not_paid" ? `${e.message} \u2014 upgrade to a paid InSitue plan to use cloud issues.` : e.message
738
+ };
739
+ }
740
+ return null;
741
+ }
742
+ server.registerTool(
743
+ "list_cloud_issues",
744
+ {
745
+ description: "List open InSitue cloud issues (captured bug reports) for the linked project, so you can fix them locally. Returns id, description (note), status, and source file:line where known. Then call `claim_cloud_issue` to start one.",
746
+ inputSchema: {}
747
+ },
748
+ async () => {
749
+ const setup = cloudSetup();
750
+ if ("error" in setup) {
751
+ return {
752
+ content: [{ type: "text", text: JSON.stringify(setup) }]
753
+ };
754
+ }
755
+ const { token, host, projectId } = setup;
756
+ try {
757
+ const result = await listIssues(host, token, projectId);
758
+ return {
759
+ content: [
760
+ {
761
+ type: "text",
762
+ text: JSON.stringify({ status: "ok", projectId, issues: result.issues })
763
+ }
764
+ ]
765
+ };
766
+ } catch (e) {
767
+ const payload = cloudErrPayload(e) ?? {
768
+ error: "error",
769
+ message: String(e)
770
+ };
771
+ return {
772
+ content: [{ type: "text", text: JSON.stringify(payload) }]
773
+ };
774
+ }
775
+ }
776
+ );
777
+ server.registerTool(
778
+ "claim_cloud_issue",
779
+ {
780
+ description: "Claim a cloud issue into local work (marks it 'fixing locally' in the dashboard) and return its repro: description, source file:line, page URL, console errors. Read the file, fix it, open a PR, then call resolve_cloud_issue.",
781
+ inputSchema: {
782
+ id: z.string().describe("The issue id from list_cloud_issues.")
783
+ }
784
+ },
785
+ async ({ id }) => {
786
+ const setup = cloudSetup();
787
+ if ("error" in setup) {
788
+ return {
789
+ content: [{ type: "text", text: JSON.stringify(setup) }]
790
+ };
791
+ }
792
+ const { token, host } = setup;
793
+ try {
794
+ const result = await claimIssue(host, token, id);
795
+ const bundle = result.bundle;
796
+ const targetObj = bundle?.["target"];
797
+ const sourceObj = targetObj?.["source"];
798
+ const runtimeObj = bundle?.["runtime"];
799
+ const errorsArr = (runtimeObj?.["errors"] ?? runtimeObj?.["console"])?.slice(0, 3);
800
+ const consoleErrors = Array.isArray(errorsArr) ? errorsArr.map((e) => typeof e === "string" ? e : JSON.stringify(e)).filter(Boolean) : [];
801
+ const source = typeof sourceObj?.["file"] === "string" ? {
802
+ file: sourceObj["file"],
803
+ ...typeof sourceObj["line"] === "number" ? { line: sourceObj["line"] } : {}
804
+ } : null;
805
+ const url = typeof runtimeObj?.["url"] === "string" ? runtimeObj["url"] : null;
806
+ return {
807
+ content: [
808
+ {
809
+ type: "text",
810
+ text: JSON.stringify({
811
+ status: "ok",
812
+ id,
813
+ note: result.note,
814
+ source,
815
+ url,
816
+ consoleErrors,
817
+ instructions: "Read the source file around the line, reproduce the issue from the description, make the fix, open a PR, then call resolve_cloud_issue with the PR url."
818
+ })
819
+ }
820
+ ]
821
+ };
822
+ } catch (e) {
823
+ const payload = cloudErrPayload(e) ?? {
824
+ error: "error",
825
+ message: String(e)
826
+ };
827
+ return {
828
+ content: [{ type: "text", text: JSON.stringify(payload) }]
829
+ };
830
+ }
831
+ }
832
+ );
833
+ server.registerTool(
834
+ "resolve_cloud_issue",
835
+ {
836
+ description: "Mark a claimed cloud issue resolved and attach the GitHub PR URL of your local fix. The dashboard shows it as resolved_local with the PR linked. Call after you've opened the PR.",
837
+ inputSchema: {
838
+ id: z.string().describe("The issue id."),
839
+ prUrl: z.string().describe("The GitHub PR URL for the fix."),
840
+ branch: z.string().optional().describe("The branch name (optional, inferred from the PR if omitted).")
841
+ }
842
+ },
843
+ async ({ id, prUrl, branch }) => {
844
+ const setup = cloudSetup();
845
+ if ("error" in setup) {
846
+ return {
847
+ content: [{ type: "text", text: JSON.stringify(setup) }]
848
+ };
849
+ }
850
+ const { token, host } = setup;
851
+ try {
852
+ const result = await resolveIssue(host, token, id, prUrl, branch);
853
+ return {
854
+ content: [
855
+ {
856
+ type: "text",
857
+ text: JSON.stringify({ status: "ok", runId: result.runId })
858
+ }
859
+ ]
860
+ };
861
+ } catch (e) {
862
+ const payload = cloudErrPayload(e) ?? {
863
+ error: "error",
864
+ message: String(e)
865
+ };
866
+ return {
867
+ content: [{ type: "text", text: JSON.stringify(payload) }]
868
+ };
869
+ }
870
+ }
871
+ );
872
+ server.registerTool(
873
+ "release_cloud_issue",
874
+ {
875
+ description: "Release a previously-claimed cloud issue back to the open queue (you decided not to fix it locally).",
876
+ inputSchema: {
877
+ id: z.string().describe("The issue id to release.")
878
+ }
879
+ },
880
+ async ({ id }) => {
881
+ const setup = cloudSetup();
882
+ if ("error" in setup) {
883
+ return {
884
+ content: [{ type: "text", text: JSON.stringify(setup) }]
885
+ };
886
+ }
887
+ const { token, host } = setup;
888
+ try {
889
+ await releaseIssue(host, token, id);
890
+ return {
891
+ content: [
892
+ {
893
+ type: "text",
894
+ text: JSON.stringify({ status: "ok", released: true })
895
+ }
896
+ ]
897
+ };
898
+ } catch (e) {
899
+ const payload = cloudErrPayload(e) ?? {
900
+ error: "error",
901
+ message: String(e)
902
+ };
903
+ return {
904
+ content: [{ type: "text", text: JSON.stringify(payload) }]
905
+ };
906
+ }
907
+ }
908
+ );
688
909
  server.registerPrompt(
689
910
  "connect",
690
911
  {
package/dist/setup-cli.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- diagnose,
4
3
  resolveProjectDir
5
- } from "./chunk-RF5Q55CG.js";
4
+ } from "./chunk-UNMH2DN4.js";
5
+ import {
6
+ diagnose
7
+ } from "./chunk-SGLSPTHD.js";
6
8
 
7
9
  // src/setup-cli.ts
8
10
  import {
package/package.json CHANGED
@@ -1,14 +1,33 @@
1
1
  {
2
2
  "name": "@insitue/claude-plugin",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
4
4
  "description": "Drive Claude (Code AND Desktop) from the InSitue browser overlay — pick an element in your app, claude reads the file and proposes the edit.",
5
+ "keywords": [
6
+ "insitue",
7
+ "claude",
8
+ "mcp",
9
+ "claude-code",
10
+ "claude-desktop",
11
+ "visual-feedback",
12
+ "browser-overlay"
13
+ ],
5
14
  "license": "MIT",
15
+ "homepage": "https://www.insitue.com",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/InSitue/insitue.git",
19
+ "directory": "packages/claude-plugin"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/InSitue/insitue/issues"
23
+ },
6
24
  "type": "module",
7
25
  "files": [
8
26
  ".claude-plugin",
9
27
  "commands",
10
28
  "dist",
11
- "README.md"
29
+ "README.md",
30
+ "CHANGELOG.md"
12
31
  ],
13
32
  "exports": {
14
33
  ".": "./dist/mcp-server.js"
@@ -33,8 +52,9 @@
33
52
  "node": ">=24"
34
53
  },
35
54
  "scripts": {
36
- "build": "tsup src/dispatcher.ts src/mcp-server.ts src/setup-cli.ts --format esm --clean --external @modelcontextprotocol/sdk --external ws --external zod",
37
- "dev": "tsup src/dispatcher.ts src/mcp-server.ts src/setup-cli.ts --format esm --watch --external @modelcontextprotocol/sdk --external ws --external zod",
55
+ "build": "tsup src/dispatcher.ts src/mcp-server.ts src/setup-cli.ts src/diagnose.ts src/cloud/api.ts src/cloud/config.ts --format esm --clean --external @modelcontextprotocol/sdk --external ws --external zod",
56
+ "dev": "tsup src/dispatcher.ts src/mcp-server.ts src/setup-cli.ts src/diagnose.ts src/cloud/api.ts src/cloud/config.ts --format esm --watch --external @modelcontextprotocol/sdk --external ws --external zod",
57
+ "test": "node --test \"test/*.test.mjs\"",
38
58
  "typecheck": "tsc --noEmit",
39
59
  "lint": "tsc --noEmit"
40
60
  }