@insitue/claude-plugin 0.4.6 → 0.5.1
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/CHANGELOG.md +32 -0
- package/README.md +63 -0
- package/commands/connect.md +50 -0
- package/dist/chunk-5APYM634.js +31 -0
- package/dist/chunk-IRPBZWNQ.js +66 -0
- package/dist/{chunk-U4C5CDNQ.js → chunk-SGLSPTHD.js} +62 -87
- package/dist/chunk-UNMH2DN4.js +72 -0
- package/dist/cloud/api.js +14 -0
- package/dist/cloud/config.js +10 -0
- package/dist/diagnose.js +6 -0
- package/dist/mcp-server.js +209 -2
- package/dist/setup-cli.js +4 -2
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @insitue/claude-plugin
|
|
2
2
|
|
|
3
|
+
## 0.5.1
|
|
4
|
+
|
|
5
|
+
- **Docs:** the operating instructions (`commands/connect.md`) now document
|
|
6
|
+
the cloud-issue workflow — `list_cloud_issues`, `claim_cloud_issue`,
|
|
7
|
+
`resolve_cloud_issue`, and `release_cloud_issue`. Previously these tools
|
|
8
|
+
were registered in the MCP server but not mentioned in the instructions
|
|
9
|
+
surfaced via `/insitue:connect`, `start_session`, or the `connect` prompt,
|
|
10
|
+
so Claude had no way to know the workflow existed. The new section covers
|
|
11
|
+
the claim→fix→PR→resolve loop, prerequisites (`insitue login` +
|
|
12
|
+
`insitue link`), the paid-plan requirement, and how to handle
|
|
13
|
+
`not_logged_in` / `not_linked` / `not_paid` error codes.
|
|
14
|
+
|
|
15
|
+
## 0.5.0
|
|
16
|
+
|
|
17
|
+
- **Feature:** four new MCP tools for working the InSitue Cloud issue queue
|
|
18
|
+
from a local Claude session:
|
|
19
|
+
- `list_cloud_issues` — list open bug reports for the linked project.
|
|
20
|
+
- `claim_cloud_issue(id)` — claim an issue; returns the full repro: note,
|
|
21
|
+
source `file:line`, page URL, and top console errors.
|
|
22
|
+
- `resolve_cloud_issue(id, prUrl, branch?)` — mark resolved and attach the
|
|
23
|
+
GitHub PR URL; `branch` is optional.
|
|
24
|
+
- `release_cloud_issue(id)` — return a claimed issue to the open queue.
|
|
25
|
+
- **Docs:** added "Fix cloud issues from Claude" section to the README
|
|
26
|
+
covering prerequisites (`insitue login` + `insitue link`), the
|
|
27
|
+
claim→fix→resolve loop, and a typical Claude session transcript.
|
|
28
|
+
- **Docs:** added the four cloud-issue tools to the MCP tools reference
|
|
29
|
+
table in the Architecture section.
|
|
30
|
+
|
|
31
|
+
Requires a paid InSitue Cloud plan. Uses the project link written by
|
|
32
|
+
`insitue link <projectId>` (the `@insitue/companion` CLI) and the auth
|
|
33
|
+
token from `insitue login`.
|
|
34
|
+
|
|
3
35
|
## 0.4.6
|
|
4
36
|
|
|
5
37
|
- **Fix (Windows):** `isInsideProject` now uses `path.sep` instead of a
|
package/README.md
CHANGED
|
@@ -233,6 +233,65 @@ extensively to stderr; claude surfaces them in the transcript.
|
|
|
233
233
|
|
|
234
234
|
---
|
|
235
235
|
|
|
236
|
+
## Fix cloud issues from Claude
|
|
237
|
+
|
|
238
|
+
> Requires a **paid InSitue Cloud plan** and the `@insitue/companion` CLI.
|
|
239
|
+
|
|
240
|
+
If your project uses InSitue Cloud, bug reports captured in production land in your Cloud issue queue. The four `cloud_issue_*` MCP tools let Claude list those issues, claim one, read the full repro, fix it locally, and mark it resolved — all without leaving the terminal.
|
|
241
|
+
|
|
242
|
+
### Prerequisites
|
|
243
|
+
|
|
244
|
+
1. **Authenticate:** run `insitue login` (from the `@insitue/companion` CLI). This stores a token at `~/.insitue/auth.json`. Get the token at <https://app.insitue.com/app/settings/developer>.
|
|
245
|
+
2. **Link your project:** run `insitue link <projectId>` in your project root. The tools read the linked project from `.insitue/project.json`.
|
|
246
|
+
|
|
247
|
+
### The loop
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
list_cloud_issues ← what's open?
|
|
251
|
+
↓
|
|
252
|
+
claim_cloud_issue(id) ← lock the issue; receive full repro
|
|
253
|
+
↓
|
|
254
|
+
(Claude reads source file, applies fix, opens a PR)
|
|
255
|
+
↓
|
|
256
|
+
resolve_cloud_issue(id, prUrl, branch?) ← attach the PR; close the issue
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
If you claim an issue and decide not to fix it, call `release_cloud_issue(id)` to return it to the queue so someone else can pick it up.
|
|
260
|
+
|
|
261
|
+
### MCP tools
|
|
262
|
+
|
|
263
|
+
| Tool | Arguments | What it does |
|
|
264
|
+
|---|---|---|
|
|
265
|
+
| `list_cloud_issues` | — | Lists open bug reports for the linked project. |
|
|
266
|
+
| `claim_cloud_issue` | `id` | Claims the issue and returns the full repro: description (note), source `file:line`, page URL, and top console errors. |
|
|
267
|
+
| `resolve_cloud_issue` | `id`, `prUrl`, `branch?` | Marks the issue resolved and attaches the GitHub PR URL. `branch` is optional — include it when the fix isn't on the default branch yet. |
|
|
268
|
+
| `release_cloud_issue` | `id` | Returns a claimed issue to the open queue without resolving it. |
|
|
269
|
+
|
|
270
|
+
### Typical Claude session
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
> /insitue:connect
|
|
274
|
+
# (pick-and-edit loop starts as normal)
|
|
275
|
+
|
|
276
|
+
# Switch to cloud issues:
|
|
277
|
+
> list open cloud issues
|
|
278
|
+
# Claude calls list_cloud_issues, picks one.
|
|
279
|
+
|
|
280
|
+
> claim issue <id>
|
|
281
|
+
# Claude calls claim_cloud_issue — gets note, file:line, page URL, console errors.
|
|
282
|
+
# Claude opens the source file at the reported line, reads context, writes a fix.
|
|
283
|
+
# Claude opens a PR via gh or the normal git flow.
|
|
284
|
+
|
|
285
|
+
> resolve issue <id> with PR https://github.com/…/pull/42
|
|
286
|
+
# Claude calls resolve_cloud_issue(id, "https://github.com/…/pull/42").
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The cloud-issue tools compose naturally with the pick→edit loop — you can interleave browser picks and issue fixes in the same session.
|
|
290
|
+
|
|
291
|
+
See [Fix cloud issues locally](../../docs/fix-cloud-issues-locally.md) for a full walkthrough.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
236
295
|
## Architecture (skip unless curious)
|
|
237
296
|
|
|
238
297
|
The plugin is a stdio MCP server that:
|
|
@@ -267,6 +326,10 @@ The plugin is a stdio MCP server that:
|
|
|
267
326
|
| `read_file` | Project-scoped file read. Desktop fallback (Code has native Read). |
|
|
268
327
|
| `apply_edit` | Project-scoped string-replacement edit. Desktop fallback (Code has native Edit). |
|
|
269
328
|
| `write_file` | Project-scoped full-file write. Desktop fallback. |
|
|
329
|
+
| `list_cloud_issues` | List open InSitue Cloud bug reports for the linked project. Paid plan + `insitue link` required. |
|
|
330
|
+
| `claim_cloud_issue` | Claim an issue by `id`; returns note, source `file:line`, page URL, and top console errors. |
|
|
331
|
+
| `resolve_cloud_issue` | Mark an issue resolved; attach `prUrl` and optional `branch`. |
|
|
332
|
+
| `release_cloud_issue` | Return a claimed issue to the open queue. |
|
|
270
333
|
|
|
271
334
|
All file tools resolve paths against the project dir and refuse
|
|
272
335
|
anything that resolves outside it (realpath-checked, so `..` games
|
package/commands/connect.md
CHANGED
|
@@ -115,6 +115,56 @@ Either path is fine; pick whichever your runtime has.
|
|
|
115
115
|
first and ask "want me to do X too?" rather than bundling
|
|
116
116
|
silently.
|
|
117
117
|
|
|
118
|
+
## Cloud-issue loop (paid plan, signed-in users)
|
|
119
|
+
|
|
120
|
+
In addition to browser picks, you can pull and fix pre-captured bug
|
|
121
|
+
reports from the InSitue dashboard. This is a separate workflow —
|
|
122
|
+
both can be used in the same session.
|
|
123
|
+
|
|
124
|
+
**Tools:**
|
|
125
|
+
|
|
126
|
+
- `mcp__insitue__list_cloud_issues` — show the open issue queue for
|
|
127
|
+
the linked project. Each entry has an `id`, a short `description`,
|
|
128
|
+
and a `source file:line` where known. Call this when the user says
|
|
129
|
+
"show me issues", "what's in the queue", or similar.
|
|
130
|
+
- `mcp__insitue__claim_cloud_issue <id>` — claim an issue (marks it
|
|
131
|
+
"fixing locally" in the dashboard) and return its repro context:
|
|
132
|
+
`note` (the reporter's description), `source.file:line`, `url`
|
|
133
|
+
(the page where it was captured), and `consoleErrors`. On a
|
|
134
|
+
successful claim: read the source file around the reported line,
|
|
135
|
+
reproduce from the note, make the fix, open a PR, then call
|
|
136
|
+
`resolve_cloud_issue`.
|
|
137
|
+
- `mcp__insitue__resolve_cloud_issue <id> <prUrl>` — mark the issue
|
|
138
|
+
resolved and attach the GitHub PR URL. Call this after the PR is
|
|
139
|
+
open, not before.
|
|
140
|
+
- `mcp__insitue__release_cloud_issue <id>` — abandon the claim and
|
|
141
|
+
return the issue to the open queue (use when you decide not to fix
|
|
142
|
+
it locally).
|
|
143
|
+
|
|
144
|
+
**Prerequisites.** These tools require:
|
|
145
|
+
- `insitue login` (creates `~/.insitue/auth.json` — generate a
|
|
146
|
+
token at https://app.insitue.com/app/settings/developer)
|
|
147
|
+
- `insitue link <projectId>` run in this repo (writes
|
|
148
|
+
`.insitue/project.json` — find the id in your project settings)
|
|
149
|
+
- A paid InSitue Cloud plan
|
|
150
|
+
|
|
151
|
+
If any tool returns `error: "not_logged_in"`, `"not_linked"`, or
|
|
152
|
+
`"not_paid"`, relay the `message` field verbatim to the user and
|
|
153
|
+
stop — do not retry. The message contains the exact corrective
|
|
154
|
+
action.
|
|
155
|
+
|
|
156
|
+
**Typical loop:**
|
|
157
|
+
|
|
158
|
+
1. `list_cloud_issues` → pick an issue with the user.
|
|
159
|
+
2. `claim_cloud_issue <id>` → read `source.file` around `source.line`,
|
|
160
|
+
understand the repro from `note` + `consoleErrors`, make the fix.
|
|
161
|
+
3. Open a PR.
|
|
162
|
+
4. `resolve_cloud_issue <id> <prUrl>`.
|
|
163
|
+
|
|
164
|
+
The cloud-issue workflow follows the same guardrails as browser picks:
|
|
165
|
+
never auto-apply writes, always wait for explicit user approval before
|
|
166
|
+
editing files.
|
|
167
|
+
|
|
118
168
|
## Failure modes to handle gracefully
|
|
119
169
|
|
|
120
170
|
- **`source.file` doesn't exist**: tell the user the path the
|
|
@@ -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
|
|
6
|
-
const pkgJson = join(
|
|
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((
|
|
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
|
-
|
|
67
|
+
resolve({ alive: true, subscribers: null });
|
|
36
68
|
}
|
|
37
69
|
);
|
|
38
|
-
req.on("error", () =>
|
|
70
|
+
req.on("error", () => resolve({ alive: false, subscribers: null }));
|
|
39
71
|
req.on("timeout", () => {
|
|
40
72
|
req.destroy();
|
|
41
|
-
|
|
73
|
+
resolve({ alive: false, subscribers: null });
|
|
42
74
|
});
|
|
43
75
|
req.end();
|
|
44
76
|
});
|
|
45
77
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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, sep } 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(sep) ? r : r + sep;
|
|
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
|
+
};
|
package/dist/diagnose.js
ADDED
package/dist/mcp-server.js
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
diagnose,
|
|
4
3
|
isInsideProject,
|
|
5
4
|
resolveProjectDir
|
|
6
|
-
} from "./chunk-
|
|
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";
|
|
@@ -699,6 +713,199 @@ server.registerTool(
|
|
|
699
713
|
};
|
|
700
714
|
}
|
|
701
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
|
+
);
|
|
702
909
|
server.registerPrompt(
|
|
703
910
|
"connect",
|
|
704
911
|
{
|
package/dist/setup-cli.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@insitue/claude-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
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
5
|
"keywords": [
|
|
6
6
|
"insitue",
|
|
@@ -52,8 +52,9 @@
|
|
|
52
52
|
"node": ">=24"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
|
-
"build": "tsup src/dispatcher.ts src/mcp-server.ts src/setup-cli.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 --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\"",
|
|
57
58
|
"typecheck": "tsc --noEmit",
|
|
58
59
|
"lint": "tsc --noEmit"
|
|
59
60
|
}
|