@looop-games/cli 0.1.7 → 0.1.8
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/lib/feedback.mjs
CHANGED
|
@@ -8,18 +8,117 @@
|
|
|
8
8
|
// looop.engine pin, and this CLI's version. A delivered file is stamped
|
|
9
9
|
// `sent:` + `id:` in its frontmatter so it never ships twice; a failed send
|
|
10
10
|
// leaves the file unstamped and is retried on the next run.
|
|
11
|
-
import { readdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
12
|
-
import { basename, join } from 'node:path';
|
|
11
|
+
import { readdirSync, readFileSync, writeFileSync, existsSync, statSync, realpathSync } from 'node:fs';
|
|
12
|
+
import { basename, join, resolve, relative, isAbsolute, sep } from 'node:path';
|
|
13
13
|
import { findProject } from './project.mjs';
|
|
14
14
|
import { getToken, getApiBase } from './config.mjs';
|
|
15
15
|
import { DEFAULT_API_BASE } from './llm-shim.mjs';
|
|
16
16
|
|
|
17
17
|
const CLI_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
18
18
|
|
|
19
|
+
// Attachment caps. The riven report cited game.js:56 and riven-render.js:302 —
|
|
20
|
+
// source files, not screenshots — so attachments are TEXT. Kept well under the
|
|
21
|
+
// intake's own ceiling so an oversized set is caught HERE, with a readable
|
|
22
|
+
// message: Cloudflare replaces a Pages Function's 5xx body with its own HTML
|
|
23
|
+
// page, which would reach the creator as an unreadable wall.
|
|
24
|
+
export const MAX_ATTACHMENTS = 20;
|
|
25
|
+
export const MAX_ATTACHMENT_BYTES = 256 * 1024; // per file
|
|
26
|
+
export const MAX_ATTACHMENTS_BYTES = 1024 * 1024; // all of them together
|
|
27
|
+
|
|
28
|
+
const frontmatter = (text) => /^---\n([\s\S]*?)\n---/.exec(text)?.[1] ?? '';
|
|
29
|
+
|
|
19
30
|
// A report is "sent" when its frontmatter carries a `sent:` stamp.
|
|
20
31
|
function isSent(text) {
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
return /^sent:\s*\S/m.test(frontmatter(text));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// `attachments:` in the report's frontmatter, in either YAML shape agents write:
|
|
36
|
+
//
|
|
37
|
+
// attachments: attachments: [game.js, src/render.js]
|
|
38
|
+
// - game.js
|
|
39
|
+
// - src/render.js
|
|
40
|
+
export function parseAttachments(text) {
|
|
41
|
+
const fm = frontmatter(text);
|
|
42
|
+
const inline = /^attachments:\s*\[(.*)\]\s*$/m.exec(fm);
|
|
43
|
+
if (inline) {
|
|
44
|
+
return inline[1]
|
|
45
|
+
.split(',')
|
|
46
|
+
.map((s) => s.trim().replace(/^['"]|['"]$/g, ''))
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
const block = /^attachments:\s*$/m.exec(fm);
|
|
50
|
+
if (!block) return [];
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const line of fm.slice(block.index).split('\n').slice(1)) {
|
|
53
|
+
const item = /^\s*-\s*(.+?)\s*$/.exec(line);
|
|
54
|
+
if (!item) break; // the list ends at the first non-item line
|
|
55
|
+
out.push(item[1].replace(/^['"]|['"]$/g, ''));
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The agent chooses this list, so it is arbitrary filesystem read access over a
|
|
61
|
+
// network. Every path must land inside the game folder — resolved, not merely
|
|
62
|
+
// inspected, because a symlink's string looks innocent. An escape is a hard
|
|
63
|
+
// error, never a skipped file.
|
|
64
|
+
function readAttachments(projectDir, paths, reportName) {
|
|
65
|
+
if (paths.length > MAX_ATTACHMENTS) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`${reportName} lists ${paths.length} attachments (max ${MAX_ATTACHMENTS}) — attach only the files the diagnosis rests on.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const root = realpathSync(projectDir);
|
|
72
|
+
const inside = (candidate) => candidate === root || candidate.startsWith(root + sep);
|
|
73
|
+
const escaped = (p, resolved) => {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`${reportName} attaches "${p}", which resolves outside the game folder (${resolved}). ` +
|
|
76
|
+
`Feedback may only attach files from this game.`,
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const files = [];
|
|
81
|
+
let total = 0;
|
|
82
|
+
|
|
83
|
+
for (const p of paths) {
|
|
84
|
+
// Containment is checked BEFORE existence, and on the lexical path first:
|
|
85
|
+
// "../../.ssh/id_rsa" is refused for what it asks for, not for whether it
|
|
86
|
+
// happens to be there. Then again after realpath — a symlink's string looks
|
|
87
|
+
// perfectly innocent.
|
|
88
|
+
const full = resolve(root, p);
|
|
89
|
+
if (isAbsolute(p) || !inside(full)) escaped(p, full);
|
|
90
|
+
|
|
91
|
+
let real;
|
|
92
|
+
try {
|
|
93
|
+
real = realpathSync(full);
|
|
94
|
+
} catch {
|
|
95
|
+
throw new Error(`${reportName} attaches "${p}", which does not exist in the game folder.`);
|
|
96
|
+
}
|
|
97
|
+
if (!inside(real)) escaped(p, real);
|
|
98
|
+
if (!statSync(real).isFile()) throw new Error(`${reportName} attaches "${p}", which is not a file.`);
|
|
99
|
+
|
|
100
|
+
const bytes = statSync(real).size;
|
|
101
|
+
if (bytes > MAX_ATTACHMENT_BYTES) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`${reportName} attaches "${p}" (${Math.round(bytes / 1024)} KiB) — too large, the per-file cap is ${MAX_ATTACHMENT_BYTES / 1024} KiB.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
total += bytes;
|
|
107
|
+
if (total > MAX_ATTACHMENTS_BYTES) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`${reportName}'s attachments are too large together (over ${MAX_ATTACHMENTS_BYTES / 1024} KiB) — attach fewer files.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const content = readFileSync(real, 'utf8');
|
|
114
|
+
if (content.includes('\0')) {
|
|
115
|
+
throw new Error(`${reportName} attaches "${p}", which is not a text file. Attach source, logs, or config.`);
|
|
116
|
+
}
|
|
117
|
+
// Report the path the creator wrote (project-relative), not the real one —
|
|
118
|
+
// it is what the report's prose cites.
|
|
119
|
+
files.push({ path: relative(root, real).split(sep).join('/'), content });
|
|
120
|
+
}
|
|
121
|
+
return files;
|
|
23
122
|
}
|
|
24
123
|
|
|
25
124
|
function titleOf(text, path) {
|
|
@@ -61,12 +160,16 @@ export async function sendFeedback({
|
|
|
61
160
|
const sent = [];
|
|
62
161
|
for (const path of unsent) {
|
|
63
162
|
const body = readFileSync(path, 'utf8');
|
|
163
|
+
// Read + guard the attachments BEFORE the request: a bad path must fail the
|
|
164
|
+
// send outright, leaving the report unstamped and retryable.
|
|
165
|
+
const files = readAttachments(project.dir, parseAttachments(body), basename(path));
|
|
64
166
|
const report = {
|
|
65
167
|
title: titleOf(body, path),
|
|
66
168
|
body,
|
|
67
169
|
slug: project.slug,
|
|
68
170
|
engine: project.pkg?.looop?.engine,
|
|
69
171
|
cli: CLI_VERSION,
|
|
172
|
+
...(files.length ? { files } : {}),
|
|
70
173
|
};
|
|
71
174
|
const res = await fetch(`${apiBase}/api/creator/feedback`, {
|
|
72
175
|
method: 'POST',
|
|
@@ -79,8 +182,9 @@ export async function sendFeedback({
|
|
|
79
182
|
}
|
|
80
183
|
const { id } = await res.json();
|
|
81
184
|
writeFileSync(path, stamp(body, id));
|
|
82
|
-
|
|
83
|
-
|
|
185
|
+
const withFiles = files.length ? ` (+${files.length} file${files.length === 1 ? '' : 's'})` : '';
|
|
186
|
+
log(`✅ Sent ${basename(path)}${withFiles} → ${id}`);
|
|
187
|
+
sent.push({ file: path, id, files: files.map((f) => f.path) });
|
|
84
188
|
}
|
|
85
189
|
log(`${sent.length} report${sent.length === 1 ? '' : 's'} delivered to the Looop team.`);
|
|
86
190
|
return { sent };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: feedback
|
|
3
|
-
description: File feedback to the Looop team when something in Looop ITSELF
|
|
3
|
+
description: File feedback to the Looop team when something in Looop ITSELF is broken or missing — the engine, a shared component, the looop CLI, login, publishing, the platform. Use the moment you or the creator think "this looks like a Looop bug, not our game" — AND, just as importantly, whenever you discover your own game breaking a Looop rule, because Looop's own instructions may be what put it there. Offer to file it right away; don't wait to be asked.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# /feedback — tell Looop when Looop is the problem
|
|
@@ -13,6 +13,34 @@ that lied, a missing capability every game would want — **offer to file it**:
|
|
|
13
13
|
|
|
14
14
|
This game's own bugs are NOT feedback — they're `/todo`s or just fixes.
|
|
15
15
|
|
|
16
|
+
## The defect you will not notice
|
|
17
|
+
|
|
18
|
+
There is one class of Looop bug the reflex above is blind to, and it is the most
|
|
19
|
+
valuable class there is: **a defect Looop's own guidance caused.**
|
|
20
|
+
|
|
21
|
+
It looks like this. You discover your game violating a Looop rule — it's routing
|
|
22
|
+
state the wrong way, using an API it shouldn't. The symptom is in *your* code, so
|
|
23
|
+
the conclusion writes itself: *"our bug, not the engine's."* You fix your game and
|
|
24
|
+
move on, and Looop never hears about it.
|
|
25
|
+
|
|
26
|
+
That inference is locally correct and globally wrong. **Looop's instructions are
|
|
27
|
+
not a fixed point — they are a suspect.** They contradict each other sometimes.
|
|
28
|
+
The mandatory ones (this repo's `AGENTS.md`, injected into every session) can be
|
|
29
|
+
wrong while an opt-in practices doc is right, and you will have followed the
|
|
30
|
+
mandatory one without ever noticing there was a choice.
|
|
31
|
+
|
|
32
|
+
So, the rule:
|
|
33
|
+
|
|
34
|
+
> **When you find your game violating a Looop rule, work out what led you there
|
|
35
|
+
> BEFORE you fix it.**
|
|
36
|
+
|
|
37
|
+
Retrace it honestly. Did a Looop-authored source — `AGENTS.md`, a
|
|
38
|
+
`shared/practices/` doc, a library README, a skill — point you this way? If yes,
|
|
39
|
+
**that is a Looop defect and it gets filed**, even though your game is also wrong.
|
|
40
|
+
Both are true at the same time; fix your game *and* file the report. If you'd have
|
|
41
|
+
made the same mistake with no guidance at all, it's just your bug — fix it and
|
|
42
|
+
move on.
|
|
43
|
+
|
|
16
44
|
## What to file
|
|
17
45
|
|
|
18
46
|
Write the report to `notes/feedback/<YYYY-MM-DD-HHMM>-<short-slug>.md`
|
|
@@ -26,6 +54,9 @@ detail you leave out is a detail they'll have to guess. Include:
|
|
|
26
54
|
created: YYYY-MM-DD
|
|
27
55
|
engine: <the looop.engine pin from package.json>
|
|
28
56
|
cli: <@looop-games/cli version from package.json devDependencies>
|
|
57
|
+
attachments:
|
|
58
|
+
- game.js
|
|
59
|
+
- src/net.js
|
|
29
60
|
---
|
|
30
61
|
# <One line: what's wrong>
|
|
31
62
|
|
|
@@ -40,6 +71,10 @@ custom port, offline, …).
|
|
|
40
71
|
The smallest steps/snippet that shows it. Exact commands **with their full
|
|
41
72
|
output pasted verbatim** — never summarize an error message.
|
|
42
73
|
|
|
74
|
+
## What led us here (if a Looop source did)
|
|
75
|
+
The file and line that pointed you wrong, quoted, and the doc that contradicts
|
|
76
|
+
it. This is the section that gets the guidance itself fixed.
|
|
77
|
+
|
|
43
78
|
## Transcript
|
|
44
79
|
The conversation that surfaced this, quoted verbatim — from the creator's
|
|
45
80
|
request that first hit the problem through your diagnosis: what they asked,
|
|
@@ -54,18 +89,37 @@ Trim game-specific noise, never evidence: cut what's irrelevant to the
|
|
|
54
89
|
defect, keep everything that shows it. (The intake caps a report at 256 KiB —
|
|
55
90
|
if the transcript pushes past that, cut it down to the relevant exchange.)
|
|
56
91
|
|
|
92
|
+
## Attach the files you're talking about
|
|
93
|
+
|
|
94
|
+
A report that cites `game.js:56` is only as good as the reader's ability to see
|
|
95
|
+
line 56. **List the files your diagnosis rests on in the `attachments:`
|
|
96
|
+
frontmatter** — they ship with the report, and the team reads them beside it.
|
|
97
|
+
|
|
98
|
+
This applies to **any** report, whatever its reason: a bug, a missing capability,
|
|
99
|
+
a rule that led you wrong, a module you had to fork. Whatever your evidence is,
|
|
100
|
+
attach it.
|
|
101
|
+
|
|
102
|
+
- **You pick the files** — you wrote the diagnosis, so you know what it rests on.
|
|
103
|
+
- Paths are **relative to the game folder** and must stay inside it. Anything
|
|
104
|
+
pointing outside is refused outright.
|
|
105
|
+
- **Text only** (source, logs, config) — up to 20 files, 256 KiB each, 1 MiB total.
|
|
106
|
+
- Attach what the report *argues from*: the file with the bug, the module you
|
|
107
|
+
worked around, the config that reproduces it. Not the whole game.
|
|
108
|
+
|
|
57
109
|
## Send it
|
|
58
110
|
|
|
59
111
|
1. Run **`npx looop feedback`** — it delivers every unsent report under
|
|
60
112
|
`notes/feedback/` to the Looop team (login required; it's the same account
|
|
61
|
-
as publishing) and stamps each delivered file with
|
|
62
|
-
frontmatter so it never ships twice.
|
|
63
|
-
2. If the send fails (offline, not logged in), the file
|
|
64
|
-
|
|
113
|
+
as publishing), attachments included, and stamps each delivered file with
|
|
114
|
+
`sent:` + `id:` in its frontmatter so it never ships twice.
|
|
115
|
+
2. If the send fails (offline, not logged in, a bad attachment path), the file
|
|
116
|
+
stays unstamped — fix what it says and run `npx looop feedback` again; it
|
|
117
|
+
retries everything unsent.
|
|
65
118
|
|
|
66
119
|
## Why bother
|
|
67
120
|
|
|
68
121
|
An override that patches an engine bug is a fork that stops receiving updates;
|
|
69
|
-
feedback is how the fix lands upstream so the fork can be deleted.
|
|
70
|
-
|
|
71
|
-
|
|
122
|
+
feedback is how the fix lands upstream so the fork can be deleted. A missing
|
|
123
|
+
capability filed from a real game is exactly how the engine decides what to build
|
|
124
|
+
next. And a rule that led you wrong will lead **every** creator wrong, in every
|
|
125
|
+
game, until somebody says so.
|