@insitue/claude-plugin 0.5.1 → 0.6.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.
- package/.claude-plugin/plugin.json +5 -2
- package/CHANGELOG.md +31 -0
- package/commands/connect.md +8 -4
- package/commands/login.md +54 -0
- package/dist/chunk-B3HSTDGI.js +59 -0
- package/dist/{chunk-SGLSPTHD.js → chunk-KGRPDRYH.js} +22 -0
- package/dist/chunk-RS3B7P4N.js +211 -0
- package/dist/cloud/config.js +7 -3
- package/dist/cloud/login.js +12 -0
- package/dist/cloud-cli.js +204 -0
- package/dist/diagnose.js +2 -1
- package/dist/dispatcher.js +21 -2
- package/dist/mcp-server.js +221 -62
- package/dist/setup-cli.js +5 -2
- package/package.json +5 -4
- package/dist/chunk-5APYM634.js +0 -31
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "insitue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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": {
|
|
7
7
|
"command": "npx",
|
|
8
|
-
"args": [
|
|
8
|
+
"args": [
|
|
9
|
+
"-y",
|
|
10
|
+
"@insitue/claude-plugin@latest"
|
|
11
|
+
],
|
|
9
12
|
"cwd": "${CLAUDE_PROJECT_DIR}"
|
|
10
13
|
}
|
|
11
14
|
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# @insitue/claude-plugin
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
- **Sign in from Claude.** New `/insitue:login` command + `authenticate` / `complete_authentication` MCP tools open a browser, you approve, and a token is stored automatically — no manual paste.
|
|
6
|
+
- **CLI subcommands.** `insitue login` / `insitue link` / `insitue whoami` for terminal use.
|
|
7
|
+
- **Auth-aware.** `diagnose` reports authentication + linked-project state; cloud-issue errors now point at `/insitue:login`.
|
|
8
|
+
|
|
9
|
+
## 0.5.2
|
|
10
|
+
|
|
11
|
+
- **Fix (hardcoded protocol version):** `mcp-server.ts` was sending
|
|
12
|
+
`protocolVersion: 5` as a literal in the WS hello handshake — the
|
|
13
|
+
same class of bug fixed in `@insitue/companion` 0.4.4 for the CLI
|
|
14
|
+
subscriber path. The literal would silently mismatch the next time
|
|
15
|
+
`@insitue/capture-core` bumps `PROTOCOL_VERSION`. The value is now
|
|
16
|
+
imported from `@insitue/capture-core` (added as a real dependency)
|
|
17
|
+
so there is a single source of truth. `@insitue/capture-core` is
|
|
18
|
+
bundled by tsup (not external) — no new runtime dep in the published
|
|
19
|
+
package.
|
|
20
|
+
- **Fix (subscriber attach race — `start_session` / `list_recent_picks`
|
|
21
|
+
says "connected" before it is):** `connectToCompanion` previously
|
|
22
|
+
returned `void` and was not awaited in `ensureSubscriberAttached`.
|
|
23
|
+
This meant `list_recent_picks` (and therefore the `/insitue:connect`
|
|
24
|
+
slash command) could return "No picks buffered yet" before the
|
|
25
|
+
companion had added this MCP server to its `subscribers` set — so
|
|
26
|
+
the browser launcher badge stayed dark for a moment after claude
|
|
27
|
+
claimed to be listening. `connectToCompanion` now returns a
|
|
28
|
+
`Promise<boolean>` that resolves `true` when `subscribe-ok` arrives
|
|
29
|
+
(subscriber set joined, badge lit), or `false` on pre-subscribe
|
|
30
|
+
close. `ensureSubscriberAttached` awaits it and resets `attached` on
|
|
31
|
+
failure so the next explicit call retries cleanly.
|
|
32
|
+
- Bumped 0.5.1 → 0.5.2.
|
|
33
|
+
|
|
3
34
|
## 0.5.1
|
|
4
35
|
|
|
5
36
|
- **Docs:** the operating instructions (`commands/connect.md`) now document
|
package/commands/connect.md
CHANGED
|
@@ -142,10 +142,14 @@ both can be used in the same session.
|
|
|
142
142
|
it locally).
|
|
143
143
|
|
|
144
144
|
**Prerequisites.** These tools require:
|
|
145
|
-
-
|
|
146
|
-
token
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
- Signing in to InSitue Cloud — run `/insitue:login` (Code) for a
|
|
146
|
+
browser-based sign-in (no token paste needed), or manually generate
|
|
147
|
+
a token at https://app.insitue.com/app/settings/developer and set it
|
|
148
|
+
with `npx @insitue/claude-plugin login --token <pat>`
|
|
149
|
+
- Linking this repo to a cloud project — `/insitue:login` auto-links
|
|
150
|
+
when it can match by git remote; otherwise run
|
|
151
|
+
`npx @insitue/claude-plugin link <projectId>` (find the id in your
|
|
152
|
+
project settings)
|
|
149
153
|
- A paid InSitue Cloud plan
|
|
150
154
|
|
|
151
155
|
If any tool returns `error: "not_logged_in"`, `"not_linked"`, or
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Sign in to InSitue Cloud via browser — no token paste required. Auto-links this repo to its cloud project.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# /insitue:login
|
|
6
|
+
|
|
7
|
+
Signs you in to InSitue Cloud using a secure browser-based flow (PKCE).
|
|
8
|
+
No token paste required — approve in your browser and you're done.
|
|
9
|
+
Cloud issues will become available via the claude-plugin tools once signed in.
|
|
10
|
+
|
|
11
|
+
## Your behaviour
|
|
12
|
+
|
|
13
|
+
1. Call `mcp__insitue__authenticate` (no arguments).
|
|
14
|
+
|
|
15
|
+
2. The tool returns `{ status: "browser_opened", url, userCode, message }`.
|
|
16
|
+
Show the user:
|
|
17
|
+
|
|
18
|
+
> I've opened your browser to sign in to InSitue. Please:
|
|
19
|
+
> 1. Complete the sign-in on the consent page.
|
|
20
|
+
> 2. Confirm the page shows the pairing code: **`<userCode>`**
|
|
21
|
+
>
|
|
22
|
+
> (If your browser didn't open, visit this URL manually: `<url>`)
|
|
23
|
+
|
|
24
|
+
Do NOT proceed to step 3 until the user says they've approved or you
|
|
25
|
+
detect they're ready. Wait for the user to confirm.
|
|
26
|
+
|
|
27
|
+
3. Call `mcp__insitue__complete_authentication` (no arguments).
|
|
28
|
+
This waits up to 5 minutes for the browser approval.
|
|
29
|
+
|
|
30
|
+
4. On `{ status: "ok" }`:
|
|
31
|
+
- If `linked: true`: reply "Signed in as **@`<login>`** and linked to
|
|
32
|
+
project `<projectId>`. Cloud issues are now available — call
|
|
33
|
+
`/insitue:connect` or `list_cloud_issues` to get started."
|
|
34
|
+
- If `linked: false` and `projectId` is null: reply "Signed in as
|
|
35
|
+
**@`<login>`**. To link this repo to a cloud project run:
|
|
36
|
+
`npx @insitue/claude-plugin link <projectId>` (find the project id in
|
|
37
|
+
your InSitue dashboard settings)."
|
|
38
|
+
|
|
39
|
+
5. On `{ status: "timeout" }`: tell the user the browser sign-in timed out
|
|
40
|
+
and they can try again by running `/insitue:login`.
|
|
41
|
+
|
|
42
|
+
6. On `{ status: "error" }`: relay the `message` field and suggest running
|
|
43
|
+
`/insitue:login` again.
|
|
44
|
+
|
|
45
|
+
## Notes
|
|
46
|
+
|
|
47
|
+
- The pairing `userCode` is a visual confirmation only — the user does NOT
|
|
48
|
+
type it anywhere. It just proves the browser consent page corresponds to
|
|
49
|
+
THIS sign-in request.
|
|
50
|
+
- Never call `complete_authentication` more than once per `authenticate` call.
|
|
51
|
+
If the user needs to retry, run `/insitue:login` from the start.
|
|
52
|
+
- Cloud issue tools (`list_cloud_issues`, `claim_cloud_issue`, etc.) require
|
|
53
|
+
both a signed-in account AND a linked project. This command handles both
|
|
54
|
+
steps where possible.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/cloud/config.ts
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } 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
|
+
function saveAuth(patch) {
|
|
27
|
+
const dir = join(homedir(), ".insitue");
|
|
28
|
+
const p = join(dir, "auth.json");
|
|
29
|
+
mkdirSync(dir, { recursive: true });
|
|
30
|
+
let existing = {};
|
|
31
|
+
if (existsSync(p)) {
|
|
32
|
+
try {
|
|
33
|
+
existing = JSON.parse(readFileSync(p, "utf8"));
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const merged = { ...existing, ...patch };
|
|
38
|
+
writeFileSync(p, JSON.stringify(merged, null, 2) + "\n", {
|
|
39
|
+
encoding: "utf8",
|
|
40
|
+
mode: 384
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function saveProjectLink(projectDir, projectId) {
|
|
44
|
+
const dir = join(projectDir, ".insitue");
|
|
45
|
+
mkdirSync(dir, { recursive: true });
|
|
46
|
+
writeFileSync(
|
|
47
|
+
join(dir, "project.json"),
|
|
48
|
+
JSON.stringify({ projectId }, null, 2) + "\n",
|
|
49
|
+
{ encoding: "utf8" }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
loadAuth,
|
|
55
|
+
loadProjectId,
|
|
56
|
+
resolveHost,
|
|
57
|
+
saveAuth,
|
|
58
|
+
saveProjectLink
|
|
59
|
+
};
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadAuth,
|
|
3
|
+
loadProjectId
|
|
4
|
+
} from "./chunk-B3HSTDGI.js";
|
|
5
|
+
|
|
1
6
|
// src/diagnose.ts
|
|
2
7
|
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
3
8
|
import { request as httpRequest } from "http";
|
|
@@ -126,7 +131,21 @@ async function diagnose(projectDir) {
|
|
|
126
131
|
"@insitue/swc-source-attr"
|
|
127
132
|
);
|
|
128
133
|
const swcPluginConfigured = detectSwcPluginConfigured(projectDir.dir);
|
|
134
|
+
const auth = loadAuth();
|
|
135
|
+
const authenticated = Boolean(auth.token);
|
|
136
|
+
const login = auth.login ?? null;
|
|
137
|
+
const linkedProjectId = loadProjectId(projectDir.dir);
|
|
129
138
|
const recommendations = [];
|
|
139
|
+
if (!authenticated) {
|
|
140
|
+
recommendations.push(
|
|
141
|
+
"Not authenticated \u2014 run `/insitue:login` (Code) or `npx @insitue/claude-plugin login` to sign in to InSitue Cloud."
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (authenticated && !linkedProjectId) {
|
|
145
|
+
recommendations.push(
|
|
146
|
+
"Signed in but no project linked \u2014 run `npx @insitue/claude-plugin link` to link this repo to a cloud project."
|
|
147
|
+
);
|
|
148
|
+
}
|
|
130
149
|
if (!sdkVersion) {
|
|
131
150
|
recommendations.push(
|
|
132
151
|
"`@insitue/sdk` not installed in the project \u2014 `pnpm add -D @insitue/sdk`"
|
|
@@ -160,6 +179,9 @@ async function diagnose(projectDir) {
|
|
|
160
179
|
sdkVersion,
|
|
161
180
|
swcPluginVersion,
|
|
162
181
|
swcPluginConfigured,
|
|
182
|
+
authenticated,
|
|
183
|
+
login,
|
|
184
|
+
linkedProjectId,
|
|
163
185
|
recommendations
|
|
164
186
|
};
|
|
165
187
|
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// src/cloud/login.ts
|
|
2
|
+
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
3
|
+
import { createServer } from "http";
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { platform } from "os";
|
|
6
|
+
import { execFileSync } from "child_process";
|
|
7
|
+
function toBase64Url(buf) {
|
|
8
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
9
|
+
}
|
|
10
|
+
function genPkce() {
|
|
11
|
+
const verifier = toBase64Url(randomBytes(32));
|
|
12
|
+
const challenge = toBase64Url(
|
|
13
|
+
createHash("sha256").update(verifier).digest()
|
|
14
|
+
);
|
|
15
|
+
return { verifier, challenge };
|
|
16
|
+
}
|
|
17
|
+
var SUCCESS_HTML = `<!DOCTYPE html>
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<head><meta charset="utf-8"><title>InSitue \u2014 signed in</title>
|
|
20
|
+
<style>body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f9fafb;}
|
|
21
|
+
.card{background:#fff;border-radius:12px;padding:40px 48px;text-align:center;box-shadow:0 1px 4px rgba(0,0,0,.12);}
|
|
22
|
+
h1{font-size:1.4rem;margin:0 0 8px;}p{color:#6b7280;margin:0;}</style></head>
|
|
23
|
+
<body><div class="card"><h1>You're signed in to InSitue</h1>
|
|
24
|
+
<p>You can close this tab and return to your terminal.</p></div></body></html>`;
|
|
25
|
+
var ERROR_HTML = `<!DOCTYPE html>
|
|
26
|
+
<html lang="en">
|
|
27
|
+
<head><meta charset="utf-8"><title>InSitue \u2014 error</title></head>
|
|
28
|
+
<body><p>Authorization failed. You may close this tab.</p></body></html>`;
|
|
29
|
+
function openBrowserUrl(url) {
|
|
30
|
+
try {
|
|
31
|
+
const p = platform();
|
|
32
|
+
const [cmd, ...args] = p === "darwin" ? ["open", url] : p === "win32" ? ["cmd", "/c", `start "" "${url}"`] : ["xdg-open", url];
|
|
33
|
+
spawn(cmd, [...args], { detached: true, stdio: "ignore" }).unref();
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function startLogin(opts) {
|
|
38
|
+
const { host, label, repo, openBrowser = true } = opts;
|
|
39
|
+
const { verifier, challenge } = genPkce();
|
|
40
|
+
const state = toBase64Url(randomBytes(16));
|
|
41
|
+
let resolveCode;
|
|
42
|
+
let rejectCode;
|
|
43
|
+
const codePromise = new Promise((res, rej) => {
|
|
44
|
+
resolveCode = res;
|
|
45
|
+
rejectCode = rej;
|
|
46
|
+
});
|
|
47
|
+
const server = createServer((req, res) => {
|
|
48
|
+
const raw = req.url ?? "";
|
|
49
|
+
const u = new URL(raw, "http://127.0.0.1");
|
|
50
|
+
if (u.pathname !== "/callback") {
|
|
51
|
+
res.writeHead(404).end();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const errorParam = u.searchParams.get("error");
|
|
55
|
+
if (errorParam) {
|
|
56
|
+
res.writeHead(200, { "content-type": "text/html" }).end(ERROR_HTML);
|
|
57
|
+
server.close();
|
|
58
|
+
rejectCode(new Error(`authorization_denied: ${errorParam}`));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const incomingState = u.searchParams.get("state") ?? "";
|
|
62
|
+
let stateOk = false;
|
|
63
|
+
try {
|
|
64
|
+
const a = Buffer.from(incomingState);
|
|
65
|
+
const b = Buffer.from(state);
|
|
66
|
+
stateOk = a.length === b.length && timingSafeEqual(a, b);
|
|
67
|
+
} catch {
|
|
68
|
+
stateOk = false;
|
|
69
|
+
}
|
|
70
|
+
if (!stateOk) {
|
|
71
|
+
res.writeHead(400, { "content-type": "text/plain" }).end("state mismatch");
|
|
72
|
+
server.close();
|
|
73
|
+
rejectCode(new Error("state mismatch"));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const code = u.searchParams.get("code") ?? "";
|
|
77
|
+
res.writeHead(200, { "content-type": "text/html" }).end(SUCCESS_HTML);
|
|
78
|
+
server.close();
|
|
79
|
+
resolveCode(code);
|
|
80
|
+
});
|
|
81
|
+
await new Promise((res, rej) => {
|
|
82
|
+
server.on("error", rej);
|
|
83
|
+
server.listen(0, "127.0.0.1", () => res());
|
|
84
|
+
});
|
|
85
|
+
const addr = server.address();
|
|
86
|
+
if (!addr || typeof addr === "string") {
|
|
87
|
+
throw new Error("Failed to bind loopback server");
|
|
88
|
+
}
|
|
89
|
+
const port = addr.port;
|
|
90
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
91
|
+
const startRes = await fetch(`${host}/api/v1/cli/authorize/start`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "content-type": "application/json" },
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
code_challenge: challenge,
|
|
96
|
+
code_challenge_method: "S256",
|
|
97
|
+
redirect_uri: redirectUri,
|
|
98
|
+
...label ? { label } : {},
|
|
99
|
+
...repo ? { repo } : {}
|
|
100
|
+
})
|
|
101
|
+
});
|
|
102
|
+
if (!startRes.ok) {
|
|
103
|
+
server.close();
|
|
104
|
+
const text = await startRes.text().catch(() => "");
|
|
105
|
+
throw new Error(
|
|
106
|
+
`authorize/start failed: HTTP ${startRes.status} ${text}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const startJson = await startRes.json();
|
|
110
|
+
const { request_id: requestId, user_code: userCode } = startJson;
|
|
111
|
+
const authorizeUrl = `${host}/cli/authorize?request_id=${encodeURIComponent(requestId)}&state=${encodeURIComponent(state)}`;
|
|
112
|
+
if (openBrowser) {
|
|
113
|
+
openBrowserUrl(authorizeUrl);
|
|
114
|
+
}
|
|
115
|
+
function cancel() {
|
|
116
|
+
try {
|
|
117
|
+
server.close();
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
rejectCode(new Error("cancelled"));
|
|
121
|
+
}
|
|
122
|
+
async function wait() {
|
|
123
|
+
let timeoutHandle;
|
|
124
|
+
try {
|
|
125
|
+
const code = await Promise.race([
|
|
126
|
+
codePromise,
|
|
127
|
+
new Promise((_, rej) => {
|
|
128
|
+
timeoutHandle = setTimeout(
|
|
129
|
+
() => rej(new Error("timeout")),
|
|
130
|
+
3e5
|
|
131
|
+
);
|
|
132
|
+
})
|
|
133
|
+
]);
|
|
134
|
+
clearTimeout(timeoutHandle);
|
|
135
|
+
const tokenRes = await fetch(`${host}/api/v1/cli/token`, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers: { "content-type": "application/json" },
|
|
138
|
+
body: JSON.stringify({ code, code_verifier: verifier, redirect_uri: redirectUri })
|
|
139
|
+
});
|
|
140
|
+
if (!tokenRes.ok) {
|
|
141
|
+
throw new Error("invalid_grant");
|
|
142
|
+
}
|
|
143
|
+
const tokenJson = await tokenRes.json();
|
|
144
|
+
return {
|
|
145
|
+
token: tokenJson.token,
|
|
146
|
+
host: tokenJson.host,
|
|
147
|
+
login: tokenJson.login,
|
|
148
|
+
projectId: tokenJson.projectId ?? null
|
|
149
|
+
};
|
|
150
|
+
} finally {
|
|
151
|
+
clearTimeout(timeoutHandle);
|
|
152
|
+
try {
|
|
153
|
+
server.close();
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { authorizeUrl, userCode, port, wait, cancel };
|
|
159
|
+
}
|
|
160
|
+
function parseRemoteToOwnerName(remote) {
|
|
161
|
+
remote = remote.trim();
|
|
162
|
+
const sshMatch = remote.match(
|
|
163
|
+
/^git@[^:]+:([A-Za-z0-9._-]+\/[A-Za-z0-9._-]+?)(?:\.git)?$/
|
|
164
|
+
);
|
|
165
|
+
if (sshMatch) return sshMatch[1];
|
|
166
|
+
try {
|
|
167
|
+
const u = new URL(remote);
|
|
168
|
+
const parts = u.pathname.replace(/^\//, "").replace(/\.git$/, "").split("/");
|
|
169
|
+
if (parts.length >= 2 && parts[0] && parts[1]) {
|
|
170
|
+
return `${parts[0]}/${parts[1]}`;
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
function detectGitRemote(cwd) {
|
|
177
|
+
try {
|
|
178
|
+
const out = execFileSync("git", ["-C", cwd, "remote", "get-url", "origin"], {
|
|
179
|
+
encoding: "utf8",
|
|
180
|
+
timeout: 5e3,
|
|
181
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
182
|
+
});
|
|
183
|
+
return parseRemoteToOwnerName(out);
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function pickProjectIdForRepo(host, token, repo) {
|
|
189
|
+
const res = await fetch(`${host}/api/v1/dev/projects`, {
|
|
190
|
+
headers: { authorization: `Bearer ${token}` }
|
|
191
|
+
});
|
|
192
|
+
if (!res.ok) {
|
|
193
|
+
return { projectId: null, candidates: [] };
|
|
194
|
+
}
|
|
195
|
+
const json = await res.json();
|
|
196
|
+
const projects = json.projects ?? [];
|
|
197
|
+
const matches = projects.filter(
|
|
198
|
+
(p) => p.repo && p.repo.toLowerCase() === repo.toLowerCase()
|
|
199
|
+
);
|
|
200
|
+
return {
|
|
201
|
+
projectId: matches.length === 1 ? matches[0].id : null,
|
|
202
|
+
candidates: projects
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export {
|
|
207
|
+
genPkce,
|
|
208
|
+
startLogin,
|
|
209
|
+
detectGitRemote,
|
|
210
|
+
pickProjectIdForRepo
|
|
211
|
+
};
|
package/dist/cloud/config.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loadAuth,
|
|
3
3
|
loadProjectId,
|
|
4
|
-
resolveHost
|
|
5
|
-
|
|
4
|
+
resolveHost,
|
|
5
|
+
saveAuth,
|
|
6
|
+
saveProjectLink
|
|
7
|
+
} from "../chunk-B3HSTDGI.js";
|
|
6
8
|
export {
|
|
7
9
|
loadAuth,
|
|
8
10
|
loadProjectId,
|
|
9
|
-
resolveHost
|
|
11
|
+
resolveHost,
|
|
12
|
+
saveAuth,
|
|
13
|
+
saveProjectLink
|
|
10
14
|
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveProjectDir
|
|
3
|
+
} from "./chunk-UNMH2DN4.js";
|
|
4
|
+
import {
|
|
5
|
+
loadAuth,
|
|
6
|
+
loadProjectId,
|
|
7
|
+
resolveHost,
|
|
8
|
+
saveAuth,
|
|
9
|
+
saveProjectLink
|
|
10
|
+
} from "./chunk-B3HSTDGI.js";
|
|
11
|
+
import {
|
|
12
|
+
detectGitRemote,
|
|
13
|
+
pickProjectIdForRepo,
|
|
14
|
+
startLogin
|
|
15
|
+
} from "./chunk-RS3B7P4N.js";
|
|
16
|
+
|
|
17
|
+
// src/cloud-cli.ts
|
|
18
|
+
import { hostname } from "os";
|
|
19
|
+
import { resolve } from "path";
|
|
20
|
+
function parseArgs(argv) {
|
|
21
|
+
const positional = [];
|
|
22
|
+
const flags = /* @__PURE__ */ new Map();
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const a = argv[i];
|
|
25
|
+
if (!a.startsWith("--")) {
|
|
26
|
+
positional.push(a);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const eq = a.indexOf("=");
|
|
30
|
+
if (eq !== -1) {
|
|
31
|
+
flags.set(a.slice(2, eq), a.slice(eq + 1));
|
|
32
|
+
} else if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
|
|
33
|
+
flags.set(a.slice(2), argv[++i]);
|
|
34
|
+
} else {
|
|
35
|
+
flags.set(a.slice(2), true);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { positional, flags };
|
|
39
|
+
}
|
|
40
|
+
async function cmdLogin(argv) {
|
|
41
|
+
const { flags } = parseArgs(argv);
|
|
42
|
+
const tokenFlag = flags.get("token");
|
|
43
|
+
if (typeof tokenFlag === "string") {
|
|
44
|
+
const hostFlag = flags.get("host");
|
|
45
|
+
const auth2 = loadAuth();
|
|
46
|
+
const host2 = typeof hostFlag === "string" ? hostFlag : resolveHost(auth2);
|
|
47
|
+
saveAuth({ token: tokenFlag, host: host2 });
|
|
48
|
+
process.stdout.write(
|
|
49
|
+
`Saved token to ~/.insitue/auth.json (host: ${host2})
|
|
50
|
+
`
|
|
51
|
+
);
|
|
52
|
+
return 0;
|
|
53
|
+
}
|
|
54
|
+
const auth = loadAuth();
|
|
55
|
+
const host = resolveHost(auth);
|
|
56
|
+
const projectDir = resolveProjectDir();
|
|
57
|
+
const repo = detectGitRemote(projectDir.dir);
|
|
58
|
+
process.stdout.write("Opening your browser to sign in to InSitue\u2026\n");
|
|
59
|
+
let flow;
|
|
60
|
+
try {
|
|
61
|
+
flow = await startLogin({ host, label: hostname(), ...repo ? { repo } : {} });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
process.stderr.write(
|
|
64
|
+
`error: could not start login: ${err.message}
|
|
65
|
+
`
|
|
66
|
+
);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
process.stdout.write(
|
|
70
|
+
`
|
|
71
|
+
Browser URL: ${flow.authorizeUrl}
|
|
72
|
+
Pairing code (confirm your browser shows this): ${flow.userCode}
|
|
73
|
+
|
|
74
|
+
Waiting for approval\u2026
|
|
75
|
+
`
|
|
76
|
+
);
|
|
77
|
+
let result;
|
|
78
|
+
try {
|
|
79
|
+
result = await flow.wait();
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const msg = err.message ?? String(err);
|
|
82
|
+
if (msg === "timeout") {
|
|
83
|
+
process.stderr.write("error: timed out waiting for browser approval\n");
|
|
84
|
+
} else {
|
|
85
|
+
process.stderr.write(`error: ${msg}
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
saveAuth({ token: result.token, host: result.host, login: result.login });
|
|
91
|
+
process.stdout.write(`Signed in as ${result.login}.
|
|
92
|
+
`);
|
|
93
|
+
const projectDir2 = resolveProjectDir();
|
|
94
|
+
let linkedProjectId = result.projectId ?? null;
|
|
95
|
+
if (!linkedProjectId && repo) {
|
|
96
|
+
const { projectId } = await pickProjectIdForRepo(
|
|
97
|
+
result.host,
|
|
98
|
+
result.token,
|
|
99
|
+
repo
|
|
100
|
+
).catch(() => ({ projectId: null, candidates: [] }));
|
|
101
|
+
linkedProjectId = projectId;
|
|
102
|
+
}
|
|
103
|
+
if (linkedProjectId) {
|
|
104
|
+
saveProjectLink(projectDir2.dir, linkedProjectId);
|
|
105
|
+
process.stdout.write(`Linked to project ${linkedProjectId}.
|
|
106
|
+
`);
|
|
107
|
+
} else {
|
|
108
|
+
process.stdout.write(
|
|
109
|
+
"Could not auto-link a project. Run `npx @insitue/claude-plugin link <projectId>` manually.\n"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
async function cmdLink(argv) {
|
|
115
|
+
const { positional, flags } = parseArgs(argv);
|
|
116
|
+
const projectFlag = flags.get("project");
|
|
117
|
+
const projectDir = resolveProjectDir(
|
|
118
|
+
typeof projectFlag === "string" ? ["--project-dir", resolve(projectFlag)] : []
|
|
119
|
+
);
|
|
120
|
+
const auth = loadAuth();
|
|
121
|
+
if (!auth.token) {
|
|
122
|
+
process.stderr.write(
|
|
123
|
+
"error: not signed in \u2014 run `npx @insitue/claude-plugin login` first\n"
|
|
124
|
+
);
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
const host = resolveHost(auth);
|
|
128
|
+
const explicitId = positional[0];
|
|
129
|
+
if (explicitId) {
|
|
130
|
+
saveProjectLink(projectDir.dir, explicitId);
|
|
131
|
+
process.stdout.write(`Linked to project ${explicitId}.
|
|
132
|
+
`);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
const repo = detectGitRemote(projectDir.dir);
|
|
136
|
+
if (!repo) {
|
|
137
|
+
process.stderr.write(
|
|
138
|
+
"error: could not detect git remote origin. Pass a projectId: `npx @insitue/claude-plugin link <projectId>`\n"
|
|
139
|
+
);
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
process.stdout.write(`Detecting project for repo ${repo}\u2026
|
|
143
|
+
`);
|
|
144
|
+
let projectId;
|
|
145
|
+
let candidates;
|
|
146
|
+
try {
|
|
147
|
+
({ projectId, candidates } = await pickProjectIdForRepo(
|
|
148
|
+
host,
|
|
149
|
+
auth.token,
|
|
150
|
+
repo
|
|
151
|
+
));
|
|
152
|
+
} catch (err) {
|
|
153
|
+
process.stderr.write(`error: ${err.message}
|
|
154
|
+
`);
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
if (projectId) {
|
|
158
|
+
saveProjectLink(projectDir.dir, projectId);
|
|
159
|
+
process.stdout.write(`Linked to project ${projectId}.
|
|
160
|
+
`);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
if (candidates.length === 0) {
|
|
164
|
+
process.stderr.write(
|
|
165
|
+
`No projects found for this account. Visit https://app.insitue.com to create one.
|
|
166
|
+
`
|
|
167
|
+
);
|
|
168
|
+
} else {
|
|
169
|
+
process.stdout.write(
|
|
170
|
+
`No project matched repo "${repo}". Available projects:
|
|
171
|
+
`
|
|
172
|
+
);
|
|
173
|
+
for (const p of candidates) {
|
|
174
|
+
process.stdout.write(` ${p.id} ${p.name}${p.repo ? ` (${p.repo})` : ""}
|
|
175
|
+
`);
|
|
176
|
+
}
|
|
177
|
+
process.stdout.write(
|
|
178
|
+
`
|
|
179
|
+
Run: npx @insitue/claude-plugin link <projectId>
|
|
180
|
+
`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
function cmdWhoami() {
|
|
186
|
+
const auth = loadAuth();
|
|
187
|
+
const projectDir = resolveProjectDir();
|
|
188
|
+
const projectId = loadProjectId(projectDir.dir);
|
|
189
|
+
if (!auth.token) {
|
|
190
|
+
process.stdout.write("Not signed in.\n");
|
|
191
|
+
return 1;
|
|
192
|
+
}
|
|
193
|
+
process.stdout.write(
|
|
194
|
+
`Signed in as ${auth.login ?? "(unknown login)"} host: ${resolveHost(auth)}
|
|
195
|
+
Project: ${projectId ?? "(not linked)"}
|
|
196
|
+
`
|
|
197
|
+
);
|
|
198
|
+
return 0;
|
|
199
|
+
}
|
|
200
|
+
export {
|
|
201
|
+
cmdLink,
|
|
202
|
+
cmdLogin,
|
|
203
|
+
cmdWhoami
|
|
204
|
+
};
|
package/dist/diagnose.js
CHANGED
package/dist/dispatcher.js
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/dispatcher.ts
|
|
4
|
-
var
|
|
4
|
+
var CLI_SUBCOMMANDS = /* @__PURE__ */ new Set(["setup", "diagnose", "help", "--help", "-h"]);
|
|
5
|
+
var CLOUD_SUBCOMMANDS = /* @__PURE__ */ new Set(["login", "link", "whoami"]);
|
|
5
6
|
var first = process.argv[2];
|
|
6
|
-
if (first &&
|
|
7
|
+
if (first && CLI_SUBCOMMANDS.has(first)) {
|
|
7
8
|
await import("./setup-cli.js");
|
|
9
|
+
} else if (first && CLOUD_SUBCOMMANDS.has(first)) {
|
|
10
|
+
const { cmdLogin, cmdLink, cmdWhoami } = await import("./cloud-cli.js");
|
|
11
|
+
const rest = process.argv.slice(3);
|
|
12
|
+
let code;
|
|
13
|
+
switch (first) {
|
|
14
|
+
case "login":
|
|
15
|
+
code = await cmdLogin(rest);
|
|
16
|
+
break;
|
|
17
|
+
case "link":
|
|
18
|
+
code = await cmdLink(rest);
|
|
19
|
+
break;
|
|
20
|
+
case "whoami":
|
|
21
|
+
code = cmdWhoami();
|
|
22
|
+
break;
|
|
23
|
+
default:
|
|
24
|
+
code = 0;
|
|
25
|
+
}
|
|
26
|
+
process.exit(code);
|
|
8
27
|
} else {
|
|
9
28
|
await import("./mcp-server.js");
|
|
10
29
|
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./chunk-UNMH2DN4.js";
|
|
6
6
|
import {
|
|
7
7
|
diagnose
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-KGRPDRYH.js";
|
|
9
9
|
import {
|
|
10
10
|
CloudApiError,
|
|
11
11
|
claimIssue,
|
|
@@ -16,8 +16,15 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
loadAuth,
|
|
18
18
|
loadProjectId,
|
|
19
|
-
resolveHost
|
|
20
|
-
|
|
19
|
+
resolveHost,
|
|
20
|
+
saveAuth,
|
|
21
|
+
saveProjectLink
|
|
22
|
+
} from "./chunk-B3HSTDGI.js";
|
|
23
|
+
import {
|
|
24
|
+
detectGitRemote,
|
|
25
|
+
pickProjectIdForRepo,
|
|
26
|
+
startLogin
|
|
27
|
+
} from "./chunk-RS3B7P4N.js";
|
|
21
28
|
|
|
22
29
|
// src/mcp-server.ts
|
|
23
30
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -25,10 +32,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
25
32
|
import { spawn } from "child_process";
|
|
26
33
|
import { existsSync as existsSync2, readFileSync as readFileSync3, rmSync } from "fs";
|
|
27
34
|
import { request as httpRequest } from "http";
|
|
35
|
+
import { hostname } from "os";
|
|
28
36
|
import { dirname as dirname3, join as join3 } from "path";
|
|
29
37
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
30
38
|
import WebSocket from "ws";
|
|
31
39
|
import { z } from "zod";
|
|
40
|
+
import { PROTOCOL_VERSION } from "@insitue/capture-core";
|
|
32
41
|
|
|
33
42
|
// src/file-tools.ts
|
|
34
43
|
import {
|
|
@@ -388,68 +397,81 @@ var activeWs = null;
|
|
|
388
397
|
var reconnectTimer = null;
|
|
389
398
|
var disconnecting = false;
|
|
390
399
|
function connectToCompanion(s) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
ws
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (tag === "hello-ok") {
|
|
417
|
-
ws.send(JSON.stringify({ t: "subscribe" }));
|
|
400
|
+
return new Promise((subscribeResolve) => {
|
|
401
|
+
let subscribed = false;
|
|
402
|
+
const url = `ws://127.0.0.1:${s.port}/insitue/cli`;
|
|
403
|
+
const ws = new WebSocket(url, {
|
|
404
|
+
headers: { "user-agent": "insitue-claude-plugin" }
|
|
405
|
+
});
|
|
406
|
+
activeWs = ws;
|
|
407
|
+
ws.on("open", () => {
|
|
408
|
+
ws.send(
|
|
409
|
+
JSON.stringify({
|
|
410
|
+
t: "hello",
|
|
411
|
+
// Imported from @insitue/capture-core — the single source of
|
|
412
|
+
// truth for the wire protocol version. A hardcoded literal here
|
|
413
|
+
// would silently drift the moment the capture-core version is
|
|
414
|
+
// bumped (same class of bug fixed in companion 0.4.4).
|
|
415
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
416
|
+
token: s.token
|
|
417
|
+
})
|
|
418
|
+
);
|
|
419
|
+
});
|
|
420
|
+
ws.on("message", (data) => {
|
|
421
|
+
let m;
|
|
422
|
+
try {
|
|
423
|
+
m = JSON.parse(String(data));
|
|
424
|
+
} catch {
|
|
418
425
|
return;
|
|
419
426
|
}
|
|
420
|
-
if (
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
427
|
+
if (m && typeof m === "object") {
|
|
428
|
+
const tag = m.t;
|
|
429
|
+
if (tag === "hello-ok") {
|
|
430
|
+
ws.send(JSON.stringify({ t: "subscribe" }));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (tag === "subscribe-ok") {
|
|
434
|
+
if (!subscribed) {
|
|
435
|
+
subscribed = true;
|
|
436
|
+
subscribeResolve(true);
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (tag === "broadcast-capture") {
|
|
441
|
+
try {
|
|
442
|
+
const summary = summariseBundle(
|
|
443
|
+
m
|
|
444
|
+
);
|
|
445
|
+
buffer.push(summary);
|
|
446
|
+
const note = summary.userNote ? summary.userNote.length > 60 ? `${summary.userNote.slice(0, 57)}\u2026` : summary.userNote : "(no description)";
|
|
447
|
+
const where = summary.source ? `${summary.source.file}:${summary.source.line}` : summary.target;
|
|
448
|
+
process.stderr.write(
|
|
449
|
+
`[insitue] \u{1F4E5} pick received \u2014 "${note}" @ ${where}
|
|
430
450
|
`
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
451
|
+
);
|
|
452
|
+
} catch (err) {
|
|
453
|
+
process.stderr.write(
|
|
454
|
+
`[insitue-mcp] dropped malformed pick: ${err.message}
|
|
435
455
|
`
|
|
436
|
-
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
437
459
|
}
|
|
438
|
-
return;
|
|
460
|
+
if (tag === "broadcast-ask") return;
|
|
439
461
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
462
|
+
});
|
|
463
|
+
ws.on("close", () => {
|
|
464
|
+
if (!subscribed) subscribeResolve(false);
|
|
465
|
+
if (activeWs === ws) activeWs = null;
|
|
466
|
+
buffer.dropWaiters();
|
|
467
|
+
if (disconnecting) return;
|
|
468
|
+
process.stderr.write(
|
|
469
|
+
"[insitue-mcp] companion link dropped \u2014 reconnecting in 2s\n"
|
|
470
|
+
);
|
|
471
|
+
reconnectTimer = setTimeout(() => void connectToCompanion(s), 2e3);
|
|
472
|
+
});
|
|
473
|
+
ws.on("error", () => {
|
|
474
|
+
});
|
|
453
475
|
});
|
|
454
476
|
}
|
|
455
477
|
var projectDir = resolveProjectDir();
|
|
@@ -473,7 +495,13 @@ async function ensureSubscriberAttached(opts = {}) {
|
|
|
473
495
|
if (!session) return;
|
|
474
496
|
}
|
|
475
497
|
attached = true;
|
|
476
|
-
connectToCompanion(session);
|
|
498
|
+
const ok = await connectToCompanion(session);
|
|
499
|
+
if (!ok) {
|
|
500
|
+
attached = false;
|
|
501
|
+
process.stderr.write(
|
|
502
|
+
"[insitue-mcp] subscriber attach failed \u2014 companion handshake did not complete\n"
|
|
503
|
+
);
|
|
504
|
+
}
|
|
477
505
|
}
|
|
478
506
|
function endSession() {
|
|
479
507
|
disconnecting = true;
|
|
@@ -713,19 +741,124 @@ server.registerTool(
|
|
|
713
741
|
};
|
|
714
742
|
}
|
|
715
743
|
);
|
|
744
|
+
var pendingLoginFlow = null;
|
|
745
|
+
server.registerTool(
|
|
746
|
+
"authenticate",
|
|
747
|
+
{
|
|
748
|
+
description: "Start a browser-based InSitue sign-in (PKCE). Opens your browser to the InSitue consent page and returns a `userCode` to visually confirm. After approving in the browser, call `complete_authentication` to finish.",
|
|
749
|
+
inputSchema: {}
|
|
750
|
+
},
|
|
751
|
+
async () => {
|
|
752
|
+
const auth = loadAuth();
|
|
753
|
+
const host = resolveHost(auth);
|
|
754
|
+
const repo = detectGitRemote(projectDir.dir);
|
|
755
|
+
const flow = await startLogin({
|
|
756
|
+
host,
|
|
757
|
+
label: hostname(),
|
|
758
|
+
...repo ? { repo } : {}
|
|
759
|
+
});
|
|
760
|
+
pendingLoginFlow = flow;
|
|
761
|
+
return {
|
|
762
|
+
content: [
|
|
763
|
+
{
|
|
764
|
+
type: "text",
|
|
765
|
+
text: JSON.stringify({
|
|
766
|
+
status: "browser_opened",
|
|
767
|
+
url: flow.authorizeUrl,
|
|
768
|
+
userCode: flow.userCode,
|
|
769
|
+
message: "Approve in your browser; confirm the code matches, then call complete_authentication."
|
|
770
|
+
})
|
|
771
|
+
}
|
|
772
|
+
]
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
);
|
|
776
|
+
server.registerTool(
|
|
777
|
+
"complete_authentication",
|
|
778
|
+
{
|
|
779
|
+
description: "Complete the in-progress InSitue sign-in started by `authenticate`. Waits for the browser approval (up to 5 minutes), saves credentials, and auto-links this repo to its cloud project when possible.",
|
|
780
|
+
inputSchema: {}
|
|
781
|
+
},
|
|
782
|
+
async () => {
|
|
783
|
+
const flow = pendingLoginFlow;
|
|
784
|
+
if (!flow) {
|
|
785
|
+
return {
|
|
786
|
+
content: [
|
|
787
|
+
{
|
|
788
|
+
type: "text",
|
|
789
|
+
text: JSON.stringify({
|
|
790
|
+
status: "error",
|
|
791
|
+
message: "No sign-in in progress. Call authenticate first."
|
|
792
|
+
})
|
|
793
|
+
}
|
|
794
|
+
]
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
let result;
|
|
799
|
+
try {
|
|
800
|
+
result = await flow.wait();
|
|
801
|
+
} catch (err) {
|
|
802
|
+
const msg = err.message ?? String(err);
|
|
803
|
+
const status = msg === "timeout" ? "timeout" : "error";
|
|
804
|
+
return {
|
|
805
|
+
content: [
|
|
806
|
+
{
|
|
807
|
+
type: "text",
|
|
808
|
+
text: JSON.stringify({ status, message: msg })
|
|
809
|
+
}
|
|
810
|
+
]
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
saveAuth({ token: result.token, host: result.host, login: result.login });
|
|
814
|
+
let linkedProjectId = result.projectId ?? null;
|
|
815
|
+
let linked = false;
|
|
816
|
+
if (!linkedProjectId) {
|
|
817
|
+
const repo = detectGitRemote(projectDir.dir);
|
|
818
|
+
if (repo) {
|
|
819
|
+
const { projectId } = await pickProjectIdForRepo(
|
|
820
|
+
result.host,
|
|
821
|
+
result.token,
|
|
822
|
+
repo
|
|
823
|
+
).catch(() => ({ projectId: null, candidates: [] }));
|
|
824
|
+
linkedProjectId = projectId;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
if (linkedProjectId) {
|
|
828
|
+
saveProjectLink(projectDir.dir, linkedProjectId);
|
|
829
|
+
linked = true;
|
|
830
|
+
}
|
|
831
|
+
return {
|
|
832
|
+
content: [
|
|
833
|
+
{
|
|
834
|
+
type: "text",
|
|
835
|
+
text: JSON.stringify({
|
|
836
|
+
status: "ok",
|
|
837
|
+
login: result.login,
|
|
838
|
+
projectId: linkedProjectId,
|
|
839
|
+
linked
|
|
840
|
+
})
|
|
841
|
+
}
|
|
842
|
+
]
|
|
843
|
+
};
|
|
844
|
+
} finally {
|
|
845
|
+
pendingLoginFlow = null;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
);
|
|
716
849
|
function cloudSetup() {
|
|
717
850
|
const auth = loadAuth();
|
|
718
851
|
if (!auth.token) {
|
|
719
852
|
return {
|
|
720
853
|
error: "not_logged_in",
|
|
721
|
-
message: "Run
|
|
854
|
+
message: "Not signed in to InSitue. Run `/insitue:login` (Code) to sign in via browser, or `npx @insitue/claude-plugin login` in your terminal."
|
|
722
855
|
};
|
|
723
856
|
}
|
|
724
857
|
const projectId = loadProjectId(projectDir.dir);
|
|
725
858
|
if (!projectId) {
|
|
726
859
|
return {
|
|
727
860
|
error: "not_linked",
|
|
728
|
-
message: "
|
|
861
|
+
message: "This repo is not linked to an InSitue cloud project. Run `/insitue:login` (Code) to sign in and auto-link, or `npx @insitue/claude-plugin link <projectId>` in your terminal."
|
|
729
862
|
};
|
|
730
863
|
}
|
|
731
864
|
return { token: auth.token, host: resolveHost(auth), projectId };
|
|
@@ -921,6 +1054,32 @@ server.registerPrompt(
|
|
|
921
1054
|
]
|
|
922
1055
|
})
|
|
923
1056
|
);
|
|
1057
|
+
server.registerPrompt(
|
|
1058
|
+
"login",
|
|
1059
|
+
{
|
|
1060
|
+
title: "Sign in to InSitue",
|
|
1061
|
+
description: "Browser-based sign-in to InSitue Cloud via PKCE. Opens your browser, confirms a pairing code, then saves credentials and auto-links the repo."
|
|
1062
|
+
},
|
|
1063
|
+
() => ({
|
|
1064
|
+
messages: [
|
|
1065
|
+
{
|
|
1066
|
+
role: "user",
|
|
1067
|
+
content: {
|
|
1068
|
+
type: "text",
|
|
1069
|
+
text: readPkgFile("commands/login.md") ?? loginInstructions()
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
]
|
|
1073
|
+
})
|
|
1074
|
+
);
|
|
1075
|
+
function loginInstructions() {
|
|
1076
|
+
return `# Sign in to InSitue
|
|
1077
|
+
|
|
1078
|
+
Call \`mcp__insitue__authenticate\` to start the browser sign-in flow.
|
|
1079
|
+
Show the user the returned URL and userCode, then call
|
|
1080
|
+
\`mcp__insitue__complete_authentication\` once they approve in the browser.
|
|
1081
|
+
Confirm the result with "Signed in as <login>" and linked project if any.`;
|
|
1082
|
+
}
|
|
924
1083
|
function readPkgFile(rel) {
|
|
925
1084
|
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
926
1085
|
for (const base of [join3(here, ".."), here]) {
|
package/dist/setup-cli.js
CHANGED
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
} from "./chunk-UNMH2DN4.js";
|
|
5
5
|
import {
|
|
6
6
|
diagnose
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-KGRPDRYH.js";
|
|
8
|
+
import "./chunk-B3HSTDGI.js";
|
|
8
9
|
|
|
9
10
|
// src/setup-cli.ts
|
|
10
11
|
import {
|
|
@@ -189,6 +190,8 @@ async function cmdDiagnose(flags2) {
|
|
|
189
190
|
"InSitue diagnostics",
|
|
190
191
|
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
191
192
|
`Project: ${report.projectDir.dir} (via ${report.projectDir.source})`,
|
|
193
|
+
`Auth: ${tick(report.authenticated)} ${report.authenticated ? `signed in as ${report.login ?? "(unknown)"}` : "not signed in"}`,
|
|
194
|
+
`Project ID: ${report.linkedProjectId ?? "(not linked)"}`,
|
|
192
195
|
`Session: ${tick(report.hasSessionFile)} .insitue/session.json ${report.hasSessionFile ? "exists" : "missing"}`,
|
|
193
196
|
`Companion: ${tick(report.companionReachable)} ${report.companionReachable ? `reachable on port ${report.companionPort}` : "not reachable"}`,
|
|
194
197
|
`@insitue/sdk: ${report.sdkVersion ?? "(not installed)"}`,
|
|
@@ -206,7 +209,7 @@ async function cmdDiagnose(flags2) {
|
|
|
206
209
|
}
|
|
207
210
|
function cmdHelp() {
|
|
208
211
|
process.stdout.write(
|
|
209
|
-
"Usage: insitue <command> [flags]\n\nCommands:\n setup Wire the InSitue MCP into Claude Desktop / Code\n diagnose Health-check the local InSitue setup\n help Show this message\n\nFlags (setup):\n --desktop Configure Claude Desktop\n --code Print the Claude Code install hint\n --both Configure both\n --project=PATH Project directory (default: cwd)\n --name=NAME Desktop MCP entry name (default: insitue-<dirname>)\n --dry-run Show what would change without writing\n\nFlags (diagnose):\n --project=PATH Project directory (default: walk-up from cwd)\n"
|
|
212
|
+
"Usage: npx @insitue/claude-plugin <command> [flags]\n\nCommands:\n login Sign in to InSitue Cloud via browser (PKCE)\n link Link this repo to an InSitue cloud project\n whoami Show current auth + linked project\n setup Wire the InSitue MCP into Claude Desktop / Code\n diagnose Health-check the local InSitue setup\n help Show this message\n\nFlags (login):\n --token=PAT Use a pre-created token instead of browser sign-in\n --host=URL Override the InSitue host (default: https://app.insitue.com)\n\nFlags (link):\n [projectId] Project id to link (auto-detected from git remote if omitted)\n --project=PATH Project directory (default: walk-up from cwd)\n\nFlags (setup):\n --desktop Configure Claude Desktop\n --code Print the Claude Code install hint\n --both Configure both\n --project=PATH Project directory (default: cwd)\n --name=NAME Desktop MCP entry name (default: insitue-<dirname>)\n --dry-run Show what would change without writing\n\nFlags (diagnose):\n --project=PATH Project directory (default: walk-up from cwd)\n"
|
|
210
213
|
);
|
|
211
214
|
return 0;
|
|
212
215
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@insitue/claude-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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
5
|
"keywords": [
|
|
6
6
|
"insitue",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
40
40
|
"ws": "^8.18.0",
|
|
41
|
-
"zod": "^3.23.8"
|
|
41
|
+
"zod": "^3.23.8",
|
|
42
|
+
"@insitue/capture-core": "0.4.1"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/ws": "^8.5.13",
|
|
@@ -52,8 +53,8 @@
|
|
|
52
53
|
"node": ">=24"
|
|
53
54
|
},
|
|
54
55
|
"scripts": {
|
|
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",
|
|
56
|
+
"build": "tsup src/dispatcher.ts src/mcp-server.ts src/setup-cli.ts src/diagnose.ts src/cloud/api.ts src/cloud/config.ts src/cloud/login.ts src/cloud-cli.ts --format esm --clean --external @modelcontextprotocol/sdk --external ws --external zod",
|
|
57
|
+
"dev": "tsup src/dispatcher.ts src/mcp-server.ts src/setup-cli.ts src/diagnose.ts src/cloud/api.ts src/cloud/config.ts src/cloud/login.ts src/cloud-cli.ts --format esm --watch --external @modelcontextprotocol/sdk --external ws --external zod",
|
|
57
58
|
"test": "node --test \"test/*.test.mjs\"",
|
|
58
59
|
"typecheck": "tsc --noEmit",
|
|
59
60
|
"lint": "tsc --noEmit"
|
package/dist/chunk-5APYM634.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
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
|
-
};
|