@spooky-sync/cli 0.0.1-canary.38 → 0.0.1-canary.380
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/AGENTS.md +246 -0
- package/README.md +6 -6
- package/devtools-mcp/index.js +24055 -30
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +2 -2
- package/dist/resolve-binary-J8VAJRMF.cjs +8 -0
- package/dist/resolve-binary-kH1Jwk0z.js +60 -0
- package/dist/resolve-binary.d.ts.map +1 -1
- package/dist/syncgen.cjs +1 -1
- package/dist/syncgen.js +1 -1
- package/package.json +13 -9
- package/templates/cookbook/INDEX.md +31 -0
- package/templates/cookbook/crdt-text-field.tsx +54 -0
- package/templates/cookbook/live-list.tsx +22 -0
- package/templates/cookbook/optimistic-mutation.tsx +33 -0
- package/devtools-mcp/bridge.d.ts +0 -23
- package/devtools-mcp/bridge.js +0 -155
- package/devtools-mcp/index.d.ts +0 -2
- package/devtools-mcp/protocol.d.ts +0 -34
- package/devtools-mcp/protocol.js +0 -37
- package/devtools-mcp/server.d.ts +0 -4
- package/devtools-mcp/server.js +0 -204
- package/devtools-mcp/surreal.d.ts +0 -13
- package/devtools-mcp/surreal.js +0 -27
- package/dist/resolve-binary-BYotYL89.js +0 -51
- package/dist/resolve-binary-CFmJcXEj.cjs +0 -8
package/dist/cli.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";const r=require("child_process"),s=require("./resolve-binary-
|
|
2
|
+
"use strict";const r=require("child_process"),s=require("./resolve-binary-J8VAJRMF.cjs"),i=s.findBinary(),e=r.spawnSync(i,process.argv.slice(2),{stdio:"inherit"});e.error&&(console.error(`Failed to execute spky: ${e.error.message}`),process.exit(1));process.exit(e.status??1);
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync as e } from "child_process";
|
|
3
|
-
import { f as s } from "./resolve-binary-
|
|
3
|
+
import { f as s } from "./resolve-binary-kH1Jwk0z.js";
|
|
4
4
|
const o = s(), r = e(o, process.argv.slice(2), { stdio: "inherit" });
|
|
5
|
-
r.error && (console.error(`Failed to execute
|
|
5
|
+
r.error && (console.error(`Failed to execute spky: ${r.error.message}`), process.exit(1));
|
|
6
6
|
process.exit(r.status ?? 1);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";const o=require("os"),e=require("path"),t=require("fs"),g=require("url"),k=require("module");var s=typeof document<"u"?document.currentScript:null;const x=g.fileURLToPath(typeof document>"u"?require("url").pathToFileURL(__filename).href:s&&s.tagName.toUpperCase()==="SCRIPT"&&s.src||new URL("resolve-binary-J8VAJRMF.cjs",document.baseURI).href),c=e.dirname(x),d={"darwin-arm64":"@spooky-sync/cli-darwin-arm64","darwin-x64":"@spooky-sync/cli-darwin-x64","linux-arm64":"@spooky-sync/cli-linux-arm64","linux-x64":"@spooky-sync/cli-linux-x64","win32-x64":"@spooky-sync/cli-win32-x64"};function $(){const r=`${o.platform()}-${o.arch()}`,n=d[r];if(!n)return;const l=o.platform()==="win32"?"spky.exe":"spky";try{const i=k.createRequire(typeof document>"u"?require("url").pathToFileURL(__filename).href:s&&s.tagName.toUpperCase()==="SCRIPT"&&s.src||new URL("resolve-binary-J8VAJRMF.cjs",document.baseURI).href).resolve(`${n}/package.json`);return e.resolve(e.dirname(i),l)}catch{return}}function w(){const r=o.platform()==="win32"?"spky.exe":"spky",n=$();if(n&&t.existsSync(n))return n;const l=[e.resolve(c,"../../../target/release",r),e.resolve(c,"../target/release",r),e.resolve(c,"../../../target/debug",r),e.resolve(c,"../target/debug",r)],u=l.filter(a=>t.existsSync(a));if(u.length>0)return u.sort((a,h)=>t.statSync(h).mtimeMs-t.statSync(a).mtimeMs),u[0];const i=e.resolve(c,"..",r);if(t.existsSync(i))return i;const p=e.resolve(process.cwd(),r);if(t.existsSync(p))return p;const m=`${o.platform()}-${o.arch()}`,y=d[m],f=y?`
|
|
2
|
+
Try installing the platform package: npm install ${y}`:`
|
|
3
|
+
Your platform (${m}) is not supported.`;throw new Error(`Could not find spky binary. Checked paths:
|
|
4
|
+
- Platform package (${y??"none"})
|
|
5
|
+
`+l.map(a=>` - ${a}
|
|
6
|
+
`).join("")+` - ${i}
|
|
7
|
+
- ${p}
|
|
8
|
+
`+f)}exports.findBinary=w;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { platform as m, arch as u } from "os";
|
|
2
|
+
import { dirname as d, resolve as t } from "path";
|
|
3
|
+
import { existsSync as c, statSync as f } from "fs";
|
|
4
|
+
import { fileURLToPath as h } from "url";
|
|
5
|
+
import { createRequire as w } from "module";
|
|
6
|
+
const $ = h(import.meta.url), i = d($), k = {
|
|
7
|
+
"darwin-arm64": "@spooky-sync/cli-darwin-arm64",
|
|
8
|
+
"darwin-x64": "@spooky-sync/cli-darwin-x64",
|
|
9
|
+
"linux-arm64": "@spooky-sync/cli-linux-arm64",
|
|
10
|
+
"linux-x64": "@spooky-sync/cli-linux-x64",
|
|
11
|
+
"win32-x64": "@spooky-sync/cli-win32-x64"
|
|
12
|
+
};
|
|
13
|
+
function P() {
|
|
14
|
+
const r = `${m()}-${u()}`, n = k[r];
|
|
15
|
+
if (!n) return;
|
|
16
|
+
const a = m() === "win32" ? "spky.exe" : "spky";
|
|
17
|
+
try {
|
|
18
|
+
const e = w(import.meta.url).resolve(`${n}/package.json`);
|
|
19
|
+
return t(d(e), a);
|
|
20
|
+
} catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function B() {
|
|
25
|
+
const r = m() === "win32" ? "spky.exe" : "spky", n = P();
|
|
26
|
+
if (n && c(n))
|
|
27
|
+
return n;
|
|
28
|
+
const a = [
|
|
29
|
+
t(i, "../../../target/release", r),
|
|
30
|
+
// workspace release
|
|
31
|
+
t(i, "../target/release", r),
|
|
32
|
+
// per-package release
|
|
33
|
+
t(i, "../../../target/debug", r),
|
|
34
|
+
// workspace debug
|
|
35
|
+
t(i, "../target/debug", r)
|
|
36
|
+
// per-package debug
|
|
37
|
+
], s = a.filter((o) => c(o));
|
|
38
|
+
if (s.length > 0)
|
|
39
|
+
return s.sort((o, x) => f(x).mtimeMs - f(o).mtimeMs), s[0];
|
|
40
|
+
const e = t(i, "..", r);
|
|
41
|
+
if (c(e))
|
|
42
|
+
return e;
|
|
43
|
+
const p = t(process.cwd(), r);
|
|
44
|
+
if (c(p))
|
|
45
|
+
return p;
|
|
46
|
+
const y = `${m()}-${u()}`, l = k[y], g = l ? `
|
|
47
|
+
Try installing the platform package: npm install ${l}` : `
|
|
48
|
+
Your platform (${y}) is not supported.`;
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Could not find spky binary. Checked paths:
|
|
51
|
+
- Platform package (${l ?? "none"})
|
|
52
|
+
` + a.map((o) => ` - ${o}
|
|
53
|
+
`).join("") + ` - ${e}
|
|
54
|
+
- ${p}
|
|
55
|
+
` + g
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
export {
|
|
59
|
+
B as f
|
|
60
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-binary.d.ts","sourceRoot":"","sources":["../src/resolve-binary.ts"],"names":[],"mappings":"AAiCA,wBAAgB,UAAU,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"resolve-binary.d.ts","sourceRoot":"","sources":["../src/resolve-binary.ts"],"names":[],"mappings":"AAiCA,wBAAgB,UAAU,IAAI,MAAM,CAuDnC"}
|
package/dist/syncgen.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("child_process"),a=require("util"),i=require("./resolve-binary-
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("child_process"),a=require("util"),i=require("./resolve-binary-J8VAJRMF.cjs"),d=a.promisify(t.execFile);async function f(e){const n=i.findBinary(),r=[];r.push("--input",e.input),r.push("--output",e.output),e.format&&r.push("--format",e.format),e.pretty&&r.push("--pretty"),e.all&&r.push("--all"),e.noHeader&&r.push("--no-header"),e.append&&r.push("--append",e.append),e.modulesDir&&r.push("--modules-dir",e.modulesDir),e.mode&&r.push("--mode",e.mode),e.endpoint&&r.push("--endpoint",e.endpoint),e.secret&&r.push("--secret",e.secret),e.config&&r.push("--config",e.config);try{const{stdout:u,stderr:c}=await d(n,r);return c&&console.error(c),u}catch(u){throw new Error(`Syncgen failed: ${u.message}`,{cause:u})}}exports.runSyncgen=f;
|
package/dist/syncgen.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile as c } from "child_process";
|
|
2
2
|
import { promisify as d } from "util";
|
|
3
|
-
import { f as t } from "./resolve-binary-
|
|
3
|
+
import { f as t } from "./resolve-binary-kH1Jwk0z.js";
|
|
4
4
|
const i = d(c);
|
|
5
5
|
async function s(e) {
|
|
6
6
|
const a = t(), r = [];
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spooky-sync/cli",
|
|
3
|
-
"version": "0.0.1-canary.
|
|
3
|
+
"version": "0.0.1-canary.380",
|
|
4
4
|
"description": "Generate TypeScript/Dart types from SurrealDB schema files",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/syncgen.cjs",
|
|
7
7
|
"module": "./dist/syncgen.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"bin": {
|
|
10
|
-
"
|
|
10
|
+
"spky": "./dist/cli.js"
|
|
11
11
|
},
|
|
12
12
|
"exports": {
|
|
13
|
+
"./devtools-mcp": "./devtools-mcp/index.js",
|
|
13
14
|
".": {
|
|
14
15
|
"import": "./dist/syncgen.js",
|
|
15
16
|
"require": "./dist/syncgen.cjs",
|
|
@@ -18,14 +19,17 @@
|
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
21
|
"dist",
|
|
21
|
-
"devtools-mcp"
|
|
22
|
+
"devtools-mcp",
|
|
23
|
+
"templates/cookbook",
|
|
24
|
+
"AGENTS.md"
|
|
22
25
|
],
|
|
23
26
|
"scripts": {
|
|
24
27
|
"dev": "vite",
|
|
28
|
+
"dev:build": "cargo build -p sp00ky-cli",
|
|
25
29
|
"build:rust": "cargo build --release",
|
|
26
30
|
"build:vite": "vite build",
|
|
27
31
|
"build:types": "tsc --emitDeclarationOnly --declaration --declarationDir dist",
|
|
28
|
-
"build:devtools-mcp": "
|
|
32
|
+
"build:devtools-mcp": "vite build --config vite.config.devtools-mcp.ts",
|
|
29
33
|
"build:js": "npm run build:vite && npm run build:types",
|
|
30
34
|
"build": "npm run build:rust && npm run build:devtools-mcp && npm run build:js",
|
|
31
35
|
"preview": "vite preview",
|
|
@@ -58,10 +62,10 @@
|
|
|
58
62
|
"vitest": "^1.0.0"
|
|
59
63
|
},
|
|
60
64
|
"optionalDependencies": {
|
|
61
|
-
"@spooky-sync/cli-darwin-arm64": "0.0.1-canary.
|
|
62
|
-
"@spooky-sync/cli-darwin-x64": "0.0.1-canary.
|
|
63
|
-
"@spooky-sync/cli-linux-arm64": "0.0.1-canary.
|
|
64
|
-
"@spooky-sync/cli-linux-x64": "0.0.1-canary.
|
|
65
|
-
"@spooky-sync/cli-win32-x64": "0.0.1-canary.
|
|
65
|
+
"@spooky-sync/cli-darwin-arm64": "0.0.1-canary.380",
|
|
66
|
+
"@spooky-sync/cli-darwin-x64": "0.0.1-canary.380",
|
|
67
|
+
"@spooky-sync/cli-linux-arm64": "0.0.1-canary.380",
|
|
68
|
+
"@spooky-sync/cli-linux-x64": "0.0.1-canary.380",
|
|
69
|
+
"@spooky-sync/cli-win32-x64": "0.0.1-canary.380"
|
|
66
70
|
}
|
|
67
71
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# sp00ky cookbook
|
|
2
|
+
|
|
3
|
+
A short, scannable list of patterns an AI agent (or human) reaches for when writing code against a sp00ky-backed app. Each entry has a one-sentence "when to use," the canonical snippet, and one gotcha.
|
|
4
|
+
|
|
5
|
+
Render any recipe with:
|
|
6
|
+
```
|
|
7
|
+
spky recipe <recipe> --table <your-table>
|
|
8
|
+
```
|
|
9
|
+
or pass `--out path/to/file.tsx` to write the snippet directly.
|
|
10
|
+
|
|
11
|
+
## Recipes
|
|
12
|
+
|
|
13
|
+
### `live-list`
|
|
14
|
+
**When to use:** you want a reactive, sorted list of rows from a table that updates as records change.
|
|
15
|
+
**Render:** `spky recipe live-list --table thread`
|
|
16
|
+
**Gotcha:** end the query with `.build()` — `useQuery` will hang on a bare builder.
|
|
17
|
+
|
|
18
|
+
### `optimistic-mutation`
|
|
19
|
+
**When to use:** you're inserting a new record from a UI action (form submit, button click) and want the local cache to update immediately while the mutation drains to the remote.
|
|
20
|
+
**Render:** `spky recipe optimistic-mutation --table thread`
|
|
21
|
+
**Gotcha:** `db.create` takes a *full* record-ID string (`'thread:abc'`), not `(table, payload)`. Use `Uuid` to mint IDs.
|
|
22
|
+
|
|
23
|
+
### `crdt-text-field`
|
|
24
|
+
**When to use:** a `TYPE string` field is annotated `-- @crdt text` in your schema and several people should edit it at once.
|
|
25
|
+
**Render:** `spky recipe crdt-text-field --table note --field body`
|
|
26
|
+
**Gotcha:** edit through `useCrdtField(table, () => id, field)` and its `getDoc()`; never put the field in a `db.create` / `db.update` payload (the server owns it). The column itself is the server-written plain text, fine to read with `useQuery` for previews, but it trails live edits until the server folds the edit log. For `@crdt prosemirror` fields bind a ProseMirror editor with `loro-prosemirror` instead of a textarea.
|
|
27
|
+
|
|
28
|
+
## Related
|
|
29
|
+
|
|
30
|
+
- See `AGENTS.md` (in your project root or `node_modules/@spooky-sync/*/AGENTS.md`) for the broader mental model and gotchas.
|
|
31
|
+
- After editing the schema: `spky generate` then `spky doctor` to confirm everything is in sync.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Recipe: crdt-text-field
|
|
2
|
+
// A collaborative textarea for `{{table}}.{{field}}`, declared in the schema as
|
|
3
|
+
//
|
|
4
|
+
// -- @crdt text
|
|
5
|
+
// DEFINE FIELD {{field}} ON TABLE {{table}} TYPE string;
|
|
6
|
+
//
|
|
7
|
+
// The editor binds to the field's Loro document. Each client sends only its own
|
|
8
|
+
// edits (rows of the generated `{{table}}__crdt_log` table, through the outbox)
|
|
9
|
+
// and merges everyone else's as they sync; the server folds them into the
|
|
10
|
+
// canonical document. The `{{field}}` column itself is the server-written plain
|
|
11
|
+
// text: render it for previews, never write it with `db.create` / `db.update`.
|
|
12
|
+
|
|
13
|
+
import { createEffect, createSignal, onCleanup, Show } from 'solid-js';
|
|
14
|
+
import { useCrdtField, useDb, useQuery } from '@spooky-sync/client-solid';
|
|
15
|
+
import { schema } from '../schema.gen';
|
|
16
|
+
|
|
17
|
+
export function {{TablePascal}}{{FieldPascal}}Editor(props: { id: string }) {
|
|
18
|
+
// 3 arguments: table, a reactive record id, field. Reopens when the id changes.
|
|
19
|
+
const field = useCrdtField('{{table}}', () => props.id, '{{field}}');
|
|
20
|
+
const [text, setText] = createSignal('');
|
|
21
|
+
|
|
22
|
+
// Follow the document: local and remote edits alike re-render the textarea.
|
|
23
|
+
createEffect(() => {
|
|
24
|
+
const doc = field()?.getDoc();
|
|
25
|
+
if (!doc) return;
|
|
26
|
+
const loroText = doc.getText('{{field}}');
|
|
27
|
+
setText(loroText.toString());
|
|
28
|
+
onCleanup(doc.subscribe(() => setText(loroText.toString())));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const handleInput = (e: InputEvent & { currentTarget: HTMLTextAreaElement }) => {
|
|
32
|
+
const doc = field()?.getDoc();
|
|
33
|
+
if (!doc) return;
|
|
34
|
+
// Diff into the document rather than replacing it, so concurrent edits merge.
|
|
35
|
+
doc.getText('{{field}}').update(e.currentTarget.value);
|
|
36
|
+
doc.commit();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Show when={field()} fallback={<p>Loading...</p>}>
|
|
41
|
+
<textarea value={text()} onInput={handleInput} />
|
|
42
|
+
</Show>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Previews read the column like any other. It is the document's text as of the
|
|
47
|
+
// server's last fold, so it trails live edits (a few seconds by default).
|
|
48
|
+
export function {{TablePascal}}{{FieldPascal}}Preview(props: { id: string }) {
|
|
49
|
+
const db = useDb<typeof schema>();
|
|
50
|
+
const result = useQuery(() =>
|
|
51
|
+
db.query('{{table}}').where({ id: props.id } as any).build()
|
|
52
|
+
);
|
|
53
|
+
return <p>{result.data()?.[0]?.{{field}}}</p>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Recipe: live-list
|
|
2
|
+
// Reactive, sorted list of `{{table}}` rows. Re-runs automatically as records change.
|
|
3
|
+
|
|
4
|
+
import { For } from 'solid-js';
|
|
5
|
+
import { useQuery, useDb } from '@spooky-sync/client-solid';
|
|
6
|
+
import { schema } from '../schema.gen';
|
|
7
|
+
|
|
8
|
+
export function {{TablePascal}}List() {
|
|
9
|
+
const db = useDb<typeof schema>();
|
|
10
|
+
|
|
11
|
+
const result = useQuery(() =>
|
|
12
|
+
db.query('{{table}}').orderBy('id', 'asc').limit(50).build()
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<ul>
|
|
17
|
+
<For each={result.data() ?? []}>
|
|
18
|
+
{(row) => <li>{row.id}</li>}
|
|
19
|
+
</For>
|
|
20
|
+
</ul>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Recipe: optimistic-mutation
|
|
2
|
+
// Insert a new `{{table}}` row from a UI action. Local cache updates immediately;
|
|
3
|
+
// the mutation drains to the remote in the background.
|
|
4
|
+
|
|
5
|
+
import { createSignal } from 'solid-js';
|
|
6
|
+
import { Uuid, useDb } from '@spooky-sync/client-solid';
|
|
7
|
+
import { schema } from '../schema.gen';
|
|
8
|
+
|
|
9
|
+
export function Create{{TablePascal}}Form() {
|
|
10
|
+
const db = useDb<typeof schema>();
|
|
11
|
+
const [submitting, setSubmitting] = createSignal(false);
|
|
12
|
+
|
|
13
|
+
const handleSubmit = async (e: SubmitEvent) => {
|
|
14
|
+
e.preventDefault();
|
|
15
|
+
setSubmitting(true);
|
|
16
|
+
try {
|
|
17
|
+
const id = new Uuid().toString();
|
|
18
|
+
await db.create(`{{table}}:${id}`, {
|
|
19
|
+
// TODO: fill in the fields your `{{table}}` schema requires.
|
|
20
|
+
});
|
|
21
|
+
} finally {
|
|
22
|
+
setSubmitting(false);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<form onSubmit={handleSubmit}>
|
|
28
|
+
<button type="submit" disabled={submitting()}>
|
|
29
|
+
{submitting() ? 'Creating…' : 'Create {{table}}'}
|
|
30
|
+
</button>
|
|
31
|
+
</form>
|
|
32
|
+
);
|
|
33
|
+
}
|
package/devtools-mcp/bridge.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
interface ConnectedTab {
|
|
2
|
-
tabId: number;
|
|
3
|
-
url?: string;
|
|
4
|
-
title?: string;
|
|
5
|
-
}
|
|
6
|
-
export declare class Bridge {
|
|
7
|
-
private wss;
|
|
8
|
-
private extensionSocket;
|
|
9
|
-
private connectedTabs;
|
|
10
|
-
private pendingRequests;
|
|
11
|
-
private requestCounter;
|
|
12
|
-
private pingInterval;
|
|
13
|
-
get isConnected(): boolean;
|
|
14
|
-
getConnectedTabs(): ConnectedTab[];
|
|
15
|
-
start(): Promise<void>;
|
|
16
|
-
private startPing;
|
|
17
|
-
private stopPing;
|
|
18
|
-
private handleMessage;
|
|
19
|
-
request(method: string, params?: Record<string, unknown>, tabId?: number): Promise<unknown>;
|
|
20
|
-
private getDefaultTabId;
|
|
21
|
-
stop(): Promise<void>;
|
|
22
|
-
}
|
|
23
|
-
export {};
|
package/devtools-mcp/bridge.js
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
|
-
import { isBridgeResponse, isBridgeNotification, BRIDGE_PORT, } from './protocol.js';
|
|
3
|
-
const REQUEST_TIMEOUT_MS = 10_000;
|
|
4
|
-
export class Bridge {
|
|
5
|
-
wss = null;
|
|
6
|
-
extensionSocket = null;
|
|
7
|
-
connectedTabs = new Map();
|
|
8
|
-
pendingRequests = new Map();
|
|
9
|
-
requestCounter = 0;
|
|
10
|
-
pingInterval = null;
|
|
11
|
-
get isConnected() {
|
|
12
|
-
return this.extensionSocket?.readyState === WebSocket.OPEN;
|
|
13
|
-
}
|
|
14
|
-
getConnectedTabs() {
|
|
15
|
-
return Array.from(this.connectedTabs.values());
|
|
16
|
-
}
|
|
17
|
-
start() {
|
|
18
|
-
const port = Number.parseInt(process.env.SP00KY_MCP_PORT || '', 10) || BRIDGE_PORT;
|
|
19
|
-
return new Promise((resolve, reject) => {
|
|
20
|
-
this.wss = new WebSocketServer({ host: '127.0.0.1', port }, () => {
|
|
21
|
-
process.stderr.write(`[sp00ky-mcp] Bridge listening on ws://127.0.0.1:${port}\n`);
|
|
22
|
-
resolve();
|
|
23
|
-
});
|
|
24
|
-
this.wss.on('error', (err) => {
|
|
25
|
-
process.stderr.write(`[sp00ky-mcp] Bridge error: ${err.message}\n`);
|
|
26
|
-
reject(err);
|
|
27
|
-
});
|
|
28
|
-
this.wss.on('connection', (ws) => {
|
|
29
|
-
process.stderr.write('[sp00ky-mcp] Extension connected\n');
|
|
30
|
-
// Only allow one extension connection at a time
|
|
31
|
-
if (this.extensionSocket) {
|
|
32
|
-
this.extensionSocket.close();
|
|
33
|
-
}
|
|
34
|
-
this.extensionSocket = ws;
|
|
35
|
-
// Start keepalive pings
|
|
36
|
-
this.startPing(ws);
|
|
37
|
-
ws.on('message', (data) => {
|
|
38
|
-
try {
|
|
39
|
-
const msg = JSON.parse(data.toString());
|
|
40
|
-
this.handleMessage(msg);
|
|
41
|
-
}
|
|
42
|
-
catch (err) {
|
|
43
|
-
process.stderr.write(`[sp00ky-mcp] Bad message: ${err}\n`);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
ws.on('close', () => {
|
|
47
|
-
process.stderr.write('[sp00ky-mcp] Extension disconnected\n');
|
|
48
|
-
if (this.extensionSocket === ws) {
|
|
49
|
-
this.extensionSocket = null;
|
|
50
|
-
this.connectedTabs.clear();
|
|
51
|
-
this.stopPing();
|
|
52
|
-
// Reject all pending requests
|
|
53
|
-
for (const [id, pending] of this.pendingRequests) {
|
|
54
|
-
pending.reject(new Error('Extension disconnected'));
|
|
55
|
-
clearTimeout(pending.timer);
|
|
56
|
-
this.pendingRequests.delete(id);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
ws.on('error', (err) => {
|
|
61
|
-
process.stderr.write(`[sp00ky-mcp] Socket error: ${err.message}\n`);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
startPing(ws) {
|
|
67
|
-
this.stopPing();
|
|
68
|
-
this.pingInterval = setInterval(() => {
|
|
69
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
70
|
-
ws.ping();
|
|
71
|
-
}
|
|
72
|
-
}, 20_000);
|
|
73
|
-
}
|
|
74
|
-
stopPing() {
|
|
75
|
-
if (this.pingInterval) {
|
|
76
|
-
clearInterval(this.pingInterval);
|
|
77
|
-
this.pingInterval = null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
handleMessage(msg) {
|
|
81
|
-
// Handle response to a pending request
|
|
82
|
-
if (isBridgeResponse(msg)) {
|
|
83
|
-
const pending = this.pendingRequests.get(msg.id);
|
|
84
|
-
if (pending) {
|
|
85
|
-
clearTimeout(pending.timer);
|
|
86
|
-
this.pendingRequests.delete(msg.id);
|
|
87
|
-
if (msg.error) {
|
|
88
|
-
pending.reject(new Error(msg.error.message));
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
pending.resolve(msg.result);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
// Handle notifications from extension
|
|
97
|
-
if (isBridgeNotification(msg)) {
|
|
98
|
-
if (msg.method === 'tabsChanged') {
|
|
99
|
-
this.connectedTabs.clear();
|
|
100
|
-
const tabs = msg.params.tabs;
|
|
101
|
-
for (const tab of tabs) {
|
|
102
|
-
this.connectedTabs.set(tab.tabId, tab);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
async request(method, params = {}, tabId) {
|
|
109
|
-
if (!this.extensionSocket || this.extensionSocket.readyState !== WebSocket.OPEN) {
|
|
110
|
-
throw new Error('No extension connected. Make sure the Sp00ky DevTools extension is running and has a page with Sp00ky open.');
|
|
111
|
-
}
|
|
112
|
-
const id = `mcp-${++this.requestCounter}`;
|
|
113
|
-
const resolvedTabId = tabId ?? this.getDefaultTabId();
|
|
114
|
-
const request = {
|
|
115
|
-
jsonrpc: '2.0',
|
|
116
|
-
id,
|
|
117
|
-
method,
|
|
118
|
-
params,
|
|
119
|
-
...(resolvedTabId !== undefined ? { tabId: resolvedTabId } : {}),
|
|
120
|
-
};
|
|
121
|
-
return new Promise((resolve, reject) => {
|
|
122
|
-
const timer = setTimeout(() => {
|
|
123
|
-
this.pendingRequests.delete(id);
|
|
124
|
-
reject(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS}ms: ${method}`));
|
|
125
|
-
}, REQUEST_TIMEOUT_MS);
|
|
126
|
-
this.pendingRequests.set(id, { resolve, reject, timer });
|
|
127
|
-
// oxlint-disable-next-line no-non-null-assertion
|
|
128
|
-
this.extensionSocket.send(JSON.stringify(request));
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
getDefaultTabId() {
|
|
132
|
-
const tabs = this.getConnectedTabs();
|
|
133
|
-
return tabs.length > 0 ? tabs[0].tabId : undefined;
|
|
134
|
-
}
|
|
135
|
-
async stop() {
|
|
136
|
-
this.stopPing();
|
|
137
|
-
for (const [id, pending] of this.pendingRequests) {
|
|
138
|
-
clearTimeout(pending.timer);
|
|
139
|
-
pending.reject(new Error('Bridge shutting down'));
|
|
140
|
-
this.pendingRequests.delete(id);
|
|
141
|
-
}
|
|
142
|
-
if (this.extensionSocket) {
|
|
143
|
-
this.extensionSocket.close();
|
|
144
|
-
this.extensionSocket = null;
|
|
145
|
-
}
|
|
146
|
-
return new Promise((resolve) => {
|
|
147
|
-
if (this.wss) {
|
|
148
|
-
this.wss.close(() => resolve());
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
resolve();
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
}
|
package/devtools-mcp/index.d.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export interface BridgeRequest {
|
|
2
|
-
jsonrpc: '2.0';
|
|
3
|
-
id: string;
|
|
4
|
-
method: string;
|
|
5
|
-
params: Record<string, unknown>;
|
|
6
|
-
tabId?: number;
|
|
7
|
-
}
|
|
8
|
-
export interface BridgeResponse {
|
|
9
|
-
jsonrpc: '2.0';
|
|
10
|
-
id: string;
|
|
11
|
-
result?: unknown;
|
|
12
|
-
error?: {
|
|
13
|
-
code: number;
|
|
14
|
-
message: string;
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
export interface BridgeNotification {
|
|
18
|
-
jsonrpc: '2.0';
|
|
19
|
-
method: string;
|
|
20
|
-
params: Record<string, unknown>;
|
|
21
|
-
}
|
|
22
|
-
export type BridgeMessage = BridgeRequest | BridgeResponse | BridgeNotification;
|
|
23
|
-
export declare const BRIDGE_METHODS: {
|
|
24
|
-
readonly GET_STATE: "getState";
|
|
25
|
-
readonly RUN_QUERY: "runQuery";
|
|
26
|
-
readonly GET_TABLE_DATA: "getTableData";
|
|
27
|
-
readonly UPDATE_TABLE_ROW: "updateTableRow";
|
|
28
|
-
readonly DELETE_TABLE_ROW: "deleteTableRow";
|
|
29
|
-
readonly CLEAR_HISTORY: "clearHistory";
|
|
30
|
-
};
|
|
31
|
-
export declare const BRIDGE_PORT = 9315;
|
|
32
|
-
export declare function isBridgeResponse(msg: unknown): msg is BridgeResponse;
|
|
33
|
-
export declare function isBridgeRequest(msg: unknown): msg is BridgeRequest;
|
|
34
|
-
export declare function isBridgeNotification(msg: unknown): msg is BridgeNotification;
|
package/devtools-mcp/protocol.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// Shared message types for MCP Server <-> Chrome Extension bridge (JSON-RPC 2.0 style)
|
|
2
|
-
// Methods the MCP server can call on the extension
|
|
3
|
-
export const BRIDGE_METHODS = {
|
|
4
|
-
GET_STATE: 'getState',
|
|
5
|
-
RUN_QUERY: 'runQuery',
|
|
6
|
-
GET_TABLE_DATA: 'getTableData',
|
|
7
|
-
UPDATE_TABLE_ROW: 'updateTableRow',
|
|
8
|
-
DELETE_TABLE_ROW: 'deleteTableRow',
|
|
9
|
-
CLEAR_HISTORY: 'clearHistory',
|
|
10
|
-
};
|
|
11
|
-
export const BRIDGE_PORT = 9315;
|
|
12
|
-
export function isBridgeResponse(msg) {
|
|
13
|
-
return (typeof msg === 'object' &&
|
|
14
|
-
msg !== null &&
|
|
15
|
-
'jsonrpc' in msg &&
|
|
16
|
-
msg.jsonrpc === '2.0' &&
|
|
17
|
-
'id' in msg &&
|
|
18
|
-
('result' in msg || 'error' in msg));
|
|
19
|
-
}
|
|
20
|
-
export function isBridgeRequest(msg) {
|
|
21
|
-
return (typeof msg === 'object' &&
|
|
22
|
-
msg !== null &&
|
|
23
|
-
'jsonrpc' in msg &&
|
|
24
|
-
msg.jsonrpc === '2.0' &&
|
|
25
|
-
'method' in msg &&
|
|
26
|
-
'id' in msg &&
|
|
27
|
-
!('result' in msg) &&
|
|
28
|
-
!('error' in msg));
|
|
29
|
-
}
|
|
30
|
-
export function isBridgeNotification(msg) {
|
|
31
|
-
return (typeof msg === 'object' &&
|
|
32
|
-
msg !== null &&
|
|
33
|
-
'jsonrpc' in msg &&
|
|
34
|
-
msg.jsonrpc === '2.0' &&
|
|
35
|
-
'method' in msg &&
|
|
36
|
-
!('id' in msg));
|
|
37
|
-
}
|
package/devtools-mcp/server.d.ts
DELETED