@insitue/claude-plugin 0.7.2 → 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 +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 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
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
|
+
|
|
3
8
|
## 0.7.2
|
|
4
9
|
|
|
5
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`).
|
|
@@ -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) {
|
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",
|