@insitue/claude-plugin 0.7.2 → 0.7.4

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.2",
3
+ "version": "0.6.4",
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.4
4
+
5
+ - **`/insitue:connect` no longer blocks the session.** In poll mode the agent silently re-looped `next_pick` forever, freezing the chat (queued messages never processed). It now polls once then hands control back, drains buffered picks on your next message, and points you at channel mode (`--dangerously-load-development-channels --channels plugin:insitue`) for truly hands-free background picks.
6
+ - **Brand the sign-in success page.** The loopback login success page uses the brand purple gradient heading instead of the off-brand green.
7
+
8
+ ## 0.7.3
9
+
10
+ - **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.
11
+ - **Project-scope awareness.** Persists the token scope; cloud errors guide you to re-login for the current project when scoped elsewhere.
12
+
3
13
  ## 0.7.2
4
14
 
5
15
  - **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`).
package/README.md CHANGED
@@ -47,6 +47,15 @@ auto-start the InSitue companion process in the background of
47
47
  your `claude` session — no separate terminal to babysit. The
48
48
  slash command `/insitue:connect` enters the loop.
49
49
 
50
+ The plugin ships four slash commands:
51
+
52
+ | Command | What it does |
53
+ |---|---|
54
+ | `/insitue:connect` | Enter the pick→edit loop — receive browser picks and act on them. |
55
+ | `/insitue:disconnect` | Leave the loop: close the WS, stop receiving picks, and kill the companion the plugin spawned. |
56
+ | `/insitue:login` | Browser sign-in (PKCE) to InSitue Cloud, then auto-link this repo to its project. |
57
+ | `/insitue:logout` | Revoke the token server-side and clear local credentials. |
58
+
50
59
  ### 1B. Claude Desktop — one-command setup
51
60
 
52
61
  Claude Desktop doesn't have a plugin marketplace, but it does
@@ -275,8 +284,8 @@ If your project uses InSitue Cloud, bug reports captured in production land in y
275
284
 
276
285
  ### Prerequisites
277
286
 
278
- 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>.
279
- 2. **Link your project:** run `insitue link <projectId>` in your project root. The tools read the linked project from `.insitue/project.json`.
287
+ 1. **Authenticate:** run `insitue login` (from the `@insitue/companion` CLI), or `/insitue:login` inside Claude Code. Bare `insitue login` is a tokenless **browser** sign-in via PKCE — it opens your browser, prints an authorize URL + a short code to confirm, and on success prints `✓ Signed in as <login>`. Credentials are saved to `~/.insitue/auth.json`. A token minted this way from inside a repo is **project-scoped** (it can only act on that project's issues). On SSH/remote where no browser or loopback redirect is available, the flow auto-switches to the device flow — or force it with `insitue login --device`. As a CI / headless fallback, paste a Personal Access Token directly with `insitue login --token pat_live_…` (mint PATs at <https://app.insitue.com/app/settings/developer>); PATs are account-wide.
288
+ 2. **Link your project:** `insitue login` already best-effort auto-links the current repo to its InSitue project, so this is usually unnecessary. If you need to link manually, run `insitue link` (auto-detects the project from the git remote) or `insitue link <projectId>` in your project root. The tools read the linked project from `.insitue/project.json`.
280
289
 
281
290
  ### The loop
282
291
 
@@ -352,7 +361,7 @@ The plugin is a stdio MCP server that:
352
361
 
353
362
  | Tool | Purpose |
354
363
  |---|---|
355
- | `next_pick` | Long-poll for the next browser pick (default 25 s; max 30 min). |
364
+ | `next_pick` | Long-poll for the next browser pick (default 8 s; max 30 min). |
356
365
  | `list_recent_picks` | Up to N buffered picks since the MCP server started. |
357
366
  | `start_session` | Returns the operating instructions + current state. Desktop entry point. |
358
367
  | `end_session` | Cleanly disconnect: close WS, suppress reconnect, kill spawned companion, drop session file. |
@@ -364,6 +373,9 @@ The plugin is a stdio MCP server that:
364
373
  | `claim_cloud_issue` | Claim an issue by `id`; returns note, source `file:line`, page URL, and top console errors. |
365
374
  | `resolve_cloud_issue` | Mark an issue resolved; attach `prUrl` and optional `branch`. |
366
375
  | `release_cloud_issue` | Return a claimed issue to the open queue. |
376
+ | `authenticate` | Start browser sign-in (PKCE): chooses the device vs loopback flow automatically and returns the authorize URL + code to confirm. |
377
+ | `complete_authentication` | Poll for approval (~5 min), then save credentials and best-effort auto-link the current repo to its project. |
378
+ | `logout` | Revoke the token server-side and clear local credentials. |
367
379
 
368
380
  All file tools resolve paths against the project dir and refuse
369
381
  anything that resolves outside it (realpath-checked, so `..` games
@@ -64,18 +64,33 @@ your context and skip any id you've already processed.
64
64
  1. Call `mcp__insitue__list_recent_picks` once. If there are
65
65
  any picks the user made before you attached, summarise them
66
66
  ("you picked X but haven't sent a description yet — make sure
67
- to click Send in the InSitue panel"). Otherwise just say
68
- "Connected — pick something in the browser when you're ready.
69
- You can also just type to ask me anything anytime; I'll answer
70
- and keep watching, no need to cancel."
71
- 2. Enter the loop: call `mcp__insitue__next_pick`. It long-polls
72
- (~8s — deliberately short. The user can type at ANY time without
73
- cancelling: Claude Code queues their message and delivers it the
74
- moment this call returns, so a short poll means they wait at most
75
- ~8s to reach you and never need to hit Esc). When it returns with
76
- `status: "timeout"` **and the user hasn't said anything**, call it
77
- again immediately without announcing itthe timeout is just a
78
- heartbeat, not news. When it returns with `status: "ok"`:
67
+ to click Send in the InSitue panel"). Otherwise say:
68
+
69
+ > Connected. Pick an element in the browser and I'll act on it
70
+ > here. You can also just type to ask me anything — I'll answer
71
+ > right away, then keep watching.
72
+ >
73
+ > Tip: for hands-free background picks (so I act the moment you
74
+ > pick, without you waiting on me), relaunch with
75
+ > `claude --dangerously-load-development-channels --channels plugin:insitue`.
76
+
77
+ 2. **Watch for a pickONE poll, then hand control back.** Call
78
+ `mcp__insitue__next_pick` (it long-polls ~8s).
79
+ - On `status: "timeout"` (no pick this window): **STOP. Do NOT
80
+ silently call `next_pick` again.** Parking yourself in back-to-back
81
+ polls FREEZES the session — a tool call can't be interrupted, so the
82
+ user types a message and you never see it (this is the #1 cause of
83
+ "InSitue is blocking me"). Instead, end your turn and hand control
84
+ back with one short line, e.g.:
85
+
86
+ > 👀 Watching. Pick something in the browser, or type anything — I'll
87
+ > grab whatever you picked.
88
+
89
+ Then wait for the user. You resume watching on their next message
90
+ (step 3). In plain poll mode you catch picks when the user next
91
+ interacts — for truly hands-free push *between* messages, they need
92
+ channel mode (the tip in step 1).
93
+ - On `status: "ok"` (a pick arrived):
79
94
  - **Always echo the prompt back first.** Before any action,
80
95
  diff, or follow-up question, lead with:
81
96
 
@@ -105,20 +120,22 @@ your context and skip any id you've already processed.
105
120
  Don't auto-apply.
106
121
  - On approval, write with the Edit tool (Code) or
107
122
  `mcp__insitue__apply_edit` (Desktop). Confirm what changed.
108
- - Loop back to `next_pick`.
109
- 3. If `next_pick` returns `status: "timeout"`, the user simply
110
- hasn't picked anything yet. Stay quiet and call `next_pick`
111
- again. **Do not narrate the loop** no "still waiting…", no
112
- "polling again…". The user sees `[insitue] 📥 pick received`
113
- on stderr the moment their pick lands; that's the
114
- confirmation, not your narration. **The user never has to cancel
115
- the watch to talk to you.** When a message from the user arrives
116
- while you're watching (it queues during the poll and lands the
117
- moment `next_pick` returns), STOP looping, answer it fully, and do
118
- whatever they ask then resume the watch by calling `next_pick`
119
- again. Their message is the priority; the watch waits for them, not
120
- the other way around. Never tell the user to cancel or re-run
121
- `/insitue:connect` just to ask a question.
123
+ - Then call `next_pick` once more to catch a rapid follow-up pick.
124
+ When it times out (picks have stopped), fall back to the
125
+ hand-back rule above yield to the user; do NOT loop forever.
126
+ 3. **On each user message: drain picks first, then answer, then resume.**
127
+ Whenever the user sends a message, before responding call `next_pick`
128
+ with a short `timeout_ms` (e.g. `1500`), repeatedly, until it returns
129
+ `status: "timeout"` handle each pick it returns (de-dupe by `id`).
130
+ This catches anything the user picked while you were idle; picks are
131
+ buffered server-side, so nothing is lost. Then answer the user's
132
+ message fully and do whatever they ask. Then resume watching with a
133
+ single `next_pick` poll (step 2's one-poll-then-hand-back rule).
134
+
135
+ The user's message is ALWAYS the priority. Never tell them to cancel
136
+ or re-run `/insitue:connect` to talk to you, and never narrate the
137
+ watch ("still waiting…", "polling again…") — the browser already shows
138
+ `[insitue] 📥 pick received` the moment a pick lands.
122
139
  4. **End the session properly.** When the user says "stop",
123
140
  "done", "quit", "thanks", "exit", "disconnect", "stop
124
141
  insitue", or anything else that clearly ends the InSitue
package/commands/login.md CHANGED
@@ -10,9 +10,13 @@ Cloud issues will become available via the claude-plugin tools once signed in.
10
10
 
11
11
  ## Your behaviour
12
12
 
13
- 1. Call `mcp__insitue__authenticate` (no arguments).
13
+ 1. Call `mcp__insitue__authenticate` (no arguments). It auto-detects the
14
+ environment: it opens a loopback browser flow locally, and automatically
15
+ switches to the device flow when an SSH session is detected or no loopback
16
+ port is available. Branch on the returned shape:
14
17
 
15
- 2. The tool returns `{ status: "browser_opened", url, userCode, message }`.
18
+ 2. **Loopback flow** the tool returns
19
+ `{ status: "browser_opened", url, userCode, message }`.
16
20
  Show the user:
17
21
 
18
22
  > I've opened your browser to sign in to InSitue. Please:
@@ -21,11 +25,20 @@ Cloud issues will become available via the claude-plugin tools once signed in.
21
25
  >
22
26
  > (If your browser didn't open, visit this URL manually: `<url>`)
23
27
 
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.
28
+ **Device flow** the tool instead returns a device-flow shape with a
29
+ `verificationUrl` and `userCode` (no loopback redirect). This is what you'll
30
+ get over SSH or when no loopback port is free. Show the user:
26
31
 
27
- 3. Call `mcp__insitue__complete_authentication` (no arguments).
28
- This waits up to 5 minutes for the browser approval.
32
+ > To sign in to InSitue, open this URL in any browser:
33
+ > **`<verificationUrl>`**
34
+ >
35
+ > Then confirm the code matches: **`<userCode>`**
36
+
37
+ In both flows, do NOT proceed to step 3 until the user says they've approved
38
+ or you detect they're ready. Wait for the user to confirm.
39
+
40
+ 3. Call `mcp__insitue__complete_authentication` (no arguments). This polls
41
+ for the browser approval (up to ~5 minutes) for either flow.
29
42
 
30
43
  4. On `{ status: "ok" }`:
31
44
  - If `linked: true`: reply "Signed in as **@`<login>`** and linked to
@@ -19,8 +19,8 @@ var SUCCESS_HTML = `<!DOCTYPE html>
19
19
  <head><meta charset="utf-8"><title>InSitue \u2014 signed in</title>
20
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
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>
22
+ h1{font-size:1.4rem;margin:0 0 8px;background:linear-gradient(180deg,#6b63ff,#5751e6);-webkit-background-clip:text;background-clip:text;color:transparent;}p{color:#6b7280;margin:0;}</style></head>
23
+ <body><div class="card"><h1>You're signed in to InSitue!</h1>
24
24
  <p>You can close this tab and return to your terminal.</p></div></body></html>`;
25
25
  var ERROR_HTML = `<!DOCTYPE html>
26
26
  <html lang="en">
@@ -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-B2OV76P2.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-B2OV76P2.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-B2OV76P2.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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@insitue/claude-plugin",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
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",