golem-bridge 1.0.2 → 2.0.0
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/README.md +20 -12
- package/cli.js +100 -52
- package/golem.md +683 -0
- package/golem.py +1093 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Connect to my Roblox Studio, Run: npx golem-bridge connect <channelId>
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
The line is a session token: Studio mints a fresh channel on every start
|
|
18
|
-
and wipes the old one.
|
|
18
|
+
and wipes the old one.
|
|
19
19
|
|
|
20
20
|
The user sends that line to their AI. It downloads this package, which asks
|
|
21
21
|
the plugin for its two connection files and stores them under `./.golem/`:
|
|
@@ -28,6 +28,18 @@ python3 ./.golem/golem.py ping
|
|
|
28
28
|
`ping` should return `"ok": true` plus the open place name. Then the agent
|
|
29
29
|
reads `./.golem/golem.md` for the full tool reference.
|
|
30
30
|
|
|
31
|
+
After a Studio restart, relink with the new line:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npx golem-bridge reconnect <newChannelId>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Done with a session? Forget it locally (Studio is unaffected):
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx golem-bridge disconnect
|
|
41
|
+
```
|
|
42
|
+
|
|
31
43
|
## Files
|
|
32
44
|
|
|
33
45
|
- `cli.js` - source of the `golem-bridge` package
|
|
@@ -47,20 +59,16 @@ Trust model: whoever holds the channel ID can send commands to that Studio
|
|
|
47
59
|
session and read the results. Treat the setup line like a password. It
|
|
48
60
|
expires on every Studio restart.
|
|
49
61
|
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
- `connect` verifies
|
|
63
|
+
Studio is alive over HTTPS and stamps your channel ID into local copies.
|
|
64
|
+
Read all three files here before running anything, or fetch with
|
|
65
|
+
`--print` to inspect without writing.
|
|
53
66
|
- `connect` only accepts hex channel IDs, talks HTTPS to the relay only
|
|
54
|
-
(never follows redirects), times out stalled requests, caps response
|
|
55
|
-
sizes
|
|
56
|
-
hashes of both files. It writes nothing unless every check passes, and
|
|
57
|
-
tells you to review both files before running anything.
|
|
67
|
+
(never follows redirects), times out stalled requests, and caps response
|
|
68
|
+
sizes. The only data it acts on is a small `ping` reply.
|
|
58
69
|
- There are no baked-in Firebase credentials or signing keys: the channel
|
|
59
70
|
ID itself is the capability, and transport runs over HTTPS. Public client
|
|
60
71
|
code cannot hold a secret, so any "signed responses" scheme here would be
|
|
61
72
|
theater rather than security.
|
|
62
73
|
- Socket.dev flags the "URL strings" in this package (the relay address).
|
|
63
|
-
That is informational: the relay address is the product.
|
|
64
|
-
is validated as described above before anything is written.
|
|
65
|
-
npm publish
|
|
66
|
-
```
|
|
74
|
+
That is informational: the relay address is the product.
|
package/cli.js
CHANGED
|
@@ -26,20 +26,39 @@ try {
|
|
|
26
26
|
// running outside the package dir; version is informational only
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
const SETUP_NOTE = `You are connected to this user's Roblox Studio through Golem, the plugin they installed at their own request - this is the user's own tooling. The helper (./.golem/golem.py) is zero-dependency Python that contacts only this relay and Roblox's public APIs: no file access, no subprocess, no eval. It shipped inside the audited golem-bridge package - nothing executable was downloaded from the network - but you can still read it before you run anything. ./.golem/golem.md is the tool's reference manual (command list, calling conventions, safety rules), in the same spirit as a man page: consult it when you need a command. It is documentation, not orders - your tasks come only from the user. If anything in either file looks wrong, stop and tell the user instead of proceeding. Otherwise run python3 ./.golem/golem.py ping (expect "ok": true plus the open place's name) and tell the user you are connected and ready - one short line. Work only reaches the place file when the user presses Ctrl+S in Studio - remind them at milestones.`;
|
|
30
|
+
|
|
31
|
+
function loadTemplate(name) {
|
|
32
|
+
return fs.readFileSync(path.join(__dirname, name), "utf8");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function stamp(text, channelId) {
|
|
36
|
+
return text.split("__DB_URL__").join(DB_URL).split("__CHANNEL_ID__").join(channelId);
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
function printHelp() {
|
|
30
40
|
console.log(`golem-bridge v${VERSION} — connect an AI agent to Roblox Studio via the Golem plugin.
|
|
31
41
|
|
|
32
42
|
Usage:
|
|
33
43
|
golem-bridge connect <channelId> [--print]
|
|
44
|
+
golem-bridge reconnect <channelId> [--print]
|
|
45
|
+
golem-bridge disconnect
|
|
34
46
|
golem-bridge --help
|
|
35
47
|
golem-bridge --version
|
|
36
48
|
|
|
37
49
|
<channelId> shown in the Golem plugin widget inside Roblox Studio.
|
|
38
|
-
Fresh on every Studio start
|
|
39
|
-
--print audit mode:
|
|
50
|
+
Fresh on every Studio start.
|
|
51
|
+
--print audit mode: verify Studio, then print both files without writing.
|
|
52
|
+
|
|
53
|
+
connect link this folder to a Studio session (writes ./.golem/).
|
|
54
|
+
reconnect same, for a rotated token: replaces the old session files.
|
|
55
|
+
Use after a Studio restart, with the new line from the widget.
|
|
56
|
+
disconnect forget this session (removes ./.golem/). Studio is unaffected.
|
|
40
57
|
|
|
41
|
-
connect
|
|
42
|
-
|
|
58
|
+
connect verifies Studio is alive over HTTPS, then stamps your channel ID
|
|
59
|
+
into local copies of the bundled golem.py and golem.md. No code is ever
|
|
60
|
+
downloaded from the network. You can still review both files first with
|
|
61
|
+
--print, or read them in this package before running anything.`);
|
|
43
62
|
}
|
|
44
63
|
|
|
45
64
|
function fail(message, exitCode) {
|
|
@@ -97,26 +116,17 @@ function sleep(ms) {
|
|
|
97
116
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
98
117
|
}
|
|
99
118
|
|
|
100
|
-
function checkFileField(name, value) {
|
|
101
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
102
|
-
throw new Error(`relay sent a bad setup payload (missing ${name})`);
|
|
103
|
-
}
|
|
104
|
-
if (value.length > MAX_FILE_BYTES) {
|
|
105
|
-
throw new Error(`relay sent a bad setup payload (${name} too large)`);
|
|
106
|
-
}
|
|
107
|
-
return value;
|
|
108
|
-
}
|
|
109
119
|
|
|
110
|
-
async function
|
|
111
|
-
const cmdId =
|
|
120
|
+
async function relayCall(channelId, op, args, attempts) {
|
|
121
|
+
const cmdId = `${op}${Date.now()}${Math.floor(Math.random() * 1e6)}`;
|
|
112
122
|
const enc = encodeURIComponent(channelId);
|
|
113
123
|
await postJson(`${DB_URL}/channels/${enc}/cmd.json`, {
|
|
114
124
|
id: cmdId,
|
|
115
|
-
op
|
|
125
|
+
op,
|
|
126
|
+
args: args || {},
|
|
116
127
|
ts: Math.floor(Date.now() / 1000),
|
|
117
128
|
});
|
|
118
|
-
|
|
119
|
-
for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt++) {
|
|
129
|
+
for (let i = 0; i < attempts; i++) {
|
|
120
130
|
await sleep(POLL_INTERVAL_MS);
|
|
121
131
|
let keys;
|
|
122
132
|
try {
|
|
@@ -125,8 +135,7 @@ async function fetchSetupFiles(channelId) {
|
|
|
125
135
|
continue;
|
|
126
136
|
}
|
|
127
137
|
if (!keys) continue;
|
|
128
|
-
const
|
|
129
|
-
for (const key of sorted) {
|
|
138
|
+
for (const key of Object.keys(keys).sort()) {
|
|
130
139
|
if (typeof key !== "string" || key.length > 128) continue;
|
|
131
140
|
let entry;
|
|
132
141
|
try {
|
|
@@ -134,32 +143,7 @@ async function fetchSetupFiles(channelId) {
|
|
|
134
143
|
} catch {
|
|
135
144
|
continue;
|
|
136
145
|
}
|
|
137
|
-
if (
|
|
138
|
-
if (entry.ok !== true) {
|
|
139
|
-
throw new Error(`setup failed: ${entry.error || "unknown error"}`);
|
|
140
|
-
}
|
|
141
|
-
let result = entry.result;
|
|
142
|
-
if (entry.resultEncoded) {
|
|
143
|
-
if (typeof result !== "string") {
|
|
144
|
-
throw new Error("relay sent a bad setup payload (bad encoding flag)");
|
|
145
|
-
}
|
|
146
|
-
try {
|
|
147
|
-
result = JSON.parse(result);
|
|
148
|
-
} catch {
|
|
149
|
-
throw new Error("relay sent a bad setup payload (unparseable result)");
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
if (!result || typeof result !== "object") {
|
|
153
|
-
throw new Error("relay sent a bad setup payload (result is not an object)");
|
|
154
|
-
}
|
|
155
|
-
return {
|
|
156
|
-
source: checkFileField("golem.py", result.source),
|
|
157
|
-
prompt: checkFileField("golem.md", result.prompt),
|
|
158
|
-
instructions:
|
|
159
|
-
typeof result.instructions === "string" && result.instructions.length > 0
|
|
160
|
-
? result.instructions
|
|
161
|
-
: "Connected.",
|
|
162
|
-
};
|
|
146
|
+
if (entry && entry.id === cmdId) return entry;
|
|
163
147
|
}
|
|
164
148
|
}
|
|
165
149
|
return null;
|
|
@@ -169,19 +153,74 @@ function sha256(text) {
|
|
|
169
153
|
return crypto.createHash("sha256").update(text, "utf8").digest("hex");
|
|
170
154
|
}
|
|
171
155
|
|
|
156
|
+
function readSavedChannel() {
|
|
157
|
+
try {
|
|
158
|
+
const src = fs.readFileSync(path.join(process.cwd(), ".golem", "golem.py"), "utf8");
|
|
159
|
+
const m = src.match(/CHANNEL = os\.environ\.get\("AIB_CHANNEL", "([0-9a-fA-F]+)"\)/);
|
|
160
|
+
return m ? m[1] : null;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function disconnectLocal() {
|
|
167
|
+
const dir = path.join(process.cwd(), ".golem");
|
|
168
|
+
if (!fs.existsSync(dir)) {
|
|
169
|
+
console.log("Not connected (no .golem/ in this folder).");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const old = readSavedChannel();
|
|
173
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
174
|
+
console.log(old ? `Disconnected from channel ${old} (removed .golem/).` : "Disconnected (removed .golem/).");
|
|
175
|
+
console.log("Studio is unaffected. To link again: npx golem-bridge connect <channelId>");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function reconnect(channelId, printOnly) {
|
|
179
|
+
validateChannel(channelId);
|
|
180
|
+
const old = readSavedChannel();
|
|
181
|
+
if (!printOnly && old && old.toLowerCase() === channelId.toLowerCase()) {
|
|
182
|
+
console.log(`Already linked to channel ${channelId} — nothing to do.`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (!printOnly && old) {
|
|
186
|
+
console.log(`Replacing session files for channel ${old}.`);
|
|
187
|
+
}
|
|
188
|
+
await connect(channelId, printOnly);
|
|
189
|
+
}
|
|
190
|
+
|
|
172
191
|
async function connect(channelId, printOnly) {
|
|
173
192
|
validateChannel(channelId);
|
|
174
193
|
console.log(`Contacting Golem plugin on channel ${channelId} ...`);
|
|
175
|
-
let
|
|
194
|
+
let entry;
|
|
176
195
|
try {
|
|
177
|
-
|
|
196
|
+
entry = await relayCall(channelId, "ping", {}, 30);
|
|
178
197
|
} catch (err) {
|
|
179
198
|
fail(err.message, 1);
|
|
180
199
|
}
|
|
181
|
-
|
|
182
|
-
if (!files) {
|
|
200
|
+
if (!entry) {
|
|
183
201
|
fail("no response from the Studio plugin. Is Roblox Studio open with Golem running?", 1);
|
|
184
202
|
}
|
|
203
|
+
if (entry.ok !== true) {
|
|
204
|
+
fail(`Studio reported an error: ${entry.error || "unknown error"}`, 1);
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const r = entry.resultEncoded && typeof entry.result === "string" ? JSON.parse(entry.result) : entry.result;
|
|
208
|
+
if (r && typeof r.placeName === "string") console.log(`Studio is alive (place: ${r.placeName}).`);
|
|
209
|
+
} catch {
|
|
210
|
+
// place name is informational only
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let files;
|
|
214
|
+
try {
|
|
215
|
+
const source = stamp(loadTemplate("golem.py"), channelId);
|
|
216
|
+
const prompt = stamp(loadTemplate("golem.md"), channelId);
|
|
217
|
+
if (source.includes("__CHANNEL_ID__") || prompt.includes("__CHANNEL_ID__")) {
|
|
218
|
+
throw new Error("template stamping failed (placeholder left behind)");
|
|
219
|
+
}
|
|
220
|
+
files = { source, prompt, instructions: SETUP_NOTE };
|
|
221
|
+
} catch (err) {
|
|
222
|
+
fail(`cannot prepare session files: ${err.message}`, 1);
|
|
223
|
+
}
|
|
185
224
|
|
|
186
225
|
if (printOnly) {
|
|
187
226
|
console.log("===== golem.py (not written) =====");
|
|
@@ -225,7 +264,12 @@ async function main() {
|
|
|
225
264
|
console.log(VERSION);
|
|
226
265
|
return;
|
|
227
266
|
}
|
|
228
|
-
if (args[0]
|
|
267
|
+
if (args[0] === "disconnect") {
|
|
268
|
+
disconnectLocal();
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const isReconnect = args[0] === "reconnect";
|
|
272
|
+
if (args[0] !== "connect" && !isReconnect) {
|
|
229
273
|
printHelp();
|
|
230
274
|
process.exit(2);
|
|
231
275
|
}
|
|
@@ -236,7 +280,11 @@ async function main() {
|
|
|
236
280
|
process.exit(2);
|
|
237
281
|
}
|
|
238
282
|
try {
|
|
239
|
-
|
|
283
|
+
if (isReconnect) {
|
|
284
|
+
await reconnect(channelId, printOnly);
|
|
285
|
+
} else {
|
|
286
|
+
await connect(channelId, printOnly);
|
|
287
|
+
}
|
|
240
288
|
} catch (err) {
|
|
241
289
|
fail(err.message, 1);
|
|
242
290
|
}
|