@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insitue",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Drive a Claude Code session from the InSitue browser overlay. Pick an element in your app, claude reads the file and proposes the edit.",
5
5
  "mcpServers": {
6
6
  "insitue": {
package/CHANGELOG.md 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
- async function startLogin(opts) {
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(
@@ -3,7 +3,7 @@ import {
3
3
  genPkce,
4
4
  pickProjectIdForRepo,
5
5
  startLogin
6
- } from "../chunk-RS3B7P4N.js";
6
+ } from "../chunk-BZDEEE6O.js";
7
7
  export {
8
8
  detectGitRemote,
9
9
  genPkce,
package/dist/cloud-cli.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  detectGitRemote,
17
17
  pickProjectIdForRepo,
18
18
  startLogin
19
- } from "./chunk-RS3B7P4N.js";
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
- process.stdout.write(
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) {
@@ -26,7 +26,7 @@ import {
26
26
  detectGitRemote,
27
27
  pickProjectIdForRepo,
28
28
  startLogin
29
- } from "./chunk-RS3B7P4N.js";
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 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.",
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.1",
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",