@insitue/claude-plugin 0.3.3 → 0.4.1
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/README.md +65 -8
- package/commands/connect.md +21 -9
- package/dist/chunk-RF5Q55CG.js +194 -0
- package/dist/dispatcher.js +10 -0
- package/dist/mcp-server.js +371 -34
- package/dist/setup-cli.js +238 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# InSitue Dev — Claude Code
|
|
1
|
+
# InSitue Dev — Claude plugin (Code + Desktop)
|
|
2
2
|
|
|
3
|
-
Drive a
|
|
4
|
-
in the browser, describe what you want
|
|
5
|
-
|
|
6
|
-
the edit. No copy-pasting file paths. No
|
|
7
|
-
numbers. The picker IS the prompt.
|
|
3
|
+
Drive a Claude session — **Code or Desktop** — from your running
|
|
4
|
+
app. Pick an element in the browser, describe what you want
|
|
5
|
+
changed, hit Send. claude reads the file at exactly the right
|
|
6
|
+
line and proposes the edit. No copy-pasting file paths. No
|
|
7
|
+
fumbling for line numbers. The picker IS the prompt.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
┌────────────────────────────────┐ ┌────────────────────┐
|
|
@@ -26,7 +26,14 @@ numbers. The picker IS the prompt.
|
|
|
26
26
|
|
|
27
27
|
## Setup (60 seconds, one-time)
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Pick your runtime:
|
|
30
|
+
|
|
31
|
+
- **Claude Code** (the CLI / terminal app) → §1A below.
|
|
32
|
+
- **Claude Desktop** (the macOS / Windows app) → §1B below.
|
|
33
|
+
|
|
34
|
+
If you use both, do both — same MCP, same widget, same picks.
|
|
35
|
+
|
|
36
|
+
### 1A. Claude Code — install via the marketplace
|
|
30
37
|
|
|
31
38
|
In any `claude` session:
|
|
32
39
|
|
|
@@ -37,7 +44,57 @@ In any `claude` session:
|
|
|
37
44
|
|
|
38
45
|
That's it for the plugin side. The MCP server it ships will
|
|
39
46
|
auto-start the InSitue companion process in the background of
|
|
40
|
-
your `claude` session — no separate terminal to babysit.
|
|
47
|
+
your `claude` session — no separate terminal to babysit. The
|
|
48
|
+
slash command `/insitue:connect` enters the loop.
|
|
49
|
+
|
|
50
|
+
### 1B. Claude Desktop — one-command setup
|
|
51
|
+
|
|
52
|
+
Claude Desktop doesn't have a plugin marketplace, but it does
|
|
53
|
+
load MCP servers from `claude_desktop_config.json`. The package
|
|
54
|
+
ships an `insitue` CLI that writes the right entry for you:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# from your project directory
|
|
58
|
+
npx -y @insitue/claude-plugin setup --desktop
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
What it does (all idempotent + backed up):
|
|
62
|
+
|
|
63
|
+
1. Detects your OS and finds the Desktop config file
|
|
64
|
+
(`~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
65
|
+
on macOS, `%APPDATA%\Claude\…` on Windows,
|
|
66
|
+
`$XDG_CONFIG_HOME/Claude/…` on Linux).
|
|
67
|
+
2. Backs up the existing file with an `.insitue-backup-<timestamp>`
|
|
68
|
+
suffix.
|
|
69
|
+
3. Adds (or updates) a `mcpServers["insitue-<projectname>"]`
|
|
70
|
+
entry pointing at `npx -y @insitue/claude-plugin@latest`
|
|
71
|
+
with `INSITUE_PROJECT_DIR` set to your project.
|
|
72
|
+
|
|
73
|
+
Restart Claude Desktop, open a new chat, and type:
|
|
74
|
+
|
|
75
|
+
> Use the InSitue MCP — call `start_session`.
|
|
76
|
+
|
|
77
|
+
claude fetches the operating instructions, attaches to the
|
|
78
|
+
companion, and enters the loop. The slash command on Code and
|
|
79
|
+
`start_session` on Desktop deliver the exact same content.
|
|
80
|
+
|
|
81
|
+
**Multi-project?** Run `setup --desktop --project=/path/to/other`
|
|
82
|
+
in each project root. Each gets its own MCP entry
|
|
83
|
+
(`insitue-<dirname>`), so switching between projects in Desktop
|
|
84
|
+
is just picking the right server-prefix in chat.
|
|
85
|
+
|
|
86
|
+
**Want to see what would change first?** Append `--dry-run`. The
|
|
87
|
+
CLI prints the JSON entry without touching the file.
|
|
88
|
+
|
|
89
|
+
**Diagnose a setup that's misbehaving:**
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx -y @insitue/claude-plugin diagnose
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Reports project dir, session file freshness, companion
|
|
96
|
+
reachability, SDK + SWC-plugin versions + wiring, and concrete
|
|
97
|
+
recommendations.
|
|
41
98
|
|
|
42
99
|
### 2. Mount the widget in your app
|
|
43
100
|
|
package/commands/connect.md
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Drive this Claude Code
|
|
2
|
+
description: Drive this Claude session (Code or Desktop) from the InSitue browser overlay — pick + describe in the browser, claude acts here.
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
#
|
|
5
|
+
# InSitue session
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
This is the operating manual the InSitue MCP loads at session
|
|
8
|
+
start. On Claude Code it lands as the `/insitue:connect` slash
|
|
9
|
+
command; on Claude Desktop the user has claude call the
|
|
10
|
+
`start_session` tool to fetch the same content. Either way, the
|
|
11
|
+
instructions below are how you behave for the rest of the chat.
|
|
12
|
+
|
|
13
|
+
The user picks an element in their running app, types a
|
|
14
|
+
description in the InSitue panel, clicks Send — and you receive
|
|
15
|
+
the pick (file, line, component, screenshot) plus the
|
|
16
|
+
description here, ready to act on.
|
|
12
17
|
|
|
13
18
|
The companion auto-starts when this MCP server boots. You do
|
|
14
19
|
not need to ask the user to run any extra commands.
|
|
15
20
|
|
|
21
|
+
**Runtime note.** Where this manual says "use the Edit tool",
|
|
22
|
+
that means:
|
|
23
|
+
- on **Claude Code** → the built-in Edit/Write/Read tools
|
|
24
|
+
- on **Claude Desktop** → the `apply_edit` / `write_file` /
|
|
25
|
+
`read_file` tools exposed by this same MCP server
|
|
26
|
+
Either path is fine; pick whichever your runtime has.
|
|
27
|
+
|
|
16
28
|
## Your behaviour
|
|
17
29
|
|
|
18
30
|
1. Call `mcp__insitue__list_recent_picks` once. If there are
|
|
@@ -53,8 +65,8 @@ not need to ask the user to run any extra commands.
|
|
|
53
65
|
- Propose the edit with a clear diff in this chat. Wait for
|
|
54
66
|
the user to say "yes" / "approve" / "go" before writing.
|
|
55
67
|
Don't auto-apply.
|
|
56
|
-
- On approval, write with the Edit tool
|
|
57
|
-
changed.
|
|
68
|
+
- On approval, write with the Edit tool (Code) or
|
|
69
|
+
`mcp__insitue__apply_edit` (Desktop). Confirm what changed.
|
|
58
70
|
- Loop back to `next_pick`.
|
|
59
71
|
3. If `next_pick` returns `status: "timeout"`, the user simply
|
|
60
72
|
hasn't picked anything yet. Stay quiet and call `next_pick`
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// src/diagnose.ts
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { request as httpRequest } from "http";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
function readPkgVersion(projectDir, pkgName) {
|
|
6
|
+
const pkgJson = join(projectDir, "node_modules", pkgName, "package.json");
|
|
7
|
+
if (!existsSync(pkgJson)) return null;
|
|
8
|
+
try {
|
|
9
|
+
return JSON.parse(readFileSync(pkgJson, "utf8")).version ?? null;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function readSession(projectDir) {
|
|
15
|
+
const file = join(projectDir, ".insitue", "session.json");
|
|
16
|
+
if (!existsSync(file)) return null;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
19
|
+
} catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function pokeCompanion(port) {
|
|
24
|
+
return new Promise((resolve2) => {
|
|
25
|
+
const req = httpRequest(
|
|
26
|
+
{
|
|
27
|
+
host: "127.0.0.1",
|
|
28
|
+
port,
|
|
29
|
+
path: "/insitue/handshake",
|
|
30
|
+
method: "GET",
|
|
31
|
+
timeout: 1500
|
|
32
|
+
},
|
|
33
|
+
(res) => {
|
|
34
|
+
res.resume();
|
|
35
|
+
resolve2({ alive: true, subscribers: null });
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
req.on("error", () => resolve2({ alive: false, subscribers: null }));
|
|
39
|
+
req.on("timeout", () => {
|
|
40
|
+
req.destroy();
|
|
41
|
+
resolve2({ alive: false, subscribers: null });
|
|
42
|
+
});
|
|
43
|
+
req.end();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function detectSwcPluginConfigured(projectDir) {
|
|
47
|
+
for (const f of [
|
|
48
|
+
"next.config.mjs",
|
|
49
|
+
"next.config.js",
|
|
50
|
+
"next.config.ts",
|
|
51
|
+
"vite.config.ts",
|
|
52
|
+
"vite.config.js"
|
|
53
|
+
]) {
|
|
54
|
+
const p = join(projectDir, f);
|
|
55
|
+
if (existsSync(p)) {
|
|
56
|
+
try {
|
|
57
|
+
const c = readFileSync(p, "utf8");
|
|
58
|
+
return c.includes("@insitue/swc-source-attr");
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
async function diagnose(projectDir) {
|
|
67
|
+
const session = readSession(projectDir.dir);
|
|
68
|
+
const hasSessionFile = session !== null;
|
|
69
|
+
let companionReachable = false;
|
|
70
|
+
let companionPort = null;
|
|
71
|
+
let companionSubscribers = null;
|
|
72
|
+
if (session) {
|
|
73
|
+
const r = await pokeCompanion(session.port);
|
|
74
|
+
companionReachable = r.alive;
|
|
75
|
+
if (r.alive) companionPort = session.port;
|
|
76
|
+
companionSubscribers = r.subscribers;
|
|
77
|
+
}
|
|
78
|
+
const sdkVersion = readPkgVersion(projectDir.dir, "@insitue/sdk");
|
|
79
|
+
const swcPluginVersion = readPkgVersion(
|
|
80
|
+
projectDir.dir,
|
|
81
|
+
"@insitue/swc-source-attr"
|
|
82
|
+
);
|
|
83
|
+
const swcPluginConfigured = detectSwcPluginConfigured(projectDir.dir);
|
|
84
|
+
const recommendations = [];
|
|
85
|
+
if (!sdkVersion) {
|
|
86
|
+
recommendations.push(
|
|
87
|
+
"`@insitue/sdk` not installed in the project \u2014 `pnpm add -D @insitue/sdk`"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (!swcPluginVersion) {
|
|
91
|
+
recommendations.push(
|
|
92
|
+
"`@insitue/swc-source-attr` not installed \u2014 exact source resolution will degrade. `pnpm add -D @insitue/swc-source-attr`"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (swcPluginVersion && swcPluginConfigured === false) {
|
|
96
|
+
recommendations.push(
|
|
97
|
+
"`@insitue/swc-source-attr` installed but not wired into next.config / vite.config \u2014 see the SDK README for the snippet"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (!hasSessionFile) {
|
|
101
|
+
recommendations.push(
|
|
102
|
+
"No `.insitue/session.json` yet \u2014 the companion hasn't run in this project. Start `pnpm dev` and the companion should auto-spawn when claude attaches."
|
|
103
|
+
);
|
|
104
|
+
} else if (!companionReachable) {
|
|
105
|
+
recommendations.push(
|
|
106
|
+
"Stale `.insitue/session.json` (companion not reachable). Delete the `.insitue/` directory and re-attach."
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
projectDir,
|
|
111
|
+
hasSessionFile,
|
|
112
|
+
companionReachable,
|
|
113
|
+
companionPort,
|
|
114
|
+
companionSubscribers,
|
|
115
|
+
sdkVersion,
|
|
116
|
+
swcPluginVersion,
|
|
117
|
+
swcPluginConfigured,
|
|
118
|
+
recommendations
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/project-dir.ts
|
|
123
|
+
import { existsSync as existsSync2, realpathSync } from "fs";
|
|
124
|
+
import { dirname, isAbsolute, join as join2, resolve } from "path";
|
|
125
|
+
function readProjectDirArg(argv) {
|
|
126
|
+
for (let i = 0; i < argv.length; i++) {
|
|
127
|
+
const a = argv[i];
|
|
128
|
+
if (a === "--project-dir" && i + 1 < argv.length) return argv[i + 1];
|
|
129
|
+
if (a.startsWith("--project-dir=")) return a.slice("--project-dir=".length);
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
function walkUpFor(start, marker) {
|
|
134
|
+
let dir = resolve(start);
|
|
135
|
+
while (true) {
|
|
136
|
+
if (existsSync2(join2(dir, marker))) return dir;
|
|
137
|
+
const parent = dirname(dir);
|
|
138
|
+
if (parent === dir) return null;
|
|
139
|
+
dir = parent;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function realpathSafe(p) {
|
|
143
|
+
try {
|
|
144
|
+
return realpathSync(p);
|
|
145
|
+
} catch {
|
|
146
|
+
return resolve(p);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function resolveProjectDir(argv = process.argv.slice(2), env = process.env) {
|
|
150
|
+
const fromArg = readProjectDirArg(argv);
|
|
151
|
+
if (fromArg) {
|
|
152
|
+
return { dir: realpathSafe(fromArg), source: "argv" };
|
|
153
|
+
}
|
|
154
|
+
if (env.INSITUE_PROJECT_DIR) {
|
|
155
|
+
return {
|
|
156
|
+
dir: realpathSafe(env.INSITUE_PROJECT_DIR),
|
|
157
|
+
source: "INSITUE_PROJECT_DIR"
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (env.CLAUDE_PROJECT_DIR) {
|
|
161
|
+
return {
|
|
162
|
+
dir: realpathSafe(env.CLAUDE_PROJECT_DIR),
|
|
163
|
+
source: "CLAUDE_PROJECT_DIR"
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const cwd = process.cwd();
|
|
167
|
+
const sessionDir = walkUpFor(cwd, ".insitue");
|
|
168
|
+
if (sessionDir && existsSync2(join2(sessionDir, ".insitue", "session.json"))) {
|
|
169
|
+
return { dir: realpathSafe(sessionDir), source: "session-walk-up" };
|
|
170
|
+
}
|
|
171
|
+
const pkgDir = walkUpFor(cwd, "package.json");
|
|
172
|
+
if (pkgDir) {
|
|
173
|
+
return { dir: realpathSafe(pkgDir), source: "package-walk-up" };
|
|
174
|
+
}
|
|
175
|
+
return { dir: realpathSafe(cwd), source: "cwd" };
|
|
176
|
+
}
|
|
177
|
+
function isInsideProject(root, target) {
|
|
178
|
+
const r = realpathSafe(root);
|
|
179
|
+
let t;
|
|
180
|
+
try {
|
|
181
|
+
t = realpathSync(target);
|
|
182
|
+
} catch {
|
|
183
|
+
t = resolve(target);
|
|
184
|
+
}
|
|
185
|
+
if (!isAbsolute(r) || !isAbsolute(t)) return false;
|
|
186
|
+
const rWithSep = r.endsWith("/") ? r : r + "/";
|
|
187
|
+
return t === r || t.startsWith(rWithSep);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export {
|
|
191
|
+
diagnose,
|
|
192
|
+
resolveProjectDir,
|
|
193
|
+
isInsideProject
|
|
194
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/dispatcher.ts
|
|
4
|
+
var SUBCOMMANDS = /* @__PURE__ */ new Set(["setup", "diagnose", "help", "--help", "-h"]);
|
|
5
|
+
var first = process.argv[2];
|
|
6
|
+
if (first && SUBCOMMANDS.has(first)) {
|
|
7
|
+
await import("./setup-cli.js");
|
|
8
|
+
} else {
|
|
9
|
+
await import("./mcp-server.js");
|
|
10
|
+
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -1,34 +1,184 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
diagnose,
|
|
4
|
+
isInsideProject,
|
|
5
|
+
resolveProjectDir
|
|
6
|
+
} from "./chunk-RF5Q55CG.js";
|
|
2
7
|
|
|
3
8
|
// src/mcp-server.ts
|
|
4
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
10
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
11
|
import { spawn } from "child_process";
|
|
7
|
-
import { existsSync, readFileSync } from "fs";
|
|
12
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
8
13
|
import { request as httpRequest } from "http";
|
|
9
|
-
import { dirname, join
|
|
14
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
15
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10
16
|
import WebSocket from "ws";
|
|
11
17
|
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
// src/file-tools.ts
|
|
20
|
+
import {
|
|
21
|
+
existsSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
readFileSync,
|
|
24
|
+
writeFileSync
|
|
25
|
+
} from "fs";
|
|
26
|
+
import { dirname, isAbsolute, join } from "path";
|
|
27
|
+
function resolveWithin(projectDir2, path) {
|
|
28
|
+
if (!path || typeof path !== "string") {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
result: { status: "error", message: "missing or invalid `path`" }
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const abs = isAbsolute(path) ? path : join(projectDir2, path);
|
|
35
|
+
if (!isInsideProject(projectDir2, abs)) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
result: {
|
|
39
|
+
status: "error",
|
|
40
|
+
message: `refused: \`${path}\` resolves outside the project dir (${projectDir2})`
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { ok: true, abs };
|
|
45
|
+
}
|
|
46
|
+
function readFileInProject(projectDir2, path, opts = {}) {
|
|
47
|
+
const resolved = resolveWithin(projectDir2, path);
|
|
48
|
+
if (!resolved.ok) return resolved.result;
|
|
49
|
+
if (!existsSync(resolved.abs)) {
|
|
50
|
+
return { status: "error", message: `file not found: ${path}` };
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const full = readFileSync(resolved.abs, "utf8");
|
|
54
|
+
if (opts.startLine == null && opts.endLine == null) {
|
|
55
|
+
return {
|
|
56
|
+
status: "ok",
|
|
57
|
+
content: full,
|
|
58
|
+
bytes: Buffer.byteLength(full, "utf8"),
|
|
59
|
+
message: `read ${path}`
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const lines = full.split("\n");
|
|
63
|
+
const start = Math.max(1, opts.startLine ?? 1) - 1;
|
|
64
|
+
const end = Math.min(lines.length, opts.endLine ?? lines.length);
|
|
65
|
+
const slice = lines.slice(start, end).join("\n");
|
|
66
|
+
return {
|
|
67
|
+
status: "ok",
|
|
68
|
+
content: slice,
|
|
69
|
+
bytes: Buffer.byteLength(slice, "utf8"),
|
|
70
|
+
message: `read ${path} L${start + 1}-${end}`
|
|
71
|
+
};
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return {
|
|
74
|
+
status: "error",
|
|
75
|
+
message: `read failed: ${err.message}`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function applyEditInProject(projectDir2, path, oldString, newString, opts = {}) {
|
|
80
|
+
const resolved = resolveWithin(projectDir2, path);
|
|
81
|
+
if (!resolved.ok) return resolved.result;
|
|
82
|
+
if (!existsSync(resolved.abs)) {
|
|
83
|
+
return { status: "error", message: `file not found: ${path}` };
|
|
84
|
+
}
|
|
85
|
+
if (oldString === newString) {
|
|
86
|
+
return {
|
|
87
|
+
status: "error",
|
|
88
|
+
message: "`oldString` and `newString` are identical \u2014 nothing to apply"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const before = readFileSync(resolved.abs, "utf8");
|
|
93
|
+
if (!before.includes(oldString)) {
|
|
94
|
+
return {
|
|
95
|
+
status: "error",
|
|
96
|
+
message: "`oldString` not found in file \u2014 fetch the current content with read_file and retry with the exact match"
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (!opts.replaceAll) {
|
|
100
|
+
const first = before.indexOf(oldString);
|
|
101
|
+
const next = before.indexOf(oldString, first + oldString.length);
|
|
102
|
+
if (next !== -1) {
|
|
103
|
+
return {
|
|
104
|
+
status: "error",
|
|
105
|
+
message: "`oldString` matches multiple times \u2014 pass `replaceAll: true` or include more context to make the match unique"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const after = opts.replaceAll ? before.split(oldString).join(newString) : before.replace(oldString, newString);
|
|
110
|
+
writeFileSync(resolved.abs, after, "utf8");
|
|
111
|
+
const diffBytes = Buffer.byteLength(after, "utf8") - Buffer.byteLength(before, "utf8");
|
|
112
|
+
return {
|
|
113
|
+
status: "ok",
|
|
114
|
+
message: `applied edit to ${path} (${diffBytes >= 0 ? "+" : ""}${diffBytes} bytes)`,
|
|
115
|
+
bytes: Buffer.byteLength(after, "utf8")
|
|
116
|
+
};
|
|
117
|
+
} catch (err) {
|
|
118
|
+
return {
|
|
119
|
+
status: "error",
|
|
120
|
+
message: `edit failed: ${err.message}`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function writeFileInProject(projectDir2, path, content, opts = {}) {
|
|
125
|
+
const resolved = resolveWithin(projectDir2, path);
|
|
126
|
+
if (!resolved.ok) return resolved.result;
|
|
127
|
+
try {
|
|
128
|
+
if (opts.createParents) {
|
|
129
|
+
mkdirSync(dirname(resolved.abs), { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
writeFileSync(resolved.abs, content, "utf8");
|
|
132
|
+
return {
|
|
133
|
+
status: "ok",
|
|
134
|
+
message: `wrote ${path} (${Buffer.byteLength(content, "utf8")} bytes)`,
|
|
135
|
+
bytes: Buffer.byteLength(content, "utf8")
|
|
136
|
+
};
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return {
|
|
139
|
+
status: "error",
|
|
140
|
+
message: `write failed: ${err.message}`
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/instructions.ts
|
|
146
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
147
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
148
|
+
import { fileURLToPath } from "url";
|
|
149
|
+
var cached = null;
|
|
150
|
+
function loadInstructions() {
|
|
151
|
+
if (cached) return cached;
|
|
152
|
+
const here = dirname2(fileURLToPath(import.meta.url));
|
|
153
|
+
const candidates = [
|
|
154
|
+
join2(here, "..", "commands", "connect.md"),
|
|
155
|
+
join2(here, "commands", "connect.md")
|
|
156
|
+
];
|
|
157
|
+
for (const p of candidates) {
|
|
158
|
+
try {
|
|
159
|
+
cached = readFileSync2(p, "utf8");
|
|
160
|
+
return cached;
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
cached = "Call `mcp__insitue__next_pick` in a loop. Each ok-status pick has a `userNote` (the user's instruction) and a `source.file:line` (where to act). Propose an edit, ask for approval in this chat, then apply with `apply_edit` (or the runtime's native Edit tool).";
|
|
165
|
+
return cached;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/mcp-server.ts
|
|
12
169
|
var MAX_BUFFERED_PICKS = 32;
|
|
13
170
|
var NEXT_PICK_DEFAULT_TIMEOUT_MS = 25 * 1e3;
|
|
14
171
|
var NEXT_PICK_MAX_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
15
|
-
function findSession(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} catch {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
const parent = dirname(dir);
|
|
30
|
-
if (parent === dir) return null;
|
|
31
|
-
dir = parent;
|
|
172
|
+
function findSession(projectDir2) {
|
|
173
|
+
const candidate = join3(projectDir2, ".insitue", "session.json");
|
|
174
|
+
if (!existsSync2(candidate)) return null;
|
|
175
|
+
try {
|
|
176
|
+
const session2 = JSON.parse(
|
|
177
|
+
readFileSync3(candidate, "utf8")
|
|
178
|
+
);
|
|
179
|
+
return { dir: projectDir2, session: session2 };
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
32
182
|
}
|
|
33
183
|
}
|
|
34
184
|
function summariseBundle(raw) {
|
|
@@ -76,13 +226,13 @@ var PickBuffer = class {
|
|
|
76
226
|
if (this.picks.length) {
|
|
77
227
|
return Promise.resolve(this.picks.shift());
|
|
78
228
|
}
|
|
79
|
-
return new Promise((
|
|
229
|
+
return new Promise((resolve) => {
|
|
80
230
|
const timer = setTimeout(() => {
|
|
81
231
|
const idx = this.waiters.findIndex((w) => w.timer === timer);
|
|
82
232
|
if (idx >= 0) this.waiters.splice(idx, 1);
|
|
83
|
-
|
|
233
|
+
resolve(null);
|
|
84
234
|
}, timeoutMs);
|
|
85
|
-
this.waiters.push({ resolve
|
|
235
|
+
this.waiters.push({ resolve, timer });
|
|
86
236
|
});
|
|
87
237
|
}
|
|
88
238
|
recent(limit) {
|
|
@@ -114,7 +264,7 @@ async function probeCompanion(session2) {
|
|
|
114
264
|
} catch {
|
|
115
265
|
return false;
|
|
116
266
|
}
|
|
117
|
-
return new Promise((
|
|
267
|
+
return new Promise((resolve) => {
|
|
118
268
|
const req = httpRequest(
|
|
119
269
|
{
|
|
120
270
|
host: "127.0.0.1",
|
|
@@ -125,20 +275,20 @@ async function probeCompanion(session2) {
|
|
|
125
275
|
},
|
|
126
276
|
(res) => {
|
|
127
277
|
res.resume();
|
|
128
|
-
|
|
278
|
+
resolve(true);
|
|
129
279
|
}
|
|
130
280
|
);
|
|
131
|
-
req.on("error", () =>
|
|
281
|
+
req.on("error", () => resolve(false));
|
|
132
282
|
req.on("timeout", () => {
|
|
133
283
|
req.destroy();
|
|
134
|
-
|
|
284
|
+
resolve(false);
|
|
135
285
|
});
|
|
136
286
|
req.end();
|
|
137
287
|
});
|
|
138
288
|
}
|
|
139
289
|
var ownedChild = null;
|
|
140
|
-
async function ensureCompanion() {
|
|
141
|
-
const existing = findSession();
|
|
290
|
+
async function ensureCompanion(projectDir2) {
|
|
291
|
+
const existing = findSession(projectDir2);
|
|
142
292
|
if (existing && await probeCompanion(existing.session)) {
|
|
143
293
|
process.stderr.write(
|
|
144
294
|
`[insitue-mcp] reusing companion at :${existing.session.port} (pid ${existing.session.pid})
|
|
@@ -147,13 +297,14 @@ async function ensureCompanion() {
|
|
|
147
297
|
return existing.session;
|
|
148
298
|
}
|
|
149
299
|
process.stderr.write(
|
|
150
|
-
|
|
300
|
+
`[insitue-mcp] starting companion via \`npx -y @insitue/companion@latest dev\` in ${projectDir2}\u2026
|
|
301
|
+
`
|
|
151
302
|
);
|
|
152
303
|
ownedChild = spawn(
|
|
153
304
|
"npx",
|
|
154
305
|
["-y", "@insitue/companion@latest", "dev"],
|
|
155
306
|
{
|
|
156
|
-
cwd:
|
|
307
|
+
cwd: projectDir2,
|
|
157
308
|
stdio: ["ignore", "pipe", "pipe"],
|
|
158
309
|
env: process.env
|
|
159
310
|
}
|
|
@@ -172,8 +323,8 @@ async function ensureCompanion() {
|
|
|
172
323
|
ownedChild = null;
|
|
173
324
|
});
|
|
174
325
|
const start = Date.now();
|
|
175
|
-
while (Date.now() - start <
|
|
176
|
-
const found = findSession();
|
|
326
|
+
while (Date.now() - start < 8e3) {
|
|
327
|
+
const found = findSession(projectDir2);
|
|
177
328
|
if (found && await probeCompanion(found.session)) {
|
|
178
329
|
return found.session;
|
|
179
330
|
}
|
|
@@ -268,7 +419,12 @@ function connectToCompanion(session2) {
|
|
|
268
419
|
ws.on("error", () => {
|
|
269
420
|
});
|
|
270
421
|
}
|
|
271
|
-
var
|
|
422
|
+
var projectDir = resolveProjectDir();
|
|
423
|
+
process.stderr.write(
|
|
424
|
+
`[insitue-mcp] project dir: ${projectDir.dir} (via ${projectDir.source})
|
|
425
|
+
`
|
|
426
|
+
);
|
|
427
|
+
var session = await ensureCompanion(projectDir.dir);
|
|
272
428
|
if (!session) {
|
|
273
429
|
process.stderr.write(
|
|
274
430
|
"[insitue-mcp] no companion available \u2014 `next_pick` will time out.\n"
|
|
@@ -282,7 +438,7 @@ function ensureSubscriberAttached() {
|
|
|
282
438
|
}
|
|
283
439
|
var server = new McpServer({
|
|
284
440
|
name: "insitue",
|
|
285
|
-
version: "0.
|
|
441
|
+
version: "0.3.0"
|
|
286
442
|
});
|
|
287
443
|
server.registerTool(
|
|
288
444
|
"next_pick",
|
|
@@ -336,4 +492,185 @@ server.registerTool(
|
|
|
336
492
|
};
|
|
337
493
|
}
|
|
338
494
|
);
|
|
495
|
+
server.registerTool(
|
|
496
|
+
"start_session",
|
|
497
|
+
{
|
|
498
|
+
description: "Returns the operating instructions for InSitue + current state (project dir, companion reachable, buffered pick count). On Claude Code you typically don't need this \u2014 the slash command `/insitue:connect` already loaded the instructions. On Claude Desktop there are no slash commands, so call this once at the start of every session before entering the next_pick loop.",
|
|
499
|
+
inputSchema: {}
|
|
500
|
+
},
|
|
501
|
+
async () => {
|
|
502
|
+
ensureSubscriberAttached();
|
|
503
|
+
const instructions = loadInstructions();
|
|
504
|
+
const buffered = buffer.recent(32).length;
|
|
505
|
+
const status = `
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
**Current state**
|
|
510
|
+
|
|
511
|
+
- Project: \`${projectDir.dir}\` (resolved via ${projectDir.source})
|
|
512
|
+
- Companion: ${session ? `reachable on port ${session.port}` : "NOT reachable"}
|
|
513
|
+
- Buffered picks waiting: ${buffered}
|
|
514
|
+
|
|
515
|
+
Begin the loop by calling \`list_recent_picks\` once, then loop on \`next_pick\`.`;
|
|
516
|
+
return {
|
|
517
|
+
content: [
|
|
518
|
+
{ type: "text", text: instructions + status }
|
|
519
|
+
]
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
);
|
|
523
|
+
server.registerTool(
|
|
524
|
+
"diagnose",
|
|
525
|
+
{
|
|
526
|
+
description: "Run a health check on the local InSitue setup \u2014 companion reachability, SDK install, SWC plugin install + wiring, session file freshness. Returns a structured report plus human-readable recommendations. Use when picks don't seem to be flowing.",
|
|
527
|
+
inputSchema: {}
|
|
528
|
+
},
|
|
529
|
+
async () => {
|
|
530
|
+
const report = await diagnose(projectDir);
|
|
531
|
+
return {
|
|
532
|
+
content: [
|
|
533
|
+
{ type: "text", text: JSON.stringify(report, null, 2) }
|
|
534
|
+
]
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
);
|
|
538
|
+
server.registerTool(
|
|
539
|
+
"read_file",
|
|
540
|
+
{
|
|
541
|
+
description: "Read a file from the resolved project directory. Paths are relative to the project root (or absolute, in which case they must still live inside the project). Optional `startLine`/`endLine` for partial reads. On Claude Code, prefer the built-in Read tool \u2014 this exists primarily for Claude Desktop, where no built-in file tools are available.",
|
|
542
|
+
inputSchema: {
|
|
543
|
+
path: z.string().describe("Project-relative or absolute path."),
|
|
544
|
+
startLine: z.number().int().positive().optional().describe("1-indexed start line (inclusive)."),
|
|
545
|
+
endLine: z.number().int().positive().optional().describe("1-indexed end line (inclusive).")
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
async ({ path, startLine, endLine }) => {
|
|
549
|
+
const opts = {};
|
|
550
|
+
if (startLine !== void 0) opts.startLine = startLine;
|
|
551
|
+
if (endLine !== void 0) opts.endLine = endLine;
|
|
552
|
+
const r = readFileInProject(projectDir.dir, path, opts);
|
|
553
|
+
return {
|
|
554
|
+
content: [
|
|
555
|
+
{ type: "text", text: JSON.stringify(r) }
|
|
556
|
+
]
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
);
|
|
560
|
+
server.registerTool(
|
|
561
|
+
"apply_edit",
|
|
562
|
+
{
|
|
563
|
+
description: "Apply a string-replacement edit to a file inside the project. `oldString` must occur exactly once in the file (otherwise pass `replaceAll: true`). Returns a status + brief summary. ALWAYS ask the user for explicit approval before calling this \u2014 InSitue's contract is human-in-the-loop on every write. On Claude Code, prefer the built-in Edit tool.",
|
|
564
|
+
inputSchema: {
|
|
565
|
+
path: z.string().describe("Project-relative or absolute path."),
|
|
566
|
+
oldString: z.string().describe("Exact text to replace."),
|
|
567
|
+
newString: z.string().describe("Replacement text."),
|
|
568
|
+
replaceAll: z.boolean().optional().describe(
|
|
569
|
+
"Replace every occurrence of `oldString` instead of refusing on ambiguity."
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
async ({ path, oldString, newString, replaceAll }) => {
|
|
574
|
+
const opts = {};
|
|
575
|
+
if (replaceAll !== void 0) opts.replaceAll = replaceAll;
|
|
576
|
+
const r = applyEditInProject(
|
|
577
|
+
projectDir.dir,
|
|
578
|
+
path,
|
|
579
|
+
oldString,
|
|
580
|
+
newString,
|
|
581
|
+
opts
|
|
582
|
+
);
|
|
583
|
+
return {
|
|
584
|
+
content: [
|
|
585
|
+
{ type: "text", text: JSON.stringify(r) }
|
|
586
|
+
]
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
);
|
|
590
|
+
server.registerTool(
|
|
591
|
+
"write_file",
|
|
592
|
+
{
|
|
593
|
+
description: "Write the full contents of a file inside the project. Use for new files or full rewrites where `apply_edit`'s string match isn't a good fit. ALWAYS ask the user for explicit approval before calling. On Claude Code, prefer the built-in Write tool.",
|
|
594
|
+
inputSchema: {
|
|
595
|
+
path: z.string().describe("Project-relative or absolute path."),
|
|
596
|
+
content: z.string().describe("Full file contents to write."),
|
|
597
|
+
createParents: z.boolean().optional().describe("Create parent directories if they don't exist.")
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
async ({ path, content, createParents }) => {
|
|
601
|
+
const opts = {};
|
|
602
|
+
if (createParents !== void 0) opts.createParents = createParents;
|
|
603
|
+
const r = writeFileInProject(projectDir.dir, path, content, opts);
|
|
604
|
+
return {
|
|
605
|
+
content: [
|
|
606
|
+
{ type: "text", text: JSON.stringify(r) }
|
|
607
|
+
]
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
);
|
|
611
|
+
server.registerPrompt(
|
|
612
|
+
"connect",
|
|
613
|
+
{
|
|
614
|
+
title: "Connect to InSitue",
|
|
615
|
+
description: "Loads the operating instructions and begins the pick \u2192 edit loop."
|
|
616
|
+
},
|
|
617
|
+
() => ({
|
|
618
|
+
messages: [
|
|
619
|
+
{
|
|
620
|
+
role: "user",
|
|
621
|
+
content: { type: "text", text: loadInstructions() }
|
|
622
|
+
}
|
|
623
|
+
]
|
|
624
|
+
})
|
|
625
|
+
);
|
|
626
|
+
function readPkgFile(rel) {
|
|
627
|
+
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
628
|
+
for (const base of [join3(here, ".."), here]) {
|
|
629
|
+
const p = join3(base, rel);
|
|
630
|
+
if (existsSync2(p)) {
|
|
631
|
+
try {
|
|
632
|
+
return readFileSync3(p, "utf8");
|
|
633
|
+
} catch {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
server.registerResource(
|
|
641
|
+
"instructions",
|
|
642
|
+
"insitue://instructions",
|
|
643
|
+
{
|
|
644
|
+
title: "InSitue operating instructions",
|
|
645
|
+
description: "The same content that drives `/insitue:connect` on Code and `start_session` on Desktop.",
|
|
646
|
+
mimeType: "text/markdown"
|
|
647
|
+
},
|
|
648
|
+
async () => ({
|
|
649
|
+
contents: [
|
|
650
|
+
{
|
|
651
|
+
uri: "insitue://instructions",
|
|
652
|
+
mimeType: "text/markdown",
|
|
653
|
+
text: loadInstructions()
|
|
654
|
+
}
|
|
655
|
+
]
|
|
656
|
+
})
|
|
657
|
+
);
|
|
658
|
+
server.registerResource(
|
|
659
|
+
"readme",
|
|
660
|
+
"insitue://readme",
|
|
661
|
+
{
|
|
662
|
+
title: "@insitue/claude-plugin README",
|
|
663
|
+
description: "Package overview, setup steps, and runtime notes.",
|
|
664
|
+
mimeType: "text/markdown"
|
|
665
|
+
},
|
|
666
|
+
async () => ({
|
|
667
|
+
contents: [
|
|
668
|
+
{
|
|
669
|
+
uri: "insitue://readme",
|
|
670
|
+
mimeType: "text/markdown",
|
|
671
|
+
text: readPkgFile("README.md") ?? "README not bundled \u2014 see https://github.com/InSitue/insitue/tree/main/packages/claude-plugin"
|
|
672
|
+
}
|
|
673
|
+
]
|
|
674
|
+
})
|
|
675
|
+
);
|
|
339
676
|
await server.connect(new StdioServerTransport());
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
diagnose,
|
|
4
|
+
resolveProjectDir
|
|
5
|
+
} from "./chunk-RF5Q55CG.js";
|
|
6
|
+
|
|
7
|
+
// src/setup-cli.ts
|
|
8
|
+
import {
|
|
9
|
+
copyFileSync,
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
writeFileSync
|
|
14
|
+
} from "fs";
|
|
15
|
+
import { basename, dirname, join, resolve } from "path";
|
|
16
|
+
import { homedir, platform } from "os";
|
|
17
|
+
function parseArgs(argv) {
|
|
18
|
+
const positional2 = [];
|
|
19
|
+
const flags2 = /* @__PURE__ */ new Map();
|
|
20
|
+
for (let i = 0; i < argv.length; i++) {
|
|
21
|
+
const a = argv[i];
|
|
22
|
+
if (!a.startsWith("--")) {
|
|
23
|
+
positional2.push(a);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const eq = a.indexOf("=");
|
|
27
|
+
if (eq !== -1) {
|
|
28
|
+
flags2.set(a.slice(2, eq), a.slice(eq + 1));
|
|
29
|
+
} else if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
|
|
30
|
+
flags2.set(a.slice(2), argv[++i]);
|
|
31
|
+
} else {
|
|
32
|
+
flags2.set(a.slice(2), true);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { positional: positional2, flags: flags2 };
|
|
36
|
+
}
|
|
37
|
+
function desktopConfigPath() {
|
|
38
|
+
const home = homedir();
|
|
39
|
+
switch (platform()) {
|
|
40
|
+
case "darwin":
|
|
41
|
+
return join(
|
|
42
|
+
home,
|
|
43
|
+
"Library",
|
|
44
|
+
"Application Support",
|
|
45
|
+
"Claude",
|
|
46
|
+
"claude_desktop_config.json"
|
|
47
|
+
);
|
|
48
|
+
case "win32":
|
|
49
|
+
return join(
|
|
50
|
+
process.env.APPDATA ?? join(home, "AppData", "Roaming"),
|
|
51
|
+
"Claude",
|
|
52
|
+
"claude_desktop_config.json"
|
|
53
|
+
);
|
|
54
|
+
default:
|
|
55
|
+
return join(
|
|
56
|
+
process.env.XDG_CONFIG_HOME ?? join(home, ".config"),
|
|
57
|
+
"Claude",
|
|
58
|
+
"claude_desktop_config.json"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function makeEntryName(projectDir, explicit) {
|
|
63
|
+
if (explicit) return explicit;
|
|
64
|
+
const base = basename(projectDir).toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
|
65
|
+
return `insitue-${base || "project"}`;
|
|
66
|
+
}
|
|
67
|
+
function buildEntry(projectDir) {
|
|
68
|
+
return {
|
|
69
|
+
command: "npx",
|
|
70
|
+
args: ["-y", "@insitue/claude-plugin@latest"],
|
|
71
|
+
env: {
|
|
72
|
+
INSITUE_PROJECT_DIR: projectDir
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function readDesktopConfig(path) {
|
|
77
|
+
if (!existsSync(path)) return {};
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
80
|
+
} catch (err) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Couldn't parse existing config at ${path}: ${err.message}. Fix the JSON manually and re-run.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function writeDesktopConfig(path, cfg) {
|
|
87
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
88
|
+
writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
89
|
+
}
|
|
90
|
+
function backupConfig(path) {
|
|
91
|
+
if (!existsSync(path)) return null;
|
|
92
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
93
|
+
const backup = `${path}.insitue-backup-${stamp}`;
|
|
94
|
+
copyFileSync(path, backup);
|
|
95
|
+
return backup;
|
|
96
|
+
}
|
|
97
|
+
async function cmdSetup(flags2) {
|
|
98
|
+
const projectFlag = flags2.get("project");
|
|
99
|
+
const projectDir = resolve(
|
|
100
|
+
typeof projectFlag === "string" ? projectFlag : process.cwd()
|
|
101
|
+
);
|
|
102
|
+
if (!existsSync(projectDir)) {
|
|
103
|
+
process.stderr.write(`error: project dir doesn't exist: ${projectDir}
|
|
104
|
+
`);
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
const wantDesktop = flags2.has("desktop") || flags2.has("both");
|
|
108
|
+
const wantCode = flags2.has("code") || flags2.has("both");
|
|
109
|
+
const wantInteractive = !flags2.has("desktop") && !flags2.has("code") && !flags2.has("both");
|
|
110
|
+
const name = typeof flags2.get("name") === "string" ? flags2.get("name") : makeEntryName(projectDir);
|
|
111
|
+
const entry = buildEntry(projectDir);
|
|
112
|
+
const dryRun = flags2.has("dry-run");
|
|
113
|
+
if (wantInteractive) {
|
|
114
|
+
process.stdout.write(
|
|
115
|
+
`InSitue setup
|
|
116
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
117
|
+
Project: ${projectDir}
|
|
118
|
+
Entry name: ${name}
|
|
119
|
+
|
|
120
|
+
Pass one of:
|
|
121
|
+
--desktop Wire into Claude Desktop
|
|
122
|
+
--code Print the Claude Code marketplace install hint
|
|
123
|
+
--both Both runtimes
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
npx @insitue/claude-plugin setup --desktop --project=${projectDir}
|
|
127
|
+
`
|
|
128
|
+
);
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
if (wantCode) {
|
|
132
|
+
process.stdout.write(
|
|
133
|
+
"\nClaude Code setup\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nInside `claude`, run:\n\n /plugin marketplace add InSitue/insitue\n /plugin install insitue@insitue-plugins\n\nThen `/insitue:connect` to start the loop. No further config\nneeded \u2014 claude provides ${CLAUDE_PROJECT_DIR} automatically.\n"
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
if (wantDesktop) {
|
|
137
|
+
const cfgPath = desktopConfigPath();
|
|
138
|
+
process.stdout.write(
|
|
139
|
+
`
|
|
140
|
+
Claude Desktop setup
|
|
141
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
142
|
+
Config file: ${cfgPath}
|
|
143
|
+
Entry name: ${name}
|
|
144
|
+
Project: ${projectDir}
|
|
145
|
+
`
|
|
146
|
+
);
|
|
147
|
+
const cfg = readDesktopConfig(cfgPath);
|
|
148
|
+
const current = cfg.mcpServers?.[name];
|
|
149
|
+
const same = current && JSON.stringify(current) === JSON.stringify(entry);
|
|
150
|
+
if (same) {
|
|
151
|
+
process.stdout.write(
|
|
152
|
+
"\u2713 Already wired correctly \u2014 no changes needed.\n"
|
|
153
|
+
);
|
|
154
|
+
} else if (dryRun) {
|
|
155
|
+
process.stdout.write(
|
|
156
|
+
`
|
|
157
|
+
[dry-run] Would write entry:
|
|
158
|
+
"${name}": ${JSON.stringify(entry, null, 2).replace(/\n/g, "\n ")}
|
|
159
|
+
`
|
|
160
|
+
);
|
|
161
|
+
} else {
|
|
162
|
+
const backup = backupConfig(cfgPath);
|
|
163
|
+
const next = { ...cfg };
|
|
164
|
+
next.mcpServers = { ...cfg.mcpServers ?? {}, [name]: entry };
|
|
165
|
+
writeDesktopConfig(cfgPath, next);
|
|
166
|
+
process.stdout.write(
|
|
167
|
+
(backup ? `\u2713 Backed up existing config \u2192 ${backup}
|
|
168
|
+
` : "\u2713 Created new config (no existing file).\n") + `\u2713 Wrote entry "${name}".
|
|
169
|
+
|
|
170
|
+
Restart Claude Desktop, then start a new chat and tell
|
|
171
|
+
claude: "Use the InSitue MCP \u2014 call \`start_session\` and
|
|
172
|
+
follow the instructions it returns."
|
|
173
|
+
`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
process.stdout.write("\n");
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
async function cmdDiagnose(flags2) {
|
|
181
|
+
const projectFlag = flags2.get("project");
|
|
182
|
+
const argv = typeof projectFlag === "string" ? ["--project-dir", projectFlag] : [];
|
|
183
|
+
const projectDir = resolveProjectDir(argv);
|
|
184
|
+
const report = await diagnose(projectDir);
|
|
185
|
+
const tick = (b) => b ? "\u2713" : "\u2717";
|
|
186
|
+
const lines = [
|
|
187
|
+
"InSitue diagnostics",
|
|
188
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
189
|
+
`Project: ${report.projectDir.dir} (via ${report.projectDir.source})`,
|
|
190
|
+
`Session: ${tick(report.hasSessionFile)} .insitue/session.json ${report.hasSessionFile ? "exists" : "missing"}`,
|
|
191
|
+
`Companion: ${tick(report.companionReachable)} ${report.companionReachable ? `reachable on port ${report.companionPort}` : "not reachable"}`,
|
|
192
|
+
`@insitue/sdk: ${report.sdkVersion ?? "(not installed)"}`,
|
|
193
|
+
`@insitue/swc-source-attr: ${report.swcPluginVersion ?? "(not installed)"}`,
|
|
194
|
+
`SWC plugin configured: ${report.swcPluginConfigured == null ? "(no next/vite config found)" : report.swcPluginConfigured ? "yes" : "no"}`
|
|
195
|
+
];
|
|
196
|
+
if (report.recommendations.length) {
|
|
197
|
+
lines.push("", "Recommendations:");
|
|
198
|
+
for (const r of report.recommendations) lines.push(` \xB7 ${r}`);
|
|
199
|
+
} else {
|
|
200
|
+
lines.push("", "No recommendations \u2014 everything looks healthy.");
|
|
201
|
+
}
|
|
202
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
function cmdHelp() {
|
|
206
|
+
process.stdout.write(
|
|
207
|
+
"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"
|
|
208
|
+
);
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
var { positional, flags } = parseArgs(process.argv.slice(2));
|
|
212
|
+
var sub = positional[0] ?? "help";
|
|
213
|
+
try {
|
|
214
|
+
let code;
|
|
215
|
+
switch (sub) {
|
|
216
|
+
case "setup":
|
|
217
|
+
code = await cmdSetup(flags);
|
|
218
|
+
break;
|
|
219
|
+
case "diagnose":
|
|
220
|
+
code = await cmdDiagnose(flags);
|
|
221
|
+
break;
|
|
222
|
+
case "help":
|
|
223
|
+
case "--help":
|
|
224
|
+
case "-h":
|
|
225
|
+
code = cmdHelp();
|
|
226
|
+
break;
|
|
227
|
+
default:
|
|
228
|
+
process.stderr.write(`unknown command: ${sub}
|
|
229
|
+
`);
|
|
230
|
+
cmdHelp();
|
|
231
|
+
code = 2;
|
|
232
|
+
}
|
|
233
|
+
process.exit(code);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
process.stderr.write(`error: ${err.message}
|
|
236
|
+
`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@insitue/claude-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Drive
|
|
3
|
+
"version": "0.4.1",
|
|
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
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
".": "./dist/mcp-server.js"
|
|
15
15
|
},
|
|
16
16
|
"bin": {
|
|
17
|
-
"insitue-
|
|
17
|
+
"insitue-claude-plugin": "./dist/dispatcher.js"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
|
-
"build": "tsup src/mcp-server.ts --format esm --clean --external @modelcontextprotocol/sdk --external ws --external zod",
|
|
34
|
-
"dev": "tsup src/mcp-server.ts --format esm --watch --external @modelcontextprotocol/sdk --external ws --external zod",
|
|
33
|
+
"build": "tsup src/dispatcher.ts src/mcp-server.ts src/setup-cli.ts --format esm --clean --external @modelcontextprotocol/sdk --external ws --external zod",
|
|
34
|
+
"dev": "tsup src/dispatcher.ts src/mcp-server.ts src/setup-cli.ts --format esm --watch --external @modelcontextprotocol/sdk --external ws --external zod",
|
|
35
35
|
"typecheck": "tsc --noEmit",
|
|
36
36
|
"lint": "tsc --noEmit"
|
|
37
37
|
}
|