@looop-games/cli 0.1.32 → 0.1.33
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/CHANGELOG.md +14 -0
- package/lib/dev.mjs +1 -0
- package/lib/static-server.mjs +89 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,20 @@ Versions: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
14
14
|
|
|
15
15
|
## [Unreleased]
|
|
16
16
|
|
|
17
|
+
## [0.1.33] - 2026-08-27
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **The dev server now carries an agent inbox** — `POST /__looop/agent-inbox`.
|
|
21
|
+
Dev tools in the running game (starting with the Performance tool's "Send to
|
|
22
|
+
agent" button) can ship a JSON report out of the browser; it lands as one
|
|
23
|
+
file under your game's `.looop/agent-inbox/` and prints one line in the
|
|
24
|
+
`looop dev` terminal. That folder is where your agent looks when you send it
|
|
25
|
+
something from inside the game — it ignores itself in git, so reports never
|
|
26
|
+
clutter `git status`. Only pages served by your own dev server can write to
|
|
27
|
+
it (cross-origin posts from other websites are refused), and single reports
|
|
28
|
+
are capped at 2 MB. There's no cleanup/rotation yet — reports are small and
|
|
29
|
+
yours to delete.
|
|
30
|
+
|
|
17
31
|
## [0.1.32] - 2026-08-23
|
|
18
32
|
|
|
19
33
|
### Fixed
|
package/lib/dev.mjs
CHANGED
|
@@ -145,6 +145,7 @@ export async function dev({ cwd = process.cwd(), port, noMp = process.env.NO_MP
|
|
|
145
145
|
const staticServer = createStaticServer({
|
|
146
146
|
slug: project.slug,
|
|
147
147
|
projectDir: project.dir, // answers /__looop/whoami — see ports.mjs
|
|
148
|
+
log, // agent-inbox report lines ride the dev logger
|
|
148
149
|
mounts: [
|
|
149
150
|
{ url: `/games/${project.slug}/`, dir: project.dir },
|
|
150
151
|
{ url: '/shared/', dir: generatedOverridesDir },
|
package/lib/static-server.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// overrides dir mounts at /shared/ ahead of the bundle.
|
|
11
11
|
import http from 'node:http';
|
|
12
12
|
import { EventEmitter } from 'node:events';
|
|
13
|
-
import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs';
|
|
13
|
+
import { readdirSync, readFileSync, statSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
14
14
|
import { extname, join, normalize, sep } from 'node:path';
|
|
15
15
|
import { injectHeadTags, rewriteHtmlScripts, rewriteJsImports, PLATFORM_URL, TOOLBOX_URL } from './inject.mjs';
|
|
16
16
|
import { WHOAMI_PATH } from './ports.mjs';
|
|
@@ -34,6 +34,12 @@ const RELOAD_CLIENT = `<script>
|
|
|
34
34
|
|
|
35
35
|
const WATCH_EXTS = new Set(['.html', '.css', '.js', '.mjs', '.json', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp']);
|
|
36
36
|
|
|
37
|
+
// The toolbox → agent report channel (see handleAgentInbox below). The path is
|
|
38
|
+
// a contract with the toolbox shell's ctx.toAgent(); the cap keeps a runaway
|
|
39
|
+
// payload from filling the disk — reports are small JSON summaries.
|
|
40
|
+
export const AGENT_INBOX_PATH = '/__looop/agent-inbox';
|
|
41
|
+
const AGENT_INBOX_MAX_BYTES = 2 * 1024 * 1024;
|
|
42
|
+
|
|
37
43
|
const MIME = {
|
|
38
44
|
'.html': 'text/html; charset=utf-8',
|
|
39
45
|
'.js': 'text/javascript; charset=utf-8',
|
|
@@ -102,6 +108,7 @@ export function createStaticServer({
|
|
|
102
108
|
injectReload = true,
|
|
103
109
|
watchIntervalMs = 400,
|
|
104
110
|
identity,
|
|
111
|
+
log = console.log,
|
|
105
112
|
// A framework-v2 game's lowered skeleton — injected into the entry HTML as an
|
|
106
113
|
// import map + globalThis.__LOOOP_SKELETON__ so startGame() can boot. Null for
|
|
107
114
|
// a v1 game (no bare `looop` import, no skeleton).
|
|
@@ -234,10 +241,91 @@ export function createStaticServer({
|
|
|
234
241
|
});
|
|
235
242
|
}
|
|
236
243
|
|
|
244
|
+
// The agent inbox — the write half of the toolbox → agent channel (project
|
|
245
|
+
// note nf7zwa). A dev tool in the running game POSTs an envelope
|
|
246
|
+
// ({ tool, at, payload }, stamped by the toolbox shell); it lands as ONE file
|
|
247
|
+
// in <projectDir>/.looop/agent-inbox/ and prints one summary line. The file
|
|
248
|
+
// is the load-bearing sink: the agent working on this game usually does NOT
|
|
249
|
+
// hold the dev-server terminal, so stdout alone would be invisible to it.
|
|
250
|
+
// The mailbox dir ignores itself (a `.gitignore` of `*` inside it), so
|
|
251
|
+
// reports never show up in the creator's `git status` whatever repo shape
|
|
252
|
+
// the game has.
|
|
253
|
+
function handleAgentInbox(req, res) {
|
|
254
|
+
if (req.method !== 'POST') {
|
|
255
|
+
res.writeHead(405, { 'Content-Type': 'text/plain', Allow: 'POST' });
|
|
256
|
+
return res.end('POST only');
|
|
257
|
+
}
|
|
258
|
+
// Only the page this server itself served may write. A hostile website in
|
|
259
|
+
// the creator's browser can fire a no-preflight POST at localhost (a CORS
|
|
260
|
+
// "simple" content-type skips the preflight, and CORS only gates reading
|
|
261
|
+
// the response, never the server-side write) — and these files are later
|
|
262
|
+
// read by an AI agent as trusted tool reports, so a foreign page must not
|
|
263
|
+
// be able to author them. Browsers send Origin on every POST; the real
|
|
264
|
+
// client is served from this server, so its Origin equals our host.
|
|
265
|
+
// Non-browser callers (curl, node) send no Origin and pass — same trust
|
|
266
|
+
// level as anything else already running on the creator's machine/LAN.
|
|
267
|
+
const origin = req.headers.origin;
|
|
268
|
+
if (origin && origin !== `http://${req.headers.host}`) {
|
|
269
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
270
|
+
return res.end('cross-origin post rejected');
|
|
271
|
+
}
|
|
272
|
+
const chunks = [];
|
|
273
|
+
let size = 0;
|
|
274
|
+
let overflowed = false;
|
|
275
|
+
req.on('data', (chunk) => {
|
|
276
|
+
if (overflowed) return; // draining the rest so the client can read the 413
|
|
277
|
+
size += chunk.length;
|
|
278
|
+
if (size > AGENT_INBOX_MAX_BYTES) {
|
|
279
|
+
overflowed = true;
|
|
280
|
+
chunks.length = 0;
|
|
281
|
+
res.writeHead(413, { 'Content-Type': 'text/plain', Connection: 'close' });
|
|
282
|
+
res.end(`report too large (max ${AGENT_INBOX_MAX_BYTES} bytes)`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
chunks.push(chunk);
|
|
286
|
+
});
|
|
287
|
+
req.on('end', () => {
|
|
288
|
+
if (overflowed) return;
|
|
289
|
+
let envelope;
|
|
290
|
+
try {
|
|
291
|
+
envelope = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
292
|
+
} catch {
|
|
293
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
294
|
+
return res.end('body must be JSON');
|
|
295
|
+
}
|
|
296
|
+
// The tool id names the file — it must be a plain slug, never a path.
|
|
297
|
+
const tool = envelope?.tool;
|
|
298
|
+
if (typeof tool !== 'string' || !/^[a-z0-9][a-z0-9-]{0,39}$/i.test(tool)) {
|
|
299
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
300
|
+
return res.end('envelope needs a tool id ([a-z0-9-])');
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const inboxDir = join(projectDir, '.looop', 'agent-inbox');
|
|
304
|
+
mkdirSync(inboxDir, { recursive: true });
|
|
305
|
+
const selfIgnore = join(inboxDir, '.gitignore');
|
|
306
|
+
if (!existsSync(selfIgnore)) writeFileSync(selfIgnore, '*\n');
|
|
307
|
+
const d = new Date();
|
|
308
|
+
const p = (n, w = 2) => String(n).padStart(w, '0');
|
|
309
|
+
const stamp = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}-${p(d.getHours())}-${p(d.getMinutes())}-${p(d.getSeconds())}-${p(d.getMilliseconds(), 3)}`;
|
|
310
|
+
let file = join(inboxDir, `${stamp}-${tool}.json`);
|
|
311
|
+
for (let n = 2; existsSync(file); n += 1) file = join(inboxDir, `${stamp}-${tool}-${n}.json`);
|
|
312
|
+
writeFileSync(file, JSON.stringify(envelope, null, 2) + '\n');
|
|
313
|
+
log(`agent-inbox: ${tool} (${(size / 1024).toFixed(1)} KB) → ${file}`);
|
|
314
|
+
sendBody(res, 200, JSON.stringify({ ok: true, file }), 'application/json');
|
|
315
|
+
} catch (err) {
|
|
316
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
317
|
+
res.end(`agent-inbox write failed: ${err?.message ?? err}`);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
237
322
|
const server = http.createServer((req, res) => {
|
|
238
323
|
const reqUrl = new URL(req.url, 'http://x');
|
|
239
324
|
const urlPath = reqUrl.pathname;
|
|
240
325
|
if (urlPath === '/__reload') return handleSse(res);
|
|
326
|
+
// Without a projectDir there is nowhere to land a report — the route
|
|
327
|
+
// simply doesn't exist (falls through to 404), same as any unknown path.
|
|
328
|
+
if (urlPath === AGENT_INBOX_PATH && projectDir) return handleAgentInbox(req, res);
|
|
241
329
|
// Ownership probe (ports.mjs): "whose dev server is this?". It is what lets
|
|
242
330
|
// another `looop dev` — a lane, another game — tell OUR stale server (kill
|
|
243
331
|
// and reclaim) from a live one that belongs to somebody else (step around,
|