goosetools-worker 0.1.2 → 0.2.2
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 +140 -6
- package/package.json +3 -2
- package/src/fonts.js +102 -0
- package/src/normalize-photos.js +53 -0
- package/src/render.js +251 -22
- package/src/video.js +205 -0
- package/styles/graph-paper/templates/closer.html +47 -0
- package/styles/graph-paper/templates/cover.html +53 -0
- package/styles/graph-paper/templates/detail.html +56 -0
- package/worker/agent-cli.js +275 -0
- package/worker/cli.js +58 -2
- package/worker/index.js +542 -30
- package/worker/service.js +395 -0
- package/worker/skill.js +26 -0
- package/worker/style.js +250 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<div class="slide detail">
|
|
2
|
+
<div class="handle">[ {{handle}} ]</div>
|
|
3
|
+
<div class="kicker">{{subtitle}}</div>
|
|
4
|
+
<div class="name">{{name}}</div>
|
|
5
|
+
<div class="body">{{text}}</div>
|
|
6
|
+
{{ui}}
|
|
7
|
+
</div>
|
|
8
|
+
<style>
|
|
9
|
+
.detail {
|
|
10
|
+
{{gridBg}}
|
|
11
|
+
color: {{colors.textOnLight}};
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
align-items: center;
|
|
15
|
+
text-align: center;
|
|
16
|
+
}
|
|
17
|
+
.detail .handle {
|
|
18
|
+
align-self: flex-start;
|
|
19
|
+
font-family: "{{fonts.pixel}}", monospace;
|
|
20
|
+
text-transform: uppercase;
|
|
21
|
+
font-weight: 400;
|
|
22
|
+
font-size: 42px;
|
|
23
|
+
color: {{colors.primary}};
|
|
24
|
+
}
|
|
25
|
+
.detail .handle:empty { display: none; }
|
|
26
|
+
.detail .kicker {
|
|
27
|
+
margin-top: 96px;
|
|
28
|
+
font-family: "{{fonts.pixel}}", monospace;
|
|
29
|
+
text-transform: uppercase;
|
|
30
|
+
font-size: 44px;
|
|
31
|
+
color: {{colors.primary}};
|
|
32
|
+
}
|
|
33
|
+
.detail .kicker:empty { display: none; }
|
|
34
|
+
.detail .name {
|
|
35
|
+
margin-top: 32px;
|
|
36
|
+
font-family: "{{fonts.heading}}", serif;
|
|
37
|
+
font-weight: {{fonts.headingWeight}};
|
|
38
|
+
font-size: 116px;
|
|
39
|
+
line-height: 1.05;
|
|
40
|
+
letter-spacing: -0.01em;
|
|
41
|
+
color: {{colors.primary}};
|
|
42
|
+
}
|
|
43
|
+
.detail .body {
|
|
44
|
+
margin-top: 72px;
|
|
45
|
+
margin-bottom: auto;
|
|
46
|
+
font-family: "{{fonts.mono}}", monospace;
|
|
47
|
+
font-size: 40px;
|
|
48
|
+
line-height: 1.65;
|
|
49
|
+
max-width: 880px;
|
|
50
|
+
text-align: left;
|
|
51
|
+
white-space: pre-wrap;
|
|
52
|
+
color: {{colors.textOnLight}};
|
|
53
|
+
}
|
|
54
|
+
.detail .body em { font-style: normal; color: {{colors.primary}}; }
|
|
55
|
+
.detail .body i { font-style: italic; }
|
|
56
|
+
</style>
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// Agent CLI adapter — runs a job's prompt through whichever coding-agent CLI
|
|
2
|
+
// the user picked on goosetools.com (claim payloads carry `agentCli`).
|
|
3
|
+
//
|
|
4
|
+
// VENDORED FILE: keep byte-identical across brand-manager-worker,
|
|
5
|
+
// caption-maker, and carousel-maker (worker/agent-cli.js in each). Fix a bug
|
|
6
|
+
// here, copy it to the other two.
|
|
7
|
+
//
|
|
8
|
+
// Modes, chosen for speed-vs-capability (see brand-manager commit c0c1b33 —
|
|
9
|
+
// tool-looping turned 36-second drafts into 10-minute ones):
|
|
10
|
+
// "text" — plain prompt-in/text-out, no tool flags at all (captions).
|
|
11
|
+
// "oneshot" — tools explicitly OFF, all context inlined by the caller.
|
|
12
|
+
// "read" — file-reading allowed (carousel drafts look at photos).
|
|
13
|
+
// "session" — tools ON and resumable where the CLI supports it.
|
|
14
|
+
//
|
|
15
|
+
// Honest capability notes per CLI:
|
|
16
|
+
// claude — full support: fine tool control, JSON envelope, --resume.
|
|
17
|
+
// codex — sandbox-level tool control only; resumable threads; JSONL out.
|
|
18
|
+
// gemini — no headless resume: sessionId is always null, callers replay
|
|
19
|
+
// history (they already do this whenever sessionId is absent).
|
|
20
|
+
// opencode — plain text out; session continuation is version-dependent, so
|
|
21
|
+
// we don't claim it: sessionId is always null.
|
|
22
|
+
//
|
|
23
|
+
// Session ids are namespaced ("claude:<uuid>", "codex:<thread-id>") because
|
|
24
|
+
// the server stores them per conversation and the user can switch agents
|
|
25
|
+
// between turns — a Claude uuid means nothing to Codex. resolveSession()
|
|
26
|
+
// drops a mismatched id so the caller falls back to history replay. Bare
|
|
27
|
+
// un-prefixed ids predate namespacing and are treated as Claude's.
|
|
28
|
+
|
|
29
|
+
import { spawn } from "node:child_process";
|
|
30
|
+
import { spawnSync } from "node:child_process";
|
|
31
|
+
|
|
32
|
+
export const AGENT_IDS = ["claude-code", "codex", "gemini", "opencode"];
|
|
33
|
+
|
|
34
|
+
const INSTALL_HINTS = {
|
|
35
|
+
"claude-code": "npm install -g @anthropic-ai/claude-code",
|
|
36
|
+
codex: "npm install -g @openai/codex, then run: codex login",
|
|
37
|
+
gemini: "npm install -g @google/gemini-cli, then run: gemini (to sign in)",
|
|
38
|
+
opencode: "npm install -g opencode-ai, then run: opencode auth login",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const LABELS = {
|
|
42
|
+
"claude-code": "Claude Code",
|
|
43
|
+
codex: "Codex CLI",
|
|
44
|
+
gemini: "Gemini CLI",
|
|
45
|
+
opencode: "OpenCode",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Session-id namespace per agent. claude-code writes "claude:" for continuity
|
|
49
|
+
// with the bare ids already stored server-side.
|
|
50
|
+
const SESSION_PREFIX = {
|
|
51
|
+
"claude-code": "claude",
|
|
52
|
+
codex: "codex",
|
|
53
|
+
gemini: "gemini",
|
|
54
|
+
opencode: "opencode",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function bin(agent) {
|
|
58
|
+
if (agent === "claude-code") return process.env.CLAUDE_BIN ?? "claude";
|
|
59
|
+
return agent; // codex / gemini / opencode binaries share their id
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** "claude" (legacy claim payloads) → "claude-code"; unknown → null. */
|
|
63
|
+
export function normalizeAgent(v) {
|
|
64
|
+
if (v == null || v === "" ) return "claude-code";
|
|
65
|
+
if (v === "claude") return "claude-code";
|
|
66
|
+
return AGENT_IDS.includes(v) ? v : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Turn a stored (possibly namespaced, possibly another agent's) session id
|
|
71
|
+
* into one usable by `agent`, or null — null tells the caller to inline the
|
|
72
|
+
* conversation history instead of resuming.
|
|
73
|
+
*/
|
|
74
|
+
export function resolveSession(agent, sessionId) {
|
|
75
|
+
if (!sessionId) return null;
|
|
76
|
+
const idx = sessionId.indexOf(":");
|
|
77
|
+
if (idx === -1) {
|
|
78
|
+
// Legacy bare id — those were always Claude Code session uuids.
|
|
79
|
+
return agent === "claude-code" ? sessionId : null;
|
|
80
|
+
}
|
|
81
|
+
const prefix = sessionId.slice(0, idx);
|
|
82
|
+
return prefix === SESSION_PREFIX[agent] ? sessionId.slice(idx + 1) : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function namespaced(agent, rawId) {
|
|
86
|
+
return rawId ? `${SESSION_PREFIX[agent]}:${rawId}` : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Strip ANSI escapes and other terminal noise some CLIs print around answers.
|
|
90
|
+
export function cleanText(text) {
|
|
91
|
+
// eslint-disable-next-line no-control-regex
|
|
92
|
+
return String(text).replace(/\[[0-9;]*[A-Za-z]/g, "").trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// stdio[0] MUST be "ignore". Inherited stdin makes invocations hang forever
|
|
96
|
+
// under launchd — brand-manager hit this exact bug (fixed in 3b1f955) and it
|
|
97
|
+
// presents as the worker silently doing nothing after a reboot.
|
|
98
|
+
function run(agent, args, { timeoutMs, cwd }) {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const child = spawn(bin(agent), args, {
|
|
101
|
+
cwd,
|
|
102
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
103
|
+
});
|
|
104
|
+
let stdout = "";
|
|
105
|
+
let stderr = "";
|
|
106
|
+
child.stdout.on("data", (d) => (stdout += d));
|
|
107
|
+
child.stderr.on("data", (d) => (stderr += d));
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
child.kill("SIGKILL");
|
|
110
|
+
reject(new Error(`${LABELS[agent]} timed out after ${timeoutMs}ms`));
|
|
111
|
+
}, timeoutMs);
|
|
112
|
+
child.on("error", (err) => {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
reject(
|
|
115
|
+
err.code === "ENOENT"
|
|
116
|
+
? new Error(
|
|
117
|
+
`${LABELS[agent]} isn't installed on this computer. Install: ${INSTALL_HINTS[agent]} — or switch your agent back to Claude Code in Setup on goosetools.com.`,
|
|
118
|
+
)
|
|
119
|
+
: err,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
child.on("close", (code) => {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
if (code === 0) resolve(stdout);
|
|
125
|
+
else
|
|
126
|
+
reject(
|
|
127
|
+
new Error(`${LABELS[agent]} exited ${code}: ${stderr.slice(0, 600)}`),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Is the CLI on PATH? { installed, hint } — cheap, for startup diagnostics. */
|
|
134
|
+
export function detect(agent) {
|
|
135
|
+
const res = spawnSync(bin(agent), ["--version"], {
|
|
136
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
137
|
+
timeout: 10_000,
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
installed: res.status === 0,
|
|
141
|
+
hint: INSTALL_HINTS[agent],
|
|
142
|
+
label: LABELS[agent],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Tools Claude must NOT touch in oneshot mode — everything is inlined.
|
|
147
|
+
const CLAUDE_TOOLS_OFF = "Read,Edit,Write,Bash,Glob,Grep,WebFetch,WebSearch";
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Run a prompt through the chosen agent CLI.
|
|
151
|
+
* Returns { text, sessionId } — sessionId is namespaced and null whenever
|
|
152
|
+
* the CLI can't resume (callers then replay history on the next turn).
|
|
153
|
+
*/
|
|
154
|
+
export async function runAgent({
|
|
155
|
+
agent = "claude-code",
|
|
156
|
+
prompt,
|
|
157
|
+
cwd,
|
|
158
|
+
mode = "text",
|
|
159
|
+
sessionId = null,
|
|
160
|
+
tools = "Read,Edit,Write,Glob,Grep",
|
|
161
|
+
timeoutMs = 5 * 60 * 1000,
|
|
162
|
+
}) {
|
|
163
|
+
const normalized = normalizeAgent(agent);
|
|
164
|
+
if (!normalized) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Unknown agent "${agent}" — update this worker: npx --yes <package>@latest install`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const resume = resolveSession(normalized, sessionId);
|
|
170
|
+
|
|
171
|
+
if (normalized === "claude-code") {
|
|
172
|
+
if (mode === "session") {
|
|
173
|
+
const args = [
|
|
174
|
+
"-p",
|
|
175
|
+
prompt,
|
|
176
|
+
"--output-format",
|
|
177
|
+
"json",
|
|
178
|
+
"--permission-mode",
|
|
179
|
+
"acceptEdits",
|
|
180
|
+
"--allowedTools",
|
|
181
|
+
tools,
|
|
182
|
+
];
|
|
183
|
+
if (resume) args.push("--resume", resume);
|
|
184
|
+
const raw = await run(normalized, args, { timeoutMs, cwd });
|
|
185
|
+
// The json envelope carries the session id. Be forgiving: a shape
|
|
186
|
+
// change shouldn't lose the user's answer.
|
|
187
|
+
try {
|
|
188
|
+
const parsed = JSON.parse(raw);
|
|
189
|
+
return {
|
|
190
|
+
text: parsed.result ?? parsed.text ?? raw,
|
|
191
|
+
sessionId: namespaced(
|
|
192
|
+
normalized,
|
|
193
|
+
parsed.session_id ?? parsed.sessionId ?? resume ?? null,
|
|
194
|
+
),
|
|
195
|
+
};
|
|
196
|
+
} catch {
|
|
197
|
+
return { text: raw, sessionId: namespaced(normalized, resume) };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const args = ["-p", prompt, "--output-format", "text"];
|
|
201
|
+
if (mode === "oneshot") args.push("--disallowedTools", CLAUDE_TOOLS_OFF);
|
|
202
|
+
if (mode === "read") args.push("--allowedTools", "Read");
|
|
203
|
+
const text = await run(normalized, args, { timeoutMs, cwd });
|
|
204
|
+
return { text, sessionId: null };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (normalized === "codex") {
|
|
208
|
+
// Sandbox is the only tool control codex offers; read-only + the prompt's
|
|
209
|
+
// own "don't use tools" language is the closest match to oneshot.
|
|
210
|
+
const sandbox = mode === "session" ? "workspace-write" : "read-only";
|
|
211
|
+
const args = ["exec"];
|
|
212
|
+
if (resume) args.push("resume", resume);
|
|
213
|
+
args.push("--skip-git-repo-check", "--sandbox", sandbox, "--json");
|
|
214
|
+
if (cwd) args.push("--cd", cwd);
|
|
215
|
+
args.push(prompt);
|
|
216
|
+
const raw = await run(normalized, args, { timeoutMs, cwd });
|
|
217
|
+
// --json emits JSONL events; harvest the thread id and the last agent
|
|
218
|
+
// message, falling back to the raw output if the shape ever changes.
|
|
219
|
+
let threadId = resume ?? null;
|
|
220
|
+
let last = null;
|
|
221
|
+
for (const line of raw.split("\n")) {
|
|
222
|
+
const trimmed = line.trim();
|
|
223
|
+
if (!trimmed.startsWith("{")) continue;
|
|
224
|
+
try {
|
|
225
|
+
const evt = JSON.parse(trimmed);
|
|
226
|
+
threadId =
|
|
227
|
+
evt.thread_id ?? evt.session_id ?? evt.thread?.id ?? threadId;
|
|
228
|
+
const item = evt.item ?? evt;
|
|
229
|
+
const type = item.item_type ?? item.type;
|
|
230
|
+
if (
|
|
231
|
+
(type === "agent_message" || type === "assistant_message") &&
|
|
232
|
+
typeof (item.text ?? item.message) === "string"
|
|
233
|
+
) {
|
|
234
|
+
last = item.text ?? item.message;
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// not an event line — ignore
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
text: cleanText(last ?? raw),
|
|
242
|
+
sessionId: mode === "session" ? namespaced(normalized, threadId) : null,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (normalized === "gemini") {
|
|
247
|
+
// No reliable headless resume — never claim one. Session mode gets yolo
|
|
248
|
+
// approvals so its tools can run unattended.
|
|
249
|
+
const args = ["-p", prompt, "--output-format", "json"];
|
|
250
|
+
if (mode === "session") args.push("--yolo");
|
|
251
|
+
let raw;
|
|
252
|
+
try {
|
|
253
|
+
raw = await run(normalized, args, { timeoutMs, cwd });
|
|
254
|
+
} catch (err) {
|
|
255
|
+
// Older gemini builds lack --output-format; retry plain.
|
|
256
|
+
if (!/output-format|unknown option/i.test(String(err.message))) throw err;
|
|
257
|
+
raw = await run(normalized, ["-p", prompt], { timeoutMs, cwd });
|
|
258
|
+
return { text: cleanText(raw), sessionId: null };
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const parsed = JSON.parse(raw);
|
|
262
|
+
return {
|
|
263
|
+
text: cleanText(parsed.response ?? parsed.result ?? raw),
|
|
264
|
+
sessionId: null,
|
|
265
|
+
};
|
|
266
|
+
} catch {
|
|
267
|
+
return { text: cleanText(raw), sessionId: null };
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// opencode — plain text out; we don't claim session continuation (it's
|
|
272
|
+
// version-dependent), so every turn replays history.
|
|
273
|
+
const raw = await run(normalized, ["run", prompt], { timeoutMs, cwd });
|
|
274
|
+
return { text: cleanText(raw), sessionId: null };
|
|
275
|
+
}
|
package/worker/cli.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// goosetools-worker entrypoint — makes first run painless for non-technical
|
|
3
3
|
// users: checks for Claude Code + the Playwright browser, installs the
|
|
4
|
-
// browser if missing, then
|
|
4
|
+
// browser if missing, then runs the requested command.
|
|
5
|
+
//
|
|
6
|
+
// install save token + register a login service (worker runs forever)
|
|
7
|
+
// update pull the newest code into the background copy + restart
|
|
8
|
+
// status what's installed, whether it's running, recent log lines
|
|
9
|
+
// uninstall remove the service + saved token
|
|
10
|
+
// run run the worker loop in this window (default)
|
|
5
11
|
|
|
6
12
|
import { execFileSync, execSync } from "node:child_process";
|
|
7
13
|
import { existsSync } from "node:fs";
|
|
@@ -11,6 +17,30 @@ import { fileURLToPath } from "node:url";
|
|
|
11
17
|
|
|
12
18
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
19
|
|
|
20
|
+
const command = ["install", "update", "status", "uninstall", "run", "doctor"].includes(process.argv[2])
|
|
21
|
+
? process.argv[2]
|
|
22
|
+
: "run";
|
|
23
|
+
|
|
24
|
+
// These two only inspect or tear down what's already there, so they skip the
|
|
25
|
+
// Claude Code / browser preflight below — `status` in particular has to work
|
|
26
|
+
// precisely when something IS wrong.
|
|
27
|
+
if (command === "uninstall") {
|
|
28
|
+
const { uninstall } = await import("./service.js");
|
|
29
|
+
uninstall();
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (command === "status") {
|
|
34
|
+
const { status } = await import("./service.js");
|
|
35
|
+
status();
|
|
36
|
+
process.exit(0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function flag(name) {
|
|
40
|
+
const i = process.argv.indexOf(name);
|
|
41
|
+
return i !== -1 ? process.argv[i + 1] : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
14
44
|
function has(cmd) {
|
|
15
45
|
try {
|
|
16
46
|
execSync(process.platform === "win32" ? `where ${cmd}` : `command -v ${cmd}`, {
|
|
@@ -23,6 +53,20 @@ function has(cmd) {
|
|
|
23
53
|
}
|
|
24
54
|
}
|
|
25
55
|
|
|
56
|
+
// Claude Code is the default agent, so it's required to set up; the other
|
|
57
|
+
// agents (picked in Setup on goosetools.com) are checked lazily per job and
|
|
58
|
+
// reported by `doctor`.
|
|
59
|
+
if (command === "doctor") {
|
|
60
|
+
const { AGENT_IDS, detect } = await import("./agent-cli.js");
|
|
61
|
+
for (const id of AGENT_IDS) {
|
|
62
|
+
const d = detect(id);
|
|
63
|
+
console.log(
|
|
64
|
+
`${d.installed ? "✓" : "✗"} ${d.label}${d.installed ? "" : ` — install: ${d.hint}`}`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
26
70
|
if (!has("claude")) {
|
|
27
71
|
console.error(
|
|
28
72
|
"\nClaude Code isn't installed yet (it writes your slides).\n" +
|
|
@@ -65,4 +109,16 @@ try {
|
|
|
65
109
|
process.exit(1);
|
|
66
110
|
}
|
|
67
111
|
|
|
68
|
-
|
|
112
|
+
if (command === "install") {
|
|
113
|
+
const { install } = await import("./service.js");
|
|
114
|
+
install({
|
|
115
|
+
url: (flag("--url") ?? "https://goosetools.com").replace(/\/$/, ""),
|
|
116
|
+
token: flag("--token"),
|
|
117
|
+
root: ROOT,
|
|
118
|
+
});
|
|
119
|
+
} else if (command === "update") {
|
|
120
|
+
const { update } = await import("./service.js");
|
|
121
|
+
update({ root: ROOT });
|
|
122
|
+
} else {
|
|
123
|
+
await import("./index.js");
|
|
124
|
+
}
|