@insitue/claude-plugin 0.7.1 → 0.7.3
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 +1 -1
- package/CHANGELOG.md +10 -0
- package/README.md +5 -0
- package/dist/{chunk-RS3B7P4N.js → chunk-BZDEEE6O.js} +119 -1
- package/dist/cloud/login.js +1 -1
- package/dist/cloud-cli.js +25 -8
- package/dist/mcp-server.js +21 -68
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @insitue/claude-plugin
|
|
2
2
|
|
|
3
|
+
## 0.7.3
|
|
4
|
+
|
|
5
|
+
- **Device-flow login for SSH/remote.** `/insitue:login` (+ `authenticate` tool) auto-detects an SSH session and uses a device flow: shows a verification URL + pairing code to approve in any browser; `complete_authentication` polls for the token. `login --device` forces it. Loopback stays the default locally.
|
|
6
|
+
- **Project-scope awareness.** Persists the token scope; cloud errors guide you to re-login for the current project when scoped elsewhere.
|
|
7
|
+
|
|
8
|
+
## 0.7.2
|
|
9
|
+
|
|
10
|
+
- **Consistent slash commands.** Removed the duplicate MCP prompts for connect/login/logout, which Claude Code surfaced as differently-named `…insitue:<name> (MCP)` entries alongside the clean `/insitue:*` slash commands. Now there is one consistent surface: `/insitue:connect`, `/insitue:disconnect`, `/insitue:login`, `/insitue:logout` (from `commands/*.md`). On Claude Desktop, use the equivalent tools (`start_session`, `authenticate`+`complete_authentication`, `logout`).
|
|
11
|
+
- **Docs:** the channels (preview) section now documents the `allowedChannelPlugins` org-policy escape hatch to drop the `--dangerously-load-development-channels` flag.
|
|
12
|
+
|
|
3
13
|
## 0.7.1
|
|
4
14
|
|
|
5
15
|
- **`/insitue:logout`.** New slash command (+ `logout` MCP tool + `npx @insitue/claude-plugin logout` CLI) that signs you out properly: it revokes this machine's token server-side, then clears the local credentials (`~/.insitue/auth.json`). Revoke is best-effort — your local creds are always cleared even if the network call fails. Pairs with the existing `/insitue:login`.
|
package/README.md
CHANGED
|
@@ -201,6 +201,11 @@ channel listener falls back to the normal poll automatically.
|
|
|
201
201
|
- This is a **Claude Code research-preview** feature. The flag
|
|
202
202
|
`--dangerously-load-development-channels` is required during the
|
|
203
203
|
preview period and may change or be renamed in a future release.
|
|
204
|
+
- **Dropping the flag:** on Team/Enterprise, an admin can add `insitue`
|
|
205
|
+
to the `allowedChannelPlugins` org policy — then the channel runs with
|
|
206
|
+
just `claude --channels plugin:insitue` (no dangerous flag). Once
|
|
207
|
+
channels leave research preview / the plugin is on Anthropic's default
|
|
208
|
+
allowlist, the flag won't be needed at all.
|
|
204
209
|
- It works only with the CLI (`claude`), not Claude Desktop.
|
|
205
210
|
- The standard `/insitue:connect` workflow (plain `claude`, no
|
|
206
211
|
extra flags) continues to work exactly as before via polling —
|
|
@@ -34,7 +34,113 @@ function openBrowserUrl(url) {
|
|
|
34
34
|
} catch {
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
function isRemoteSession() {
|
|
38
|
+
return !!(process.env["SSH_CONNECTION"] || process.env["SSH_TTY"]);
|
|
39
|
+
}
|
|
40
|
+
async function startDeviceFlow(opts) {
|
|
41
|
+
const { host, label, repo, openBrowser = true } = opts;
|
|
42
|
+
const { verifier, challenge } = genPkce();
|
|
43
|
+
const startRes = await fetch(`${host}/api/v1/cli/authorize/start`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: { "content-type": "application/json" },
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
code_challenge: challenge,
|
|
48
|
+
code_challenge_method: "S256",
|
|
49
|
+
...label ? { label } : {},
|
|
50
|
+
...repo ? { repo } : {}
|
|
51
|
+
})
|
|
52
|
+
});
|
|
53
|
+
if (!startRes.ok) {
|
|
54
|
+
const text = await startRes.text().catch(() => "");
|
|
55
|
+
throw new Error(`authorize/start failed: HTTP ${startRes.status} ${text}`);
|
|
56
|
+
}
|
|
57
|
+
const startJson = await startRes.json();
|
|
58
|
+
const { request_id: requestId, user_code: userCode, expires_in } = startJson;
|
|
59
|
+
const verificationUrl = `${host}/cli/authorize?request_id=${encodeURIComponent(requestId)}`;
|
|
60
|
+
if (openBrowser) {
|
|
61
|
+
openBrowserUrl(verificationUrl);
|
|
62
|
+
}
|
|
63
|
+
let cancelled = false;
|
|
64
|
+
let cancelFn = null;
|
|
65
|
+
function cancel() {
|
|
66
|
+
cancelled = true;
|
|
67
|
+
if (cancelFn) cancelFn();
|
|
68
|
+
}
|
|
69
|
+
function wait() {
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const timeoutMs = Math.min((expires_in ?? 600) * 1e3, 6e5);
|
|
72
|
+
let pollIntervalMs = 5e3;
|
|
73
|
+
let timer;
|
|
74
|
+
let settled = false;
|
|
75
|
+
const overallTimeout = setTimeout(() => {
|
|
76
|
+
if (settled) return;
|
|
77
|
+
settled = true;
|
|
78
|
+
if (timer) clearTimeout(timer);
|
|
79
|
+
reject(new Error("timed out waiting for device approval (10 min)"));
|
|
80
|
+
}, timeoutMs);
|
|
81
|
+
cancelFn = () => {
|
|
82
|
+
if (settled) return;
|
|
83
|
+
settled = true;
|
|
84
|
+
if (timer) clearTimeout(timer);
|
|
85
|
+
clearTimeout(overallTimeout);
|
|
86
|
+
reject(new Error("cancelled"));
|
|
87
|
+
};
|
|
88
|
+
const poll = async () => {
|
|
89
|
+
if (settled || cancelled) return;
|
|
90
|
+
try {
|
|
91
|
+
const tokenRes = await fetch(`${host}/api/v1/cli/token`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "content-type": "application/json" },
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
request_id: requestId,
|
|
96
|
+
code_verifier: verifier
|
|
97
|
+
})
|
|
98
|
+
});
|
|
99
|
+
if (tokenRes.ok) {
|
|
100
|
+
if (settled) return;
|
|
101
|
+
settled = true;
|
|
102
|
+
clearTimeout(overallTimeout);
|
|
103
|
+
const tokenJson = await tokenRes.json();
|
|
104
|
+
resolve({
|
|
105
|
+
token: tokenJson.token,
|
|
106
|
+
host: tokenJson.host,
|
|
107
|
+
login: tokenJson.login,
|
|
108
|
+
projectId: tokenJson.projectId ?? null
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
let errBody = {};
|
|
113
|
+
try {
|
|
114
|
+
errBody = await tokenRes.json();
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
const errCode = errBody.error ?? "";
|
|
118
|
+
if (errCode === "authorization_pending") {
|
|
119
|
+
} else if (errCode === "slow_down") {
|
|
120
|
+
pollIntervalMs = Math.min(pollIntervalMs + 5e3, 3e4);
|
|
121
|
+
} else {
|
|
122
|
+
if (settled) return;
|
|
123
|
+
settled = true;
|
|
124
|
+
clearTimeout(overallTimeout);
|
|
125
|
+
reject(
|
|
126
|
+
new Error(
|
|
127
|
+
errCode === "invalid_grant" ? "sign-in failed: token request rejected (invalid_grant). The request may have expired." : `sign-in failed: ${errCode || `HTTP ${tokenRes.status}`}`
|
|
128
|
+
)
|
|
129
|
+
);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
}
|
|
134
|
+
if (!settled && !cancelled) {
|
|
135
|
+
timer = setTimeout(() => void poll(), pollIntervalMs);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
timer = setTimeout(() => void poll(), pollIntervalMs);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return { authorizeUrl: verificationUrl, userCode, port: 0, wait, cancel };
|
|
142
|
+
}
|
|
143
|
+
async function startLoopbackFlow(opts) {
|
|
38
144
|
const { host, label, repo, openBrowser = true } = opts;
|
|
39
145
|
const { verifier, challenge } = genPkce();
|
|
40
146
|
const state = toBase64Url(randomBytes(16));
|
|
@@ -157,6 +263,18 @@ async function startLogin(opts) {
|
|
|
157
263
|
}
|
|
158
264
|
return { authorizeUrl, userCode, port, wait, cancel };
|
|
159
265
|
}
|
|
266
|
+
async function startLogin(opts) {
|
|
267
|
+
const { mode, ...rest } = opts;
|
|
268
|
+
const effectiveMode = (() => {
|
|
269
|
+
if (mode === "device") return "device";
|
|
270
|
+
if (mode === "loopback") return "loopback";
|
|
271
|
+
return isRemoteSession() ? "device" : "loopback";
|
|
272
|
+
})();
|
|
273
|
+
if (effectiveMode === "device") {
|
|
274
|
+
return startDeviceFlow(rest);
|
|
275
|
+
}
|
|
276
|
+
return startLoopbackFlow(rest);
|
|
277
|
+
}
|
|
160
278
|
function parseRemoteToOwnerName(remote) {
|
|
161
279
|
remote = remote.trim();
|
|
162
280
|
const sshMatch = remote.match(
|
package/dist/cloud/login.js
CHANGED
package/dist/cloud-cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
detectGitRemote,
|
|
17
17
|
pickProjectIdForRepo,
|
|
18
18
|
startLogin
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-BZDEEE6O.js";
|
|
20
20
|
|
|
21
21
|
// src/cloud-cli.ts
|
|
22
22
|
import { hostname } from "os";
|
|
@@ -55,14 +55,14 @@ async function cmdLogin(argv) {
|
|
|
55
55
|
);
|
|
56
56
|
return 0;
|
|
57
57
|
}
|
|
58
|
+
const mode = flags.has("device") ? "device" : flags.has("loopback") ? "loopback" : "auto";
|
|
58
59
|
const auth = loadAuth();
|
|
59
60
|
const host = resolveHost(auth);
|
|
60
61
|
const projectDir = resolveProjectDir();
|
|
61
62
|
const repo = detectGitRemote(projectDir.dir);
|
|
62
|
-
process.stdout.write("Opening your browser to sign in to InSitue\u2026\n");
|
|
63
63
|
let flow;
|
|
64
64
|
try {
|
|
65
|
-
flow = await startLogin({ host, label: hostname(), ...repo ? { repo } : {} });
|
|
65
|
+
flow = await startLogin({ host, label: hostname(), mode, ...repo ? { repo } : {} });
|
|
66
66
|
} catch (err) {
|
|
67
67
|
process.stderr.write(
|
|
68
68
|
`error: could not start login: ${err.message}
|
|
@@ -70,20 +70,33 @@ async function cmdLogin(argv) {
|
|
|
70
70
|
);
|
|
71
71
|
return 1;
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
if (mode === "device" || mode === "auto" && flow.port === 0) {
|
|
74
|
+
process.stdout.write(
|
|
75
|
+
`
|
|
76
|
+
Open this URL in any browser to approve:
|
|
77
|
+
${flow.authorizeUrl}
|
|
78
|
+
Confirm the code matches: ${flow.userCode}
|
|
79
|
+
|
|
80
|
+
Waiting for approval\u2026
|
|
81
|
+
`
|
|
82
|
+
);
|
|
83
|
+
} else {
|
|
84
|
+
process.stdout.write("Opening your browser to sign in to InSitue\u2026\n");
|
|
85
|
+
process.stdout.write(
|
|
86
|
+
`
|
|
75
87
|
Browser URL: ${flow.authorizeUrl}
|
|
76
88
|
Pairing code (confirm your browser shows this): ${flow.userCode}
|
|
77
89
|
|
|
78
90
|
Waiting for approval\u2026
|
|
79
91
|
`
|
|
80
|
-
|
|
92
|
+
);
|
|
93
|
+
}
|
|
81
94
|
let result;
|
|
82
95
|
try {
|
|
83
96
|
result = await flow.wait();
|
|
84
97
|
} catch (err) {
|
|
85
98
|
const msg = err.message ?? String(err);
|
|
86
|
-
if (msg === "timeout") {
|
|
99
|
+
if (msg === "timeout" || msg.startsWith("timed out")) {
|
|
87
100
|
process.stderr.write("error: timed out waiting for browser approval\n");
|
|
88
101
|
} else {
|
|
89
102
|
process.stderr.write(`error: ${msg}
|
|
@@ -91,9 +104,13 @@ Waiting for approval\u2026
|
|
|
91
104
|
}
|
|
92
105
|
return 1;
|
|
93
106
|
}
|
|
94
|
-
saveAuth({ token: result.token, host: result.host, login: result.login });
|
|
107
|
+
saveAuth({ token: result.token, host: result.host, login: result.login, projectId: result.projectId });
|
|
95
108
|
process.stdout.write(`Signed in as ${result.login}.
|
|
96
109
|
`);
|
|
110
|
+
if (result.projectId) {
|
|
111
|
+
process.stdout.write(`Token scoped to project ${result.projectId}.
|
|
112
|
+
`);
|
|
113
|
+
}
|
|
97
114
|
const projectDir2 = resolveProjectDir();
|
|
98
115
|
let linkedProjectId = result.projectId ?? null;
|
|
99
116
|
if (!linkedProjectId && repo) {
|
package/dist/mcp-server.js
CHANGED
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
detectGitRemote,
|
|
27
27
|
pickProjectIdForRepo,
|
|
28
28
|
startLogin
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-BZDEEE6O.js";
|
|
30
30
|
|
|
31
31
|
// src/mcp-server.ts
|
|
32
32
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -826,10 +826,11 @@ server.registerTool(
|
|
|
826
826
|
}
|
|
827
827
|
);
|
|
828
828
|
var pendingLoginFlow = null;
|
|
829
|
+
var pendingLoginIsDevice = false;
|
|
829
830
|
server.registerTool(
|
|
830
831
|
"authenticate",
|
|
831
832
|
{
|
|
832
|
-
description: "Start
|
|
833
|
+
description: "Start an InSitue sign-in (PKCE). Auto-detects the best method: on SSH or headless environments uses device-flow (returns a URL + code for you to open in any browser); on a local machine opens your browser directly. After approving in the browser, call `complete_authentication` to finish.",
|
|
833
834
|
inputSchema: {}
|
|
834
835
|
},
|
|
835
836
|
async () => {
|
|
@@ -839,9 +840,26 @@ server.registerTool(
|
|
|
839
840
|
const flow = await startLogin({
|
|
840
841
|
host,
|
|
841
842
|
label: hostname(),
|
|
843
|
+
mode: "auto",
|
|
842
844
|
...repo ? { repo } : {}
|
|
843
845
|
});
|
|
844
846
|
pendingLoginFlow = flow;
|
|
847
|
+
pendingLoginIsDevice = flow.port === 0;
|
|
848
|
+
if (pendingLoginIsDevice) {
|
|
849
|
+
return {
|
|
850
|
+
content: [
|
|
851
|
+
{
|
|
852
|
+
type: "text",
|
|
853
|
+
text: JSON.stringify({
|
|
854
|
+
status: "device",
|
|
855
|
+
verificationUrl: flow.authorizeUrl,
|
|
856
|
+
userCode: flow.userCode,
|
|
857
|
+
message: "Open the URL in any browser, confirm the code, then I'll finish."
|
|
858
|
+
})
|
|
859
|
+
}
|
|
860
|
+
]
|
|
861
|
+
};
|
|
862
|
+
}
|
|
845
863
|
return {
|
|
846
864
|
content: [
|
|
847
865
|
{
|
|
@@ -894,7 +912,7 @@ server.registerTool(
|
|
|
894
912
|
]
|
|
895
913
|
};
|
|
896
914
|
}
|
|
897
|
-
saveAuth({ token: result.token, host: result.host, login: result.login });
|
|
915
|
+
saveAuth({ token: result.token, host: result.host, login: result.login, projectId: result.projectId });
|
|
898
916
|
let linkedProjectId = result.projectId ?? null;
|
|
899
917
|
let linked = false;
|
|
900
918
|
if (!linkedProjectId) {
|
|
@@ -1154,71 +1172,6 @@ server.registerTool(
|
|
|
1154
1172
|
}
|
|
1155
1173
|
}
|
|
1156
1174
|
);
|
|
1157
|
-
server.registerPrompt(
|
|
1158
|
-
"connect",
|
|
1159
|
-
{
|
|
1160
|
-
title: "Connect to InSitue",
|
|
1161
|
-
description: "Loads the operating instructions and begins the pick \u2192 edit loop."
|
|
1162
|
-
},
|
|
1163
|
-
() => ({
|
|
1164
|
-
messages: [
|
|
1165
|
-
{
|
|
1166
|
-
role: "user",
|
|
1167
|
-
content: { type: "text", text: loadInstructions() }
|
|
1168
|
-
}
|
|
1169
|
-
]
|
|
1170
|
-
})
|
|
1171
|
-
);
|
|
1172
|
-
server.registerPrompt(
|
|
1173
|
-
"login",
|
|
1174
|
-
{
|
|
1175
|
-
title: "Sign in to InSitue",
|
|
1176
|
-
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."
|
|
1177
|
-
},
|
|
1178
|
-
() => ({
|
|
1179
|
-
messages: [
|
|
1180
|
-
{
|
|
1181
|
-
role: "user",
|
|
1182
|
-
content: {
|
|
1183
|
-
type: "text",
|
|
1184
|
-
text: readPkgFile("commands/login.md") ?? loginInstructions()
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
]
|
|
1188
|
-
})
|
|
1189
|
-
);
|
|
1190
|
-
function loginInstructions() {
|
|
1191
|
-
return `# Sign in to InSitue
|
|
1192
|
-
|
|
1193
|
-
Call \`mcp__insitue__authenticate\` to start the browser sign-in flow.
|
|
1194
|
-
Show the user the returned URL and userCode, then call
|
|
1195
|
-
\`mcp__insitue__complete_authentication\` once they approve in the browser.
|
|
1196
|
-
Confirm the result with "Signed in as <login>" and linked project if any.`;
|
|
1197
|
-
}
|
|
1198
|
-
server.registerPrompt(
|
|
1199
|
-
"logout",
|
|
1200
|
-
{
|
|
1201
|
-
title: "Sign out of InSitue",
|
|
1202
|
-
description: "Sign out of InSitue Cloud \u2014 revokes this machine's token and clears local credentials."
|
|
1203
|
-
},
|
|
1204
|
-
() => ({
|
|
1205
|
-
messages: [
|
|
1206
|
-
{
|
|
1207
|
-
role: "user",
|
|
1208
|
-
content: {
|
|
1209
|
-
type: "text",
|
|
1210
|
-
text: readPkgFile("commands/logout.md") ?? logoutInstructions()
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
]
|
|
1214
|
-
})
|
|
1215
|
-
);
|
|
1216
|
-
function logoutInstructions() {
|
|
1217
|
-
return `# Sign out of InSitue
|
|
1218
|
-
|
|
1219
|
-
Call \`mcp__insitue__logout\` (no arguments).
|
|
1220
|
-
Confirm the result in one line, e.g. "Signed out of InSitue." or relay any error message.`;
|
|
1221
|
-
}
|
|
1222
1175
|
function readPkgFile(rel) {
|
|
1223
1176
|
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
1224
1177
|
for (const base of [join3(here, ".."), here]) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@insitue/claude-plugin",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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",
|