@flumecode/runner 0.23.1 → 0.25.0-preview.39
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 +19 -7
- package/dist/cli.js +1148 -586
- package/dist/mcp-stdio.js +18 -1
- package/package.json +1 -1
- package/skills-plugin/rules/coding-guideline.md +1 -0
- package/skills-plugin/skills/preview-ui/SKILL.md +67 -108
- package/skills-plugin/skills/request-to-plan/SKILL.md +7 -0
- package/skills-plugin/skills/setup-preview/SKILL.md +166 -0
package/dist/cli.js
CHANGED
|
@@ -62,29 +62,27 @@ async function get(cfg, path) {
|
|
|
62
62
|
if (!res.ok) throw new Error(await safeText2(res));
|
|
63
63
|
return res.json();
|
|
64
64
|
}
|
|
65
|
-
async function post(cfg, path,
|
|
65
|
+
async function post(cfg, path, body2) {
|
|
66
66
|
const res = await fetch(cfg.serverUrl + path, {
|
|
67
67
|
method: "POST",
|
|
68
68
|
headers: { ...authHeaders(cfg), "Content-Type": "application/json" },
|
|
69
|
-
body: JSON.stringify(
|
|
69
|
+
body: JSON.stringify(body2)
|
|
70
70
|
});
|
|
71
71
|
if (!res.ok) throw new Error(await safeText2(res));
|
|
72
72
|
return res.json();
|
|
73
73
|
}
|
|
74
|
-
var listRepositories, listAgents,
|
|
74
|
+
var listRepositories, listAgents, getRequest, createRequest, addRequestComment, answerWidgets, acceptPlan, startImplementing, listJobs, listRunners;
|
|
75
75
|
var init_tui_api = __esm({
|
|
76
76
|
"src/tui-api.ts"() {
|
|
77
77
|
"use strict";
|
|
78
78
|
init_version();
|
|
79
79
|
listRepositories = (cfg) => get(cfg, "/api/cli/repositories").then((r) => r.repositories);
|
|
80
80
|
listAgents = (cfg) => get(cfg, "/api/cli/agents").then((r) => r.agents);
|
|
81
|
-
listRequests = (cfg, repoId) => get(cfg, `/api/cli/repositories/${repoId}/requests`).then(
|
|
82
|
-
(r) => r.requests
|
|
83
|
-
);
|
|
84
81
|
getRequest = (cfg, id) => get(cfg, `/api/cli/requests/${id}`);
|
|
85
82
|
createRequest = (cfg, input) => post(cfg, "/api/cli/requests", input);
|
|
83
|
+
addRequestComment = (cfg, requestId, body2) => post(cfg, `/api/cli/requests/${requestId}/comments`, { body: body2 });
|
|
86
84
|
answerWidgets = (cfg, messageId, payload) => post(cfg, `/api/cli/messages/${messageId}/answer`, payload);
|
|
87
|
-
acceptPlan = (cfg, messageId,
|
|
85
|
+
acceptPlan = (cfg, messageId, mergeBranch) => post(cfg, `/api/cli/plans/${messageId}/accept`, { target: { kind: "new" }, mergeBranch });
|
|
88
86
|
startImplementing = (cfg, sessionId) => post(cfg, `/api/cli/sessions/${sessionId}/implement`, {});
|
|
89
87
|
listJobs = (cfg) => get(cfg, "/api/cli/jobs").then((r) => r.jobs);
|
|
90
88
|
listRunners = (cfg) => get(cfg, "/api/cli/runners").then((r) => r.runners);
|
|
@@ -121,17 +119,18 @@ var init_usePoll = __esm({
|
|
|
121
119
|
});
|
|
122
120
|
|
|
123
121
|
// src/tui/StatusBar.tsx
|
|
124
|
-
import {
|
|
125
|
-
import {
|
|
122
|
+
import { Text } from "ink";
|
|
123
|
+
import { jsxs } from "react/jsx-runtime";
|
|
126
124
|
function StatusBar({ jobs, runners }) {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
return /* @__PURE__ */
|
|
130
|
-
"
|
|
131
|
-
|
|
132
|
-
"
|
|
133
|
-
|
|
134
|
-
|
|
125
|
+
const running = jobs?.filter((j) => j.status === "running").length ?? 0;
|
|
126
|
+
const online = runners?.filter((r) => r.status === "online").length ?? 0;
|
|
127
|
+
return /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
128
|
+
"jobs ",
|
|
129
|
+
running,
|
|
130
|
+
" running \xB7 runners ",
|
|
131
|
+
online,
|
|
132
|
+
" online"
|
|
133
|
+
] });
|
|
135
134
|
}
|
|
136
135
|
var init_StatusBar = __esm({
|
|
137
136
|
"src/tui/StatusBar.tsx"() {
|
|
@@ -141,8 +140,8 @@ var init_StatusBar = __esm({
|
|
|
141
140
|
|
|
142
141
|
// src/tui/RepoListScreen.tsx
|
|
143
142
|
import { useState as useState2 } from "react";
|
|
144
|
-
import { Box
|
|
145
|
-
import { jsx
|
|
143
|
+
import { Box, Text as Text2, useInput } from "ink";
|
|
144
|
+
import { jsx, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
146
145
|
function RepoListScreen({ config, onSelect }) {
|
|
147
146
|
const repos = usePoll(() => listRepositories(config), 1e4);
|
|
148
147
|
const [cursor, setCursor] = useState2(0);
|
|
@@ -155,10 +154,10 @@ function RepoListScreen({ config, onSelect }) {
|
|
|
155
154
|
if (repo) onSelect(repo);
|
|
156
155
|
}
|
|
157
156
|
});
|
|
158
|
-
if (!repos) return /* @__PURE__ */
|
|
159
|
-
if (repos.length === 0) return /* @__PURE__ */
|
|
160
|
-
return /* @__PURE__ */ jsxs2(
|
|
161
|
-
/* @__PURE__ */
|
|
157
|
+
if (!repos) return /* @__PURE__ */ jsx(Text2, { children: "Loading repositories\u2026" });
|
|
158
|
+
if (repos.length === 0) return /* @__PURE__ */ jsx(Text2, { children: "No repositories found." });
|
|
159
|
+
return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
|
|
160
|
+
/* @__PURE__ */ jsx(Text2, { bold: true, children: "Select a repository (\u2191\u2193 Enter):" }),
|
|
162
161
|
repos.map((repo, i) => /* @__PURE__ */ jsxs2(Text2, { color: i === cursor ? "cyan" : void 0, children: [
|
|
163
162
|
i === cursor ? "\u25B6 " : " ",
|
|
164
163
|
repo.fullName
|
|
@@ -173,188 +172,241 @@ var init_RepoListScreen = __esm({
|
|
|
173
172
|
}
|
|
174
173
|
});
|
|
175
174
|
|
|
176
|
-
// src/tui/
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (key.escape) {
|
|
185
|
-
onBack();
|
|
186
|
-
return;
|
|
175
|
+
// src/tui/markdown.ts
|
|
176
|
+
function parseSpans(text) {
|
|
177
|
+
const spans = [];
|
|
178
|
+
let plain = "";
|
|
179
|
+
const flush = () => {
|
|
180
|
+
if (plain) {
|
|
181
|
+
spans.push({ text: plain });
|
|
182
|
+
plain = "";
|
|
187
183
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
184
|
+
};
|
|
185
|
+
let i = 0;
|
|
186
|
+
while (i < text.length) {
|
|
187
|
+
const ch = text[i];
|
|
188
|
+
if (ch === "`") {
|
|
189
|
+
const end = text.indexOf("`", i + 1);
|
|
190
|
+
if (end > i) {
|
|
191
|
+
flush();
|
|
192
|
+
spans.push({ text: text.slice(i + 1, end), code: true });
|
|
193
|
+
i = end + 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
196
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
"
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
init_usePoll();
|
|
218
|
-
init_tui_api();
|
|
197
|
+
if (ch === "*" && text[i + 1] === "*") {
|
|
198
|
+
const end = text.indexOf("**", i + 2);
|
|
199
|
+
if (end > i + 1) {
|
|
200
|
+
flush();
|
|
201
|
+
spans.push({ text: text.slice(i + 2, end), bold: true });
|
|
202
|
+
i = end + 2;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (ch === "*" && text[i + 1] !== void 0 && text[i + 1] !== " " && text[i + 1] !== "*") {
|
|
207
|
+
const end = text.indexOf("*", i + 1);
|
|
208
|
+
if (end > i + 1) {
|
|
209
|
+
flush();
|
|
210
|
+
spans.push({ text: text.slice(i + 1, end), italic: true });
|
|
211
|
+
i = end + 1;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
plain += ch;
|
|
216
|
+
i++;
|
|
219
217
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const [agentCursor, setAgentCursor] = useState4(0);
|
|
233
|
-
const [branch, setBranch] = useState4(repo.defaultBranch ?? "");
|
|
234
|
-
const [error, setError] = useState4(null);
|
|
235
|
-
const [submitting, setSubmitting] = useState4(false);
|
|
236
|
-
useInput3((_input, key) => {
|
|
237
|
-
if (key.escape) {
|
|
238
|
-
onBack();
|
|
239
|
-
return;
|
|
218
|
+
flush();
|
|
219
|
+
return spans;
|
|
220
|
+
}
|
|
221
|
+
function parseBlocks(src) {
|
|
222
|
+
const blocks = [];
|
|
223
|
+
const lines2 = src.split("\n");
|
|
224
|
+
let para = [];
|
|
225
|
+
let code = null;
|
|
226
|
+
const flushPara = () => {
|
|
227
|
+
if (para.length) {
|
|
228
|
+
blocks.push({ t: "p", spans: parseSpans(para.join(" ")) });
|
|
229
|
+
para = [];
|
|
240
230
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
231
|
+
};
|
|
232
|
+
for (const raw of lines2) {
|
|
233
|
+
if (code) {
|
|
234
|
+
if (raw.trimEnd() === "```") {
|
|
235
|
+
blocks.push({ t: "code", ...code.lang ? { lang: code.lang } : {}, lines: code.lines });
|
|
236
|
+
code = null;
|
|
237
|
+
} else {
|
|
238
|
+
code.lines.push(raw);
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
244
241
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
242
|
+
const line = raw.trimEnd();
|
|
243
|
+
const trimmed = line.trim();
|
|
244
|
+
if (trimmed.startsWith("```")) {
|
|
245
|
+
flushPara();
|
|
246
|
+
const lang = trimmed.slice(3).trim();
|
|
247
|
+
code = { ...lang ? { lang } : {}, lines: [] };
|
|
248
|
+
continue;
|
|
248
249
|
}
|
|
249
|
-
if (
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (key.tab) setField("branch");
|
|
250
|
+
if (trimmed === "") {
|
|
251
|
+
flushPara();
|
|
252
|
+
continue;
|
|
253
253
|
}
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
const h = /^(#{1,3})\s+(.*)$/.exec(trimmed);
|
|
255
|
+
if (h && h[1] && h[2] !== void 0) {
|
|
256
|
+
flushPara();
|
|
257
|
+
blocks.push({ t: "h", level: h[1].length, spans: parseSpans(h[2]) });
|
|
258
|
+
continue;
|
|
256
259
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
260
|
+
if (/^(-{3,}|\*{3,})$/.test(trimmed)) {
|
|
261
|
+
flushPara();
|
|
262
|
+
blocks.push({ t: "rule" });
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
const indent = line.length - line.trimStart().length;
|
|
266
|
+
const li = /^[-*]\s+(.*)$/.exec(trimmed);
|
|
267
|
+
if (li && li[1] !== void 0) {
|
|
268
|
+
flushPara();
|
|
269
|
+
blocks.push({ t: "li", depth: indent >= 2 ? 1 : 0, spans: parseSpans(li[1]) });
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const ol = /^(\d+)[.)]\s+(.*)$/.exec(trimmed);
|
|
273
|
+
if (ol && ol[1] && ol[2] !== void 0) {
|
|
274
|
+
flushPara();
|
|
275
|
+
blocks.push({
|
|
276
|
+
t: "li",
|
|
277
|
+
depth: indent >= 2 ? 1 : 0,
|
|
278
|
+
ordered: Number(ol[1]),
|
|
279
|
+
spans: parseSpans(ol[2])
|
|
269
280
|
});
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (trimmed.startsWith("> ") || trimmed === ">") {
|
|
284
|
+
flushPara();
|
|
285
|
+
blocks.push({ t: "quote", spans: parseSpans(trimmed.replace(/^>\s?/, "")) });
|
|
286
|
+
continue;
|
|
274
287
|
}
|
|
288
|
+
para.push(trimmed);
|
|
275
289
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
] }
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
290
|
+
if (code)
|
|
291
|
+
blocks.push({ t: "code", ...code.lang ? { lang: code.lang } : {}, lines: code.lines });
|
|
292
|
+
flushPara();
|
|
293
|
+
return blocks;
|
|
294
|
+
}
|
|
295
|
+
function parseMarkdown(src) {
|
|
296
|
+
try {
|
|
297
|
+
return parseBlocks(src);
|
|
298
|
+
} catch {
|
|
299
|
+
return [{ t: "p", spans: [{ text: src }] }];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
var init_markdown = __esm({
|
|
303
|
+
"src/tui/markdown.ts"() {
|
|
304
|
+
"use strict";
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// src/tui/Markdown.tsx
|
|
309
|
+
import { Box as Box2, Text as Text3 } from "ink";
|
|
310
|
+
import { jsx as jsx2, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
311
|
+
function Markdown({ source, width }) {
|
|
312
|
+
const blocks = parseMarkdown(source);
|
|
313
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: blocks.map((block, i) => renderBlock(block, i, blocks, width)) });
|
|
314
|
+
}
|
|
315
|
+
function spanNodes(spans) {
|
|
316
|
+
return spans.map((s, k) => /* @__PURE__ */ jsx2(Text3, { bold: s.bold, italic: s.italic, color: s.code ? "magenta" : void 0, children: s.text }, k));
|
|
317
|
+
}
|
|
318
|
+
function renderBlock(block, i, blocks, width) {
|
|
319
|
+
switch (block.t) {
|
|
320
|
+
case "h":
|
|
321
|
+
return /* @__PURE__ */ jsx2(Box2, { marginTop: i === 0 ? 0 : 1, children: /* @__PURE__ */ jsx2(Text3, { bold: true, underline: block.level !== 3, children: spanNodes(block.spans) }) }, i);
|
|
322
|
+
case "p":
|
|
323
|
+
return /* @__PURE__ */ jsx2(Box2, { marginBottom: 1, children: /* @__PURE__ */ jsx2(Text3, { children: spanNodes(block.spans) }) }, i);
|
|
324
|
+
case "li": {
|
|
325
|
+
const marker = block.ordered ? `${block.ordered}.` : block.depth === 1 ? "\u25E6" : "\u2022";
|
|
326
|
+
const isLastOfRun = blocks[i + 1]?.t !== "li";
|
|
327
|
+
return /* @__PURE__ */ jsxs3(Box2, { marginBottom: isLastOfRun ? 1 : 0, children: [
|
|
328
|
+
/* @__PURE__ */ jsx2(Box2, { width: block.depth === 1 ? 4 : 2, flexShrink: 0, children: /* @__PURE__ */ jsx2(Text3, { dimColor: true, children: marker }) }),
|
|
329
|
+
/* @__PURE__ */ jsx2(Box2, { flexGrow: 1, children: /* @__PURE__ */ jsx2(Text3, { children: spanNodes(block.spans) }) })
|
|
330
|
+
] }, i);
|
|
331
|
+
}
|
|
332
|
+
case "code":
|
|
333
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", paddingLeft: 2, marginBottom: 1, children: block.lines.map((line, k) => (
|
|
334
|
+
// An empty string renders nothing in ink, so substitute a single space.
|
|
335
|
+
/* @__PURE__ */ jsx2(Text3, { dimColor: true, children: line || " " }, k)
|
|
336
|
+
)) }, i);
|
|
337
|
+
case "quote":
|
|
338
|
+
return /* @__PURE__ */ jsxs3(Box2, { marginBottom: 1, children: [
|
|
339
|
+
/* @__PURE__ */ jsx2(Box2, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx2(Text3, { dimColor: true, children: "\u258C" }) }),
|
|
340
|
+
/* @__PURE__ */ jsx2(Box2, { flexGrow: 1, children: /* @__PURE__ */ jsx2(Text3, { dimColor: true, children: spanNodes(block.spans) }) })
|
|
341
|
+
] }, i);
|
|
342
|
+
case "rule":
|
|
343
|
+
return /* @__PURE__ */ jsx2(Box2, { marginBottom: 1, children: /* @__PURE__ */ jsx2(Text3, { dimColor: true, children: "\u2500".repeat(Math.max(8, Math.min(width - 2, 60))) }) }, i);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
var init_Markdown = __esm({
|
|
347
|
+
"src/tui/Markdown.tsx"() {
|
|
348
|
+
"use strict";
|
|
349
|
+
init_markdown();
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// src/tui/Spinner.tsx
|
|
354
|
+
import { useEffect as useEffect2, useState as useState3 } from "react";
|
|
355
|
+
import { Text as Text4 } from "ink";
|
|
356
|
+
import { jsxs as jsxs4 } from "react/jsx-runtime";
|
|
357
|
+
function Spinner({ label }) {
|
|
358
|
+
const [frame, setFrame] = useState3(0);
|
|
359
|
+
useEffect2(() => {
|
|
360
|
+
const id = setInterval(() => setFrame((f) => (f + 1) % FRAMES.length), 100);
|
|
361
|
+
return () => clearInterval(id);
|
|
362
|
+
}, []);
|
|
363
|
+
return /* @__PURE__ */ jsxs4(Text4, { color: "yellow", children: [
|
|
364
|
+
FRAMES[frame],
|
|
365
|
+
" ",
|
|
366
|
+
label
|
|
310
367
|
] });
|
|
311
368
|
}
|
|
312
|
-
var
|
|
313
|
-
|
|
369
|
+
var FRAMES;
|
|
370
|
+
var init_Spinner = __esm({
|
|
371
|
+
"src/tui/Spinner.tsx"() {
|
|
314
372
|
"use strict";
|
|
315
|
-
|
|
316
|
-
init_tui_api();
|
|
373
|
+
FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
317
374
|
}
|
|
318
375
|
});
|
|
319
376
|
|
|
320
377
|
// src/tui/WidgetAnswerScreen.tsx
|
|
321
|
-
import { useState as
|
|
322
|
-
import { Box as
|
|
323
|
-
import
|
|
324
|
-
import { jsx as
|
|
325
|
-
function
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
334
|
-
const
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const [error, setError] = useState5(null);
|
|
339
|
-
const [submitting, setSubmitting] = useState5(false);
|
|
340
|
-
const widget = message.widgets[widgetIndex];
|
|
341
|
-
function handleSingleInput(input, key, w) {
|
|
342
|
-
const opts2 = [...w.options, { id: "__other__", label: "Other" }];
|
|
378
|
+
import { useRef as useRef2, useState as useState4 } from "react";
|
|
379
|
+
import { Box as Box3, Text as Text5, useInput as useInput2 } from "ink";
|
|
380
|
+
import TextInput from "ink-text-input";
|
|
381
|
+
import { jsx as jsx3, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
382
|
+
function WidgetAnswerScreen({ config, message: message2, onDone }) {
|
|
383
|
+
const [widgetIndex, setWidgetIndex] = useState4(0);
|
|
384
|
+
const [cursor, setCursor] = useState4(0);
|
|
385
|
+
const [checked, setChecked] = useState4(/* @__PURE__ */ new Set());
|
|
386
|
+
const [customText, setCustomText] = useState4("");
|
|
387
|
+
const [showCustom, setShowCustom] = useState4(false);
|
|
388
|
+
const [error, setError] = useState4(null);
|
|
389
|
+
const [submitting, setSubmitting] = useState4(false);
|
|
390
|
+
const answersRef = useRef2([]);
|
|
391
|
+
const summaryRef = useRef2([]);
|
|
392
|
+
const widget = message2.widgets[widgetIndex];
|
|
393
|
+
function handleSingleInput(key, w) {
|
|
394
|
+
const opts2 = [...w.options, OTHER];
|
|
343
395
|
if (key.upArrow) {
|
|
396
|
+
setShowCustom(false);
|
|
344
397
|
setCursor((c) => Math.max(0, c - 1));
|
|
345
398
|
} else if (key.downArrow) {
|
|
399
|
+
setShowCustom(false);
|
|
346
400
|
setCursor((c) => Math.min(opts2.length - 1, c + 1));
|
|
347
401
|
} else if (key.return) {
|
|
402
|
+
if (showCustom) return;
|
|
348
403
|
const picked = opts2[cursor];
|
|
349
|
-
if (picked?.id ===
|
|
350
|
-
|
|
351
|
-
} else {
|
|
352
|
-
void advanceOrSubmit(w, picked?.id, void 0, void 0);
|
|
353
|
-
}
|
|
404
|
+
if (picked?.id === OTHER.id) setShowCustom(true);
|
|
405
|
+
else if (picked) void advanceOrSubmit(w, picked.id, void 0, void 0);
|
|
354
406
|
}
|
|
355
407
|
}
|
|
356
408
|
function handleMultiInput(input, key, w) {
|
|
357
|
-
const opts2 = [...w.options,
|
|
409
|
+
const opts2 = [...w.options, OTHER];
|
|
358
410
|
if (key.upArrow) {
|
|
359
411
|
setCursor((c) => Math.max(0, c - 1));
|
|
360
412
|
} else if (key.downArrow) {
|
|
@@ -362,7 +414,7 @@ function WidgetAnswerScreen({ config, message, onDone }) {
|
|
|
362
414
|
} else if (input === " ") {
|
|
363
415
|
const picked = opts2[cursor];
|
|
364
416
|
if (!picked) return;
|
|
365
|
-
if (picked.id ===
|
|
417
|
+
if (picked.id === OTHER.id) {
|
|
366
418
|
setShowCustom((s) => !s);
|
|
367
419
|
} else {
|
|
368
420
|
const next = new Set(checked);
|
|
@@ -374,24 +426,26 @@ function WidgetAnswerScreen({ config, message, onDone }) {
|
|
|
374
426
|
void advanceOrSubmit(w, void 0, [...checked], customText || void 0);
|
|
375
427
|
}
|
|
376
428
|
}
|
|
377
|
-
|
|
378
|
-
if (!widget) return;
|
|
379
|
-
if (widget.type === "single_select")
|
|
380
|
-
|
|
381
|
-
} else {
|
|
382
|
-
handleMultiInput(input, key, widget);
|
|
383
|
-
}
|
|
429
|
+
useInput2((input, key) => {
|
|
430
|
+
if (!widget || submitting) return;
|
|
431
|
+
if (widget.type === "single_select") handleSingleInput(key, widget);
|
|
432
|
+
else handleMultiInput(input, key, widget);
|
|
384
433
|
});
|
|
434
|
+
const onCustomSubmit = (value) => {
|
|
435
|
+
if (!widget || widget.type !== "single_select" || submitting) return;
|
|
436
|
+
const text = value.trim();
|
|
437
|
+
if (!text) return;
|
|
438
|
+
void advanceOrSubmit(widget, void 0, void 0, text);
|
|
439
|
+
};
|
|
385
440
|
async function advanceOrSubmit(w, optionId, optionIds, custom) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (widgetIndex < message.widgets.length - 1) {
|
|
441
|
+
answersRef.current.push({
|
|
442
|
+
widgetId: w.id,
|
|
443
|
+
...optionId !== void 0 ? { optionId } : {},
|
|
444
|
+
...optionIds !== void 0 ? { optionIds } : {},
|
|
445
|
+
...custom ? { customText: custom } : {}
|
|
446
|
+
});
|
|
447
|
+
summaryRef.current.push(`${w.question} \u2192 ${answerLabel(w, optionId, optionIds, custom)}`);
|
|
448
|
+
if (widgetIndex < message2.widgets.length - 1) {
|
|
395
449
|
setWidgetIndex((i) => i + 1);
|
|
396
450
|
setCursor(0);
|
|
397
451
|
setChecked(/* @__PURE__ */ new Set());
|
|
@@ -402,177 +456,396 @@ function WidgetAnswerScreen({ config, message, onDone }) {
|
|
|
402
456
|
setSubmitting(true);
|
|
403
457
|
setError(null);
|
|
404
458
|
try {
|
|
405
|
-
await answerWidgets(config,
|
|
406
|
-
|
|
459
|
+
await answerWidgets(config, message2.id, {
|
|
460
|
+
requestId: message2.requestId,
|
|
461
|
+
answers: answersRef.current
|
|
462
|
+
});
|
|
463
|
+
onDone(summaryRef.current);
|
|
407
464
|
} catch (err) {
|
|
465
|
+
answersRef.current.pop();
|
|
466
|
+
summaryRef.current.pop();
|
|
408
467
|
setError(err instanceof Error ? err.message : "Unknown error");
|
|
409
468
|
setSubmitting(false);
|
|
410
469
|
}
|
|
411
470
|
}
|
|
412
|
-
if (!widget) return /* @__PURE__ */
|
|
471
|
+
if (!widget) return /* @__PURE__ */ jsx3(Text5, { children: "No widgets to answer." });
|
|
413
472
|
const isSingle = widget.type === "single_select";
|
|
414
|
-
const opts = [...widget.options,
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
473
|
+
const opts = [...widget.options, OTHER];
|
|
474
|
+
const total = opts.length;
|
|
475
|
+
const start2 = total <= WINDOW ? 0 : Math.min(Math.max(0, cursor - Math.floor(WINDOW / 2)), total - WINDOW);
|
|
476
|
+
const end = Math.min(total, start2 + WINDOW);
|
|
477
|
+
const moreAbove = start2;
|
|
478
|
+
const moreBelow = total - end;
|
|
479
|
+
return /* @__PURE__ */ jsxs5(Box3, { flexDirection: "column", children: [
|
|
480
|
+
/* @__PURE__ */ jsxs5(Box3, { children: [
|
|
481
|
+
/* @__PURE__ */ jsx3(Box3, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx3(Text5, { bold: true, children: "\u25CF" }) }),
|
|
482
|
+
/* @__PURE__ */ jsx3(Box3, { flexGrow: 1, children: /* @__PURE__ */ jsxs5(Text5, { children: [
|
|
483
|
+
/* @__PURE__ */ jsx3(Text5, { bold: true, children: widget.question }),
|
|
484
|
+
/* @__PURE__ */ jsx3(Text5, { dimColor: true, children: ` (${widgetIndex + 1}/${message2.widgets.length})` })
|
|
485
|
+
] }) })
|
|
486
|
+
] }),
|
|
487
|
+
widget.body ? /* @__PURE__ */ jsx3(Box3, { marginTop: 1, marginLeft: 2, children: /* @__PURE__ */ jsx3(Markdown, { source: widget.body, width: 76 }) }) : null,
|
|
488
|
+
/* @__PURE__ */ jsxs5(Box3, { flexDirection: "column", marginTop: 1, children: [
|
|
489
|
+
moreAbove > 0 && /* @__PURE__ */ jsx3(Text5, { dimColor: true, children: ` \u2026 ${moreAbove} more above` }),
|
|
490
|
+
opts.slice(start2, end).map((opt, vi) => {
|
|
491
|
+
const i = start2 + vi;
|
|
492
|
+
const onCursor = i === cursor;
|
|
493
|
+
return /* @__PURE__ */ jsxs5(Box3, { children: [
|
|
494
|
+
/* @__PURE__ */ jsx3(Box3, { width: 2, flexShrink: 0, children: onCursor ? /* @__PURE__ */ jsx3(Text5, { color: "cyan", bold: true, children: "\u276F " }) : /* @__PURE__ */ jsx3(Text5, { children: " " }) }),
|
|
495
|
+
/* @__PURE__ */ jsx3(Box3, { flexGrow: 1, children: /* @__PURE__ */ jsxs5(Text5, { color: onCursor ? "cyan" : void 0, children: [
|
|
496
|
+
!isSingle && (checked.has(opt.id) ? /* @__PURE__ */ jsx3(Text5, { color: "green", children: "\u25C9 " }) : /* @__PURE__ */ jsx3(Text5, { dimColor: true, children: "\u25EF " })),
|
|
497
|
+
opt.label
|
|
498
|
+
] }) })
|
|
499
|
+
] }, opt.id);
|
|
500
|
+
}),
|
|
501
|
+
moreBelow > 0 && /* @__PURE__ */ jsx3(Text5, { dimColor: true, children: ` \u2026 ${moreBelow} more below` })
|
|
423
502
|
] }),
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
503
|
+
showCustom && /* @__PURE__ */ jsxs5(Box3, { marginTop: 1, children: [
|
|
504
|
+
/* @__PURE__ */ jsx3(Box3, { width: 2, flexShrink: 0, children: /* @__PURE__ */ jsx3(Text5, { color: "cyan", bold: true, children: "\u203A" }) }),
|
|
505
|
+
/* @__PURE__ */ jsx3(
|
|
506
|
+
TextInput,
|
|
507
|
+
{
|
|
508
|
+
value: customText,
|
|
509
|
+
onChange: setCustomText,
|
|
510
|
+
focus: showCustom,
|
|
511
|
+
onSubmit: isSingle ? onCustomSubmit : void 0
|
|
512
|
+
}
|
|
513
|
+
)
|
|
430
514
|
] }),
|
|
431
|
-
|
|
515
|
+
/* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(Text5, { dimColor: true, children: isSingle ? "\u2191\u2193 move \xB7 enter select" : "\u2191\u2193 move \xB7 space toggle \xB7 enter confirm" }) }),
|
|
516
|
+
submitting && /* @__PURE__ */ jsx3(Spinner, { label: "submitting\u2026" }),
|
|
432
517
|
error && /* @__PURE__ */ jsxs5(Text5, { color: "red", children: [
|
|
433
518
|
"Error: ",
|
|
434
519
|
error
|
|
435
520
|
] })
|
|
436
521
|
] });
|
|
437
522
|
}
|
|
523
|
+
function answerLabel(w, optionId, optionIds, custom) {
|
|
524
|
+
const pickedIds = [...optionId ? [optionId] : [], ...optionIds ?? []];
|
|
525
|
+
const labels = pickedIds.map((id) => w.options.find((o) => o.id === id)?.label).filter((label) => label !== void 0);
|
|
526
|
+
if (custom) labels.push(custom);
|
|
527
|
+
return labels.length ? labels.join(", ") : "(no selection)";
|
|
528
|
+
}
|
|
529
|
+
var OTHER, WINDOW;
|
|
438
530
|
var init_WidgetAnswerScreen = __esm({
|
|
439
531
|
"src/tui/WidgetAnswerScreen.tsx"() {
|
|
440
532
|
"use strict";
|
|
441
533
|
init_tui_api();
|
|
534
|
+
init_Markdown();
|
|
535
|
+
init_Spinner();
|
|
536
|
+
OTHER = { id: "__other__", label: "Other" };
|
|
537
|
+
WINDOW = 10;
|
|
442
538
|
}
|
|
443
539
|
});
|
|
444
540
|
|
|
445
|
-
// src/tui/
|
|
446
|
-
import {
|
|
447
|
-
import {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
541
|
+
// src/tui/Message.tsx
|
|
542
|
+
import { Box as Box4, Text as Text6 } from "ink";
|
|
543
|
+
import { Fragment, jsx as jsx4, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
544
|
+
function Message({ msg, width }) {
|
|
545
|
+
return /* @__PURE__ */ jsxs6(Box4, { marginBottom: 1, flexDirection: "row", children: [
|
|
546
|
+
/* @__PURE__ */ jsx4(Box4, { width: 2, flexShrink: 0, children: glyph(msg) }),
|
|
547
|
+
/* @__PURE__ */ jsx4(Box4, { flexDirection: "column", flexGrow: 1, children: body(msg, width) })
|
|
548
|
+
] });
|
|
549
|
+
}
|
|
550
|
+
function glyph(msg) {
|
|
551
|
+
if (msg.role === "user") {
|
|
552
|
+
return /* @__PURE__ */ jsx4(Text6, { color: "cyan", bold: true, children: "\u203A" });
|
|
553
|
+
}
|
|
554
|
+
if (msg.kind === "success") return /* @__PURE__ */ jsx4(Text6, { color: "green", children: "\u2713" });
|
|
555
|
+
if (msg.kind === "error") return /* @__PURE__ */ jsx4(Text6, { color: "red", children: "\u2717" });
|
|
556
|
+
if (msg.kind === "info" || msg.role === "system") return /* @__PURE__ */ jsx4(Text6, { dimColor: true, children: "\xB7" });
|
|
557
|
+
return /* @__PURE__ */ jsx4(Text6, { bold: true, children: "\u25CF" });
|
|
558
|
+
}
|
|
559
|
+
function body(msg, width) {
|
|
560
|
+
if (msg.role === "user") return /* @__PURE__ */ jsx4(Text6, { color: "cyan", children: msg.text });
|
|
561
|
+
if (msg.kind === "markdown") return /* @__PURE__ */ jsx4(Markdown, { source: msg.text, width: width - 2 });
|
|
562
|
+
if (msg.kind === "plan") {
|
|
563
|
+
return /* @__PURE__ */ jsxs6(Fragment, { children: [
|
|
564
|
+
/* @__PURE__ */ jsx4(Text6, { bold: true, children: "Plan" }),
|
|
565
|
+
/* @__PURE__ */ jsx4(Text6, { dimColor: true, children: "\u2500".repeat(Math.max(8, Math.min(width - 4, 50))) }),
|
|
566
|
+
/* @__PURE__ */ jsx4(Markdown, { source: msg.text, width: width - 2 })
|
|
567
|
+
] });
|
|
568
|
+
}
|
|
569
|
+
if (msg.kind === "info" || msg.kind === "success" || msg.kind === "error") {
|
|
570
|
+
return /* @__PURE__ */ jsx4(Text6, { dimColor: true, children: msg.text });
|
|
571
|
+
}
|
|
572
|
+
return /* @__PURE__ */ jsx4(Text6, { children: msg.text });
|
|
573
|
+
}
|
|
574
|
+
var init_Message = __esm({
|
|
575
|
+
"src/tui/Message.tsx"() {
|
|
576
|
+
"use strict";
|
|
577
|
+
init_Markdown();
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// src/tui/ConversationScreen.tsx
|
|
582
|
+
import { useCallback, useEffect as useEffect3, useRef as useRef3, useState as useState5 } from "react";
|
|
583
|
+
import { Box as Box5, Static, Text as Text7, useInput as useInput3, useStdout } from "ink";
|
|
584
|
+
import TextInput2 from "ink-text-input";
|
|
585
|
+
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
586
|
+
function deriveTitle(text) {
|
|
587
|
+
const firstLine = (text.split("\n")[0] ?? "").trim();
|
|
588
|
+
const t = firstLine.length > 0 ? firstLine : text.trim();
|
|
589
|
+
return t.length > 72 ? `${t.slice(0, 69).trimEnd()}\u2026` : t;
|
|
590
|
+
}
|
|
591
|
+
function ConversationScreen({ config, repo, onBack }) {
|
|
592
|
+
const idRef = useRef3(0);
|
|
593
|
+
const [msgs, setMsgs] = useState5([
|
|
594
|
+
{
|
|
595
|
+
id: 0,
|
|
596
|
+
role: "system",
|
|
597
|
+
kind: "info",
|
|
598
|
+
text: `${repo.fullName} \u2014 describe what you'd like to build. Your message becomes a request; the FlumeCode agent (your runner) takes it from there.`
|
|
467
599
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
600
|
+
]);
|
|
601
|
+
const [input, setInput] = useState5("");
|
|
602
|
+
const historyRef = useRef3([]);
|
|
603
|
+
const [histIndex, setHistIndex] = useState5(null);
|
|
604
|
+
const [phase, setPhase] = useState5("compose");
|
|
605
|
+
const [error, setError] = useState5(null);
|
|
606
|
+
const [agentPending, setAgentPending] = useState5(false);
|
|
607
|
+
const [requestId, setRequestId] = useState5(null);
|
|
608
|
+
const planMessageId = useRef3(null);
|
|
609
|
+
const shownIds = useRef3(/* @__PURE__ */ new Set());
|
|
610
|
+
const [widgetMsg, setWidgetMsg] = useState5(null);
|
|
611
|
+
const { stdout: stdout2 } = useStdout();
|
|
612
|
+
const width = Math.min(stdout2?.columns ?? 80, 100);
|
|
613
|
+
const runners = usePoll(() => listRunners(config), 15e3);
|
|
614
|
+
const noRunnerOnline = runners !== null && !runners.some((r) => r.status === "online");
|
|
615
|
+
const push = useCallback((role, text, kind = "text") => {
|
|
616
|
+
setMsgs((prev) => [...prev, { id: ++idRef.current, role, kind, text }]);
|
|
617
|
+
}, []);
|
|
618
|
+
const createFromIdea = useCallback(
|
|
619
|
+
async (idea) => {
|
|
620
|
+
setPhase("creating");
|
|
621
|
+
try {
|
|
622
|
+
const agents = await listAgents(config);
|
|
623
|
+
const agentId = agents[0]?.id;
|
|
624
|
+
if (!agentId) throw new Error("No agent is configured for this repository.");
|
|
625
|
+
const { requestId: id } = await createRequest(config, {
|
|
626
|
+
title: deriveTitle(idea),
|
|
627
|
+
body: idea,
|
|
628
|
+
agentId,
|
|
629
|
+
repoId: repo.id,
|
|
630
|
+
// Fixed to the repo's default branch (same as the merge target below).
|
|
631
|
+
checkoutBranch: repo.defaultBranch ?? "main"
|
|
632
|
+
});
|
|
633
|
+
setRequestId(id);
|
|
634
|
+
setError(null);
|
|
635
|
+
push("system", `Request created \u2014 ${config.serverUrl}/requests/${id}`, "success");
|
|
636
|
+
push(
|
|
637
|
+
"system",
|
|
638
|
+
"The agent (your runner) is working on it. Needs a connected runner; Esc to leave.",
|
|
639
|
+
"info"
|
|
640
|
+
);
|
|
641
|
+
setPhase("thread");
|
|
642
|
+
} catch (err) {
|
|
643
|
+
setError(
|
|
644
|
+
`Could not create the request: ${err instanceof Error ? err.message : String(err)}`
|
|
645
|
+
);
|
|
646
|
+
setPhase("compose");
|
|
647
|
+
}
|
|
648
|
+
},
|
|
649
|
+
[config, push, repo]
|
|
650
|
+
);
|
|
651
|
+
const postFollowUp = useCallback(
|
|
652
|
+
async (text) => {
|
|
653
|
+
if (!requestId) return;
|
|
654
|
+
try {
|
|
655
|
+
await addRequestComment(config, requestId, text);
|
|
656
|
+
setError(null);
|
|
657
|
+
} catch (err) {
|
|
658
|
+
setError(`Could not send: ${err instanceof Error ? err.message : String(err)}`);
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
[config, requestId]
|
|
662
|
+
);
|
|
663
|
+
const doImplement = useCallback(async () => {
|
|
664
|
+
const msgId = planMessageId.current;
|
|
665
|
+
if (!msgId) return;
|
|
666
|
+
setPhase("working");
|
|
667
|
+
try {
|
|
668
|
+
const mergeBranch = repo.defaultBranch ?? "main";
|
|
669
|
+
const { session } = await acceptPlan(config, msgId, mergeBranch);
|
|
670
|
+
push(
|
|
671
|
+
"system",
|
|
672
|
+
`Coding session created \u2014 ${config.serverUrl}/repositories/${repo.id}/sessions/${session.id}`,
|
|
673
|
+
"success"
|
|
674
|
+
);
|
|
675
|
+
await startImplementing(config, session.id);
|
|
676
|
+
push(
|
|
677
|
+
"system",
|
|
678
|
+
"Implementation started \u2014 the runner will build it and open a PR. Track it in the status bar or the web.",
|
|
679
|
+
"success"
|
|
680
|
+
);
|
|
681
|
+
} catch (err) {
|
|
682
|
+
push(
|
|
683
|
+
"system",
|
|
684
|
+
`Could not start implementing: ${err instanceof Error ? err.message : String(err)}`,
|
|
685
|
+
"error"
|
|
686
|
+
);
|
|
471
687
|
}
|
|
472
|
-
|
|
473
|
-
|
|
688
|
+
setPhase("thread");
|
|
689
|
+
}, [config, push, repo]);
|
|
690
|
+
const onWidgetDone = useCallback(
|
|
691
|
+
(summary) => {
|
|
692
|
+
setWidgetMsg(null);
|
|
693
|
+
for (const line of summary) push("system", line, "info");
|
|
694
|
+
push("system", "the agent is continuing\u2026", "info");
|
|
695
|
+
setPhase("thread");
|
|
696
|
+
},
|
|
697
|
+
[push]
|
|
698
|
+
);
|
|
699
|
+
useEffect3(() => {
|
|
700
|
+
if (phase !== "thread" || !requestId) return;
|
|
701
|
+
let live = true;
|
|
702
|
+
const poll = async () => {
|
|
703
|
+
try {
|
|
704
|
+
const data = await getRequest(config, requestId);
|
|
705
|
+
if (!live) return;
|
|
706
|
+
let pending = false;
|
|
707
|
+
for (const m of data.messages) {
|
|
708
|
+
if (shownIds.current.has(m.id)) continue;
|
|
709
|
+
if (m.authorType && m.authorType !== "agent") {
|
|
710
|
+
shownIds.current.add(m.id);
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
if (m.type === "plan" && m.body?.trim()) {
|
|
714
|
+
shownIds.current.add(m.id);
|
|
715
|
+
planMessageId.current = m.id;
|
|
716
|
+
push("assistant", m.body.trim(), "plan");
|
|
717
|
+
setAgentPending(false);
|
|
718
|
+
setPhase("confirm-implement");
|
|
719
|
+
live = false;
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const ws = m.widgets;
|
|
723
|
+
if (ws && ws.length) {
|
|
724
|
+
shownIds.current.add(m.id);
|
|
725
|
+
if (m.body?.trim()) push("assistant", m.body.trim(), "markdown");
|
|
726
|
+
push("system", "The agent is asking \u2014 pick an option below.", "info");
|
|
727
|
+
setWidgetMsg({ id: m.id, widgets: ws, requestId });
|
|
728
|
+
setAgentPending(false);
|
|
729
|
+
setPhase("answering-widget");
|
|
730
|
+
live = false;
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (m.type === "comment" && m.body?.trim()) {
|
|
734
|
+
shownIds.current.add(m.id);
|
|
735
|
+
push("assistant", m.body.trim(), "markdown");
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
if (m.authorType === "agent" && !m.body?.trim()) pending = true;
|
|
739
|
+
}
|
|
740
|
+
setAgentPending(pending);
|
|
741
|
+
} catch {
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
void poll();
|
|
745
|
+
const iv = setInterval(() => void poll(), 3e3);
|
|
746
|
+
return () => {
|
|
747
|
+
live = false;
|
|
748
|
+
clearInterval(iv);
|
|
749
|
+
};
|
|
750
|
+
}, [phase, requestId, config, push]);
|
|
751
|
+
useInput3((ch, key) => {
|
|
752
|
+
if (phase === "answering-widget") return;
|
|
753
|
+
if (key.escape) {
|
|
754
|
+
if (phase === "compose" || phase === "thread") onBack();
|
|
755
|
+
else if (phase === "confirm-implement") {
|
|
756
|
+
push("system", "Holding off on implementation.", "info");
|
|
757
|
+
setPhase("thread");
|
|
758
|
+
}
|
|
474
759
|
return;
|
|
475
760
|
}
|
|
476
|
-
if (key.
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
761
|
+
if ((phase === "compose" || phase === "thread") && (key.upArrow || key.downArrow)) {
|
|
762
|
+
const h = historyRef.current;
|
|
763
|
+
if (h.length === 0) return;
|
|
764
|
+
if (key.upArrow) {
|
|
765
|
+
const idx = histIndex === null ? h.length - 1 : Math.max(0, histIndex - 1);
|
|
766
|
+
setHistIndex(idx);
|
|
767
|
+
setInput(h[idx] ?? "");
|
|
768
|
+
} else if (histIndex !== null && histIndex < h.length - 1) {
|
|
769
|
+
const idx = histIndex + 1;
|
|
770
|
+
setHistIndex(idx);
|
|
771
|
+
setInput(h[idx] ?? "");
|
|
772
|
+
} else {
|
|
773
|
+
setHistIndex(null);
|
|
774
|
+
setInput("");
|
|
485
775
|
}
|
|
776
|
+
return;
|
|
486
777
|
}
|
|
487
|
-
if (
|
|
488
|
-
void doImplement(
|
|
778
|
+
if (phase === "confirm-implement") {
|
|
779
|
+
if (ch === "y" || ch === "Y") void doImplement();
|
|
780
|
+
else if (ch === "n" || ch === "N") {
|
|
781
|
+
push("system", "Holding off on implementation.", "info");
|
|
782
|
+
setPhase("thread");
|
|
783
|
+
}
|
|
489
784
|
}
|
|
490
785
|
});
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
);
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
async function doImplement(sessionId) {
|
|
506
|
-
try {
|
|
507
|
-
await startImplementing(config, sessionId);
|
|
508
|
-
} catch (err) {
|
|
509
|
-
setError(err instanceof Error ? err.message : "Unknown error");
|
|
786
|
+
const onSubmit = () => {
|
|
787
|
+
const text = input.trim();
|
|
788
|
+
if (!text) return;
|
|
789
|
+
const h = historyRef.current;
|
|
790
|
+
if (h[h.length - 1] !== text) h.push(text);
|
|
791
|
+
setHistIndex(null);
|
|
792
|
+
setInput("");
|
|
793
|
+
if (phase === "compose") {
|
|
794
|
+
push("user", text);
|
|
795
|
+
void createFromIdea(text);
|
|
796
|
+
} else if (phase === "thread") {
|
|
797
|
+
push("user", text);
|
|
798
|
+
void postFollowUp(text);
|
|
510
799
|
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
/* @__PURE__ */
|
|
525
|
-
/* @__PURE__ */
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
/* @__PURE__ */ jsx6(Text6, { children: "Enter merge branch name:" }),
|
|
537
|
-
/* @__PURE__ */ jsx6(
|
|
538
|
-
TextInput3,
|
|
800
|
+
};
|
|
801
|
+
const inputActive = phase === "compose" || phase === "thread";
|
|
802
|
+
const staticItems = [{ kind: "header", id: -1 }, ...msgs];
|
|
803
|
+
return /* @__PURE__ */ jsxs7(Box5, { flexDirection: "column", children: [
|
|
804
|
+
/* @__PURE__ */ jsx5(Static, { items: staticItems, children: (item) => item.kind === "header" ? /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsxs7(Text7, { bold: true, children: [
|
|
805
|
+
"\u273B FlumeCode \xB7 ",
|
|
806
|
+
repo.fullName,
|
|
807
|
+
" ",
|
|
808
|
+
/* @__PURE__ */ jsx5(Text7, { dimColor: true, children: "esc to go back" })
|
|
809
|
+
] }) }, String(item.id)) : /* @__PURE__ */ jsx5(Message, { msg: item, width }, String(item.id)) }),
|
|
810
|
+
phase === "creating" && /* @__PURE__ */ jsx5(Spinner, { label: "creating request\u2026" }),
|
|
811
|
+
phase === "working" && /* @__PURE__ */ jsx5(Spinner, { label: "working\u2026" }),
|
|
812
|
+
phase === "confirm-implement" && /* @__PURE__ */ jsxs7(Text7, { children: [
|
|
813
|
+
/* @__PURE__ */ jsx5(Text7, { color: "green", bold: true, children: "\u276F Start implementing?" }),
|
|
814
|
+
/* @__PURE__ */ jsx5(Text7, { dimColor: true, children: " y = yes \xB7 n = keep refining" })
|
|
815
|
+
] }),
|
|
816
|
+
phase === "answering-widget" && widgetMsg && /* @__PURE__ */ jsx5(WidgetAnswerScreen, { config, message: widgetMsg, onDone: onWidgetDone }),
|
|
817
|
+
phase === "thread" && agentPending && /* @__PURE__ */ jsx5(Spinner, { label: "waiting for the agent\u2026" }),
|
|
818
|
+
phase === "thread" && agentPending && noRunnerOnline && /* @__PURE__ */ jsx5(Text7, { color: "yellow", children: "\u26A0 no runner online \u2014 start one with `flumecode start`" }),
|
|
819
|
+
inputActive && /* @__PURE__ */ jsxs7(Fragment2, { children: [
|
|
820
|
+
error && /* @__PURE__ */ jsx5(Text7, { color: "red", children: error }),
|
|
821
|
+
/* @__PURE__ */ jsxs7(Box5, { borderStyle: "round", borderColor: inputActive ? "cyan" : "gray", paddingX: 1, children: [
|
|
822
|
+
/* @__PURE__ */ jsx5(Text7, { color: "cyan", bold: true, children: "\u203A " }),
|
|
823
|
+
/* @__PURE__ */ jsx5(
|
|
824
|
+
TextInput2,
|
|
539
825
|
{
|
|
540
|
-
value:
|
|
541
|
-
onChange:
|
|
542
|
-
|
|
826
|
+
value: input,
|
|
827
|
+
onChange: (v) => {
|
|
828
|
+
setInput(v);
|
|
829
|
+
if (histIndex !== null) setHistIndex(null);
|
|
830
|
+
},
|
|
831
|
+
onSubmit,
|
|
543
832
|
focus: true
|
|
544
|
-
}
|
|
833
|
+
},
|
|
834
|
+
`input-${histIndex ?? "live"}`
|
|
545
835
|
)
|
|
546
836
|
] }),
|
|
547
|
-
|
|
548
|
-
] })
|
|
549
|
-
}
|
|
550
|
-
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
|
|
551
|
-
/* @__PURE__ */ jsx6(Text6, { bold: true, children: "Thread (\u2191\u2193 navigate, Enter act on selected, i implement, Esc back)" }),
|
|
552
|
-
!data && /* @__PURE__ */ jsx6(Text6, { children: "Loading\u2026" }),
|
|
553
|
-
overlay.kind === "accepted" && /* @__PURE__ */ jsx6(Text6, { color: "green", children: "Plan accepted! Job queued." }),
|
|
554
|
-
messages.map((msg, i) => /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", marginY: 0, children: /* @__PURE__ */ jsxs6(Text6, { color: i === cursor ? "cyan" : void 0, children: [
|
|
555
|
-
i === cursor ? "\u25B6 " : " ",
|
|
556
|
-
"[",
|
|
557
|
-
msg.type,
|
|
558
|
-
"] ",
|
|
559
|
-
msg.body?.slice(0, 80) ?? "",
|
|
560
|
-
msg.widgets?.length ? ` (${msg.widgets.length} widget${msg.widgets.length > 1 ? "s" : ""})` : ""
|
|
561
|
-
] }) }, msg.id)),
|
|
562
|
-
implementableSession && /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
563
|
-
"Press i to start implementing session ",
|
|
564
|
-
implementableSession.id.slice(0, 8),
|
|
565
|
-
"\u2026"
|
|
566
|
-
] }),
|
|
567
|
-
error && /* @__PURE__ */ jsx6(Text6, { color: "red", children: error })
|
|
837
|
+
/* @__PURE__ */ jsx5(Text7, { dimColor: true, children: phase === "compose" ? "enter to create a request \xB7 esc back" : "enter send \xB7 esc back" })
|
|
838
|
+
] })
|
|
568
839
|
] });
|
|
569
840
|
}
|
|
570
|
-
var
|
|
571
|
-
"src/tui/
|
|
841
|
+
var init_ConversationScreen = __esm({
|
|
842
|
+
"src/tui/ConversationScreen.tsx"() {
|
|
572
843
|
"use strict";
|
|
573
|
-
init_usePoll();
|
|
574
844
|
init_tui_api();
|
|
575
845
|
init_WidgetAnswerScreen();
|
|
846
|
+
init_Message();
|
|
847
|
+
init_Spinner();
|
|
848
|
+
init_usePoll();
|
|
576
849
|
}
|
|
577
850
|
});
|
|
578
851
|
|
|
@@ -581,59 +854,37 @@ var App_exports = {};
|
|
|
581
854
|
__export(App_exports, {
|
|
582
855
|
App: () => App
|
|
583
856
|
});
|
|
584
|
-
import { useState as
|
|
585
|
-
import { Box as
|
|
586
|
-
import { jsx as
|
|
857
|
+
import { useState as useState6 } from "react";
|
|
858
|
+
import { Box as Box6 } from "ink";
|
|
859
|
+
import { jsx as jsx6, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
587
860
|
function renderScreen(screen, setScreen, config) {
|
|
588
861
|
switch (screen.name) {
|
|
589
862
|
case "repos":
|
|
590
|
-
return /* @__PURE__ */
|
|
863
|
+
return /* @__PURE__ */ jsx6(
|
|
591
864
|
RepoListScreen,
|
|
592
865
|
{
|
|
593
866
|
config,
|
|
594
|
-
onSelect: (repo) => setScreen({ name: "
|
|
867
|
+
onSelect: (repo) => setScreen({ name: "conversation", repo })
|
|
595
868
|
}
|
|
596
869
|
);
|
|
597
|
-
case "
|
|
598
|
-
return /* @__PURE__ */
|
|
599
|
-
|
|
870
|
+
case "conversation":
|
|
871
|
+
return /* @__PURE__ */ jsx6(
|
|
872
|
+
ConversationScreen,
|
|
600
873
|
{
|
|
601
874
|
config,
|
|
602
875
|
repo: screen.repo,
|
|
603
|
-
onOpen: (req) => setScreen({ name: "thread", repo: screen.repo, requestId: req.id }),
|
|
604
|
-
onNew: () => setScreen({ name: "new-request", repo: screen.repo }),
|
|
605
876
|
onBack: () => setScreen({ name: "repos" })
|
|
606
877
|
}
|
|
607
878
|
);
|
|
608
|
-
case "new-request":
|
|
609
|
-
return /* @__PURE__ */ jsx7(
|
|
610
|
-
NewRequestScreen,
|
|
611
|
-
{
|
|
612
|
-
config,
|
|
613
|
-
repo: screen.repo,
|
|
614
|
-
onCreated: (requestId) => setScreen({ name: "thread", repo: screen.repo, requestId }),
|
|
615
|
-
onBack: () => setScreen({ name: "requests", repo: screen.repo })
|
|
616
|
-
}
|
|
617
|
-
);
|
|
618
|
-
case "thread":
|
|
619
|
-
return /* @__PURE__ */ jsx7(
|
|
620
|
-
ThreadScreen,
|
|
621
|
-
{
|
|
622
|
-
config,
|
|
623
|
-
repo: screen.repo,
|
|
624
|
-
requestId: screen.requestId,
|
|
625
|
-
onBack: () => setScreen({ name: "requests", repo: screen.repo })
|
|
626
|
-
}
|
|
627
|
-
);
|
|
628
879
|
}
|
|
629
880
|
}
|
|
630
881
|
function App({ config }) {
|
|
631
|
-
const [screen, setScreen] =
|
|
882
|
+
const [screen, setScreen] = useState6({ name: "repos" });
|
|
632
883
|
const jobs = usePoll(() => listJobs(config), 3e3);
|
|
633
884
|
const runners = usePoll(() => listRunners(config), 3e3);
|
|
634
|
-
return /* @__PURE__ */
|
|
885
|
+
return /* @__PURE__ */ jsxs8(Box6, { flexDirection: "column", children: [
|
|
635
886
|
renderScreen(screen, setScreen, config),
|
|
636
|
-
/* @__PURE__ */
|
|
887
|
+
/* @__PURE__ */ jsx6(StatusBar, { jobs, runners })
|
|
637
888
|
] });
|
|
638
889
|
}
|
|
639
890
|
var init_App = __esm({
|
|
@@ -643,15 +894,17 @@ var init_App = __esm({
|
|
|
643
894
|
init_usePoll();
|
|
644
895
|
init_StatusBar();
|
|
645
896
|
init_RepoListScreen();
|
|
646
|
-
|
|
647
|
-
init_NewRequestScreen();
|
|
648
|
-
init_ThreadScreen();
|
|
897
|
+
init_ConversationScreen();
|
|
649
898
|
}
|
|
650
899
|
});
|
|
651
900
|
|
|
652
901
|
// src/cli.ts
|
|
653
902
|
import { createInterface } from "node:readline/promises";
|
|
654
903
|
import { stdin, stdout } from "node:process";
|
|
904
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
905
|
+
import { openSync } from "node:fs";
|
|
906
|
+
import { homedir as homedir2 } from "node:os";
|
|
907
|
+
import { join as join7 } from "node:path";
|
|
655
908
|
|
|
656
909
|
// src/config.ts
|
|
657
910
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -663,8 +916,8 @@ function readConfig() {
|
|
|
663
916
|
if (!existsSync(configPath)) return null;
|
|
664
917
|
try {
|
|
665
918
|
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
666
|
-
if (!parsed.serverUrl || !parsed.token) return null;
|
|
667
|
-
const config = { serverUrl: parsed.serverUrl, token: parsed.token };
|
|
919
|
+
if (!parsed.serverUrl || !parsed.token && !parsed.apiToken) return null;
|
|
920
|
+
const config = { serverUrl: parsed.serverUrl, token: parsed.token ?? "" };
|
|
668
921
|
if (parsed.apiToken) config.apiToken = parsed.apiToken;
|
|
669
922
|
return config;
|
|
670
923
|
} catch {
|
|
@@ -676,6 +929,118 @@ function writeConfig(config) {
|
|
|
676
929
|
writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
677
930
|
}
|
|
678
931
|
|
|
932
|
+
// src/browser-login.ts
|
|
933
|
+
import { createServer } from "node:http";
|
|
934
|
+
import { randomBytes } from "node:crypto";
|
|
935
|
+
import { hostname } from "node:os";
|
|
936
|
+
import { spawn } from "node:child_process";
|
|
937
|
+
var LOGIN_TIMEOUT_MS = 5 * 6e4;
|
|
938
|
+
function openBrowser(url) {
|
|
939
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
940
|
+
try {
|
|
941
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
942
|
+
child.on("error", () => {
|
|
943
|
+
});
|
|
944
|
+
child.unref();
|
|
945
|
+
} catch {
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
function respondHtml(res, status, title, body2) {
|
|
949
|
+
const html = `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title><style>
|
|
950
|
+
body{font:16px/1.5 system-ui,sans-serif;background:#0b0b0f;color:#e8e8ea;display:grid;place-items:center;height:100vh;margin:0}
|
|
951
|
+
.box{max-width:28rem;padding:2rem;text-align:center}
|
|
952
|
+
h1{font-size:1.25rem;margin:0 0 .5rem}p{color:#a0a0a8;margin:0}
|
|
953
|
+
</style></head><body><div class="box"><h1>${title}</h1><p>${body2}</p></div></body></html>`;
|
|
954
|
+
res.statusCode = status;
|
|
955
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
956
|
+
res.end(html);
|
|
957
|
+
}
|
|
958
|
+
function browserLogin(opts) {
|
|
959
|
+
const print = opts.print ?? ((m) => console.log(m));
|
|
960
|
+
const open = opts.open ?? openBrowser;
|
|
961
|
+
const state = randomBytes(16).toString("hex");
|
|
962
|
+
return new Promise((resolve, reject) => {
|
|
963
|
+
let timer;
|
|
964
|
+
const server = createServer((req, res) => {
|
|
965
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
966
|
+
if (url.pathname !== "/" && url.pathname !== "/callback") {
|
|
967
|
+
res.statusCode = 404;
|
|
968
|
+
res.end("Not found");
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
const params = url.searchParams;
|
|
972
|
+
if (params.get("state") !== state) {
|
|
973
|
+
respondHtml(
|
|
974
|
+
res,
|
|
975
|
+
400,
|
|
976
|
+
"Login mismatch",
|
|
977
|
+
"This authorization did not match the pending login. Run `flumecode login` again."
|
|
978
|
+
);
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const error = params.get("error");
|
|
982
|
+
if (error) {
|
|
983
|
+
respondHtml(
|
|
984
|
+
res,
|
|
985
|
+
200,
|
|
986
|
+
"Login canceled",
|
|
987
|
+
"You can close this tab and return to the terminal."
|
|
988
|
+
);
|
|
989
|
+
cleanup2();
|
|
990
|
+
reject(
|
|
991
|
+
new Error(
|
|
992
|
+
error === "denied" ? "Login was denied in the browser." : `Login failed: ${error}`
|
|
993
|
+
)
|
|
994
|
+
);
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
respondHtml(
|
|
998
|
+
res,
|
|
999
|
+
200,
|
|
1000
|
+
"You're signed in",
|
|
1001
|
+
"Authorization complete \u2014 close this tab and return to the terminal."
|
|
1002
|
+
);
|
|
1003
|
+
cleanup2();
|
|
1004
|
+
resolve({
|
|
1005
|
+
cliToken: params.get("cli") ?? void 0,
|
|
1006
|
+
runnerToken: params.get("runner") ?? void 0
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
const cleanup2 = () => {
|
|
1010
|
+
clearTimeout(timer);
|
|
1011
|
+
server.close();
|
|
1012
|
+
};
|
|
1013
|
+
server.on("error", (err) => {
|
|
1014
|
+
cleanup2();
|
|
1015
|
+
reject(err);
|
|
1016
|
+
});
|
|
1017
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1018
|
+
const addr = server.address();
|
|
1019
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
1020
|
+
if (!port) {
|
|
1021
|
+
cleanup2();
|
|
1022
|
+
reject(new Error("Could not start the local login listener."));
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const authorizeUrl = `${opts.serverUrl}/cli/authorize?` + new URLSearchParams({
|
|
1026
|
+
port: String(port),
|
|
1027
|
+
state,
|
|
1028
|
+
host: hostname(),
|
|
1029
|
+
scope: opts.scope
|
|
1030
|
+
}).toString();
|
|
1031
|
+
print("Opening your browser to authorize this machine\u2026");
|
|
1032
|
+
print(`If it doesn't open, paste this URL:
|
|
1033
|
+
${authorizeUrl}
|
|
1034
|
+
`);
|
|
1035
|
+
open(authorizeUrl);
|
|
1036
|
+
});
|
|
1037
|
+
timer = setTimeout(() => {
|
|
1038
|
+
cleanup2();
|
|
1039
|
+
reject(new Error("Timed out waiting for browser authorization (5 min)."));
|
|
1040
|
+
}, LOGIN_TIMEOUT_MS);
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
|
|
679
1044
|
// src/run.ts
|
|
680
1045
|
import { existsSync as existsSync5 } from "node:fs";
|
|
681
1046
|
import { join as join6 } from "node:path";
|
|
@@ -735,8 +1100,8 @@ async function pollJobCanceling(config, jobId) {
|
|
|
735
1100
|
});
|
|
736
1101
|
noteServerVersion(res);
|
|
737
1102
|
if (!res.ok) return false;
|
|
738
|
-
const
|
|
739
|
-
return
|
|
1103
|
+
const body2 = await res.json();
|
|
1104
|
+
return body2.canceling === true;
|
|
740
1105
|
}
|
|
741
1106
|
async function reportJob(config, jobId, result) {
|
|
742
1107
|
const res = await fetch(`${config.serverUrl}/api/runner/jobs/${jobId}/complete`, {
|
|
@@ -967,6 +1332,9 @@ var requirementSchema = z2.object({
|
|
|
967
1332
|
)
|
|
968
1333
|
});
|
|
969
1334
|
var planInputSchema = {
|
|
1335
|
+
dependsOn: z2.array(z2.number().int().positive()).optional().describe(
|
|
1336
|
+
"1-based positions of EARLIER plans in this same submission that must be merged or closed before this one. Use ONLY when the work must be strictly serial; omit for independent plans."
|
|
1337
|
+
),
|
|
970
1338
|
title: z2.string().min(1).max(120).describe(
|
|
971
1339
|
"A concise, descriptive name for THIS plan. Must be distinct from the request title and from any sibling plans on the same request. Keep it under 120 characters."
|
|
972
1340
|
),
|
|
@@ -1088,6 +1456,20 @@ function renderPlan(plan) {
|
|
|
1088
1456
|
lines2.push(PLAN_MARKER);
|
|
1089
1457
|
return lines2.join("\n");
|
|
1090
1458
|
}
|
|
1459
|
+
function validateDependsOnReferences(arr, ctx) {
|
|
1460
|
+
for (let i = 0; i < arr.length; i++) {
|
|
1461
|
+
const deps = arr[i]?.dependsOn ?? [];
|
|
1462
|
+
for (const dep of deps) {
|
|
1463
|
+
if (dep > i || dep < 1) {
|
|
1464
|
+
ctx.addIssue({
|
|
1465
|
+
code: z2.ZodIssueCode.custom,
|
|
1466
|
+
path: [i, "dependsOn"],
|
|
1467
|
+
message: "dependsOn must reference an earlier plan (1-based) in this submission."
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1091
1473
|
var submitPlanInputSchema = {
|
|
1092
1474
|
plans: z2.array(requireAtLeastTwoCriteria(requireRootCauseForFix(z2.object(planInputSchema)))).min(1).refine(
|
|
1093
1475
|
(arr) => {
|
|
@@ -1095,18 +1477,21 @@ var submitPlanInputSchema = {
|
|
|
1095
1477
|
return new Set(titles).size === titles.length;
|
|
1096
1478
|
},
|
|
1097
1479
|
{ message: "Each plan must have a distinct non-empty title" }
|
|
1098
|
-
)
|
|
1480
|
+
).superRefine(validateDependsOnReferences)
|
|
1099
1481
|
};
|
|
1100
1482
|
var submitPlanSchema = z2.object(submitPlanInputSchema);
|
|
1483
|
+
function toPlanDraft(plan) {
|
|
1484
|
+
return { body: renderPlan(plan), dependsOn: plan.dependsOn ?? [] };
|
|
1485
|
+
}
|
|
1101
1486
|
function createPlanTooling() {
|
|
1102
1487
|
let renderedPlans = null;
|
|
1103
1488
|
const submitPlan = tool2(
|
|
1104
1489
|
SUBMIT_PLAN,
|
|
1105
|
-
`Submit ALL your plans in a single call \u2014 one entry per plan; each becomes its own independently-acceptable Accept-as-plan draft. Do NOT call submit_plan more than once. requirements is required in each plan: an array of objects, each with a requirement (plain-language intent) and an acceptanceCriteria array (machine-checkable proof for that requirement). At least 1 requirement required; at least 2 acceptance criteria total across all requirements. Each requirement's acceptanceCriteria must be non-empty (at least 1 item). The 'title' field names each specific plan \u2014 make it concise and distinct from the request title and from sibling plan titles. When a plan's scope is "fix", rootCause is required: a non-empty explanation of the underlying cause of the bug (not just the symptom). motivation is optional: the user's stated or asked-for reason for the request.
|
|
1490
|
+
`Submit ALL your plans in a single call \u2014 one entry per plan; each becomes its own independently-acceptable Accept-as-plan draft. Do NOT call submit_plan more than once. requirements is required in each plan: an array of objects, each with a requirement (plain-language intent) and an acceptanceCriteria array (machine-checkable proof for that requirement). At least 1 requirement required; at least 2 acceptance criteria total across all requirements. Each requirement's acceptanceCriteria must be non-empty (at least 1 item). The 'title' field names each specific plan \u2014 make it concise and distinct from the request title and from sibling plan titles. When a plan's scope is "fix", rootCause is required: a non-empty explanation of the underlying cause of the bug (not just the symptom). motivation is optional: the user's stated or asked-for reason for the request. dependsOn is optional: a list of 1-based positions of EARLIER plans in this same submission that must be merged or closed before this one \u2014 use only when plans must be worked strictly serially.`,
|
|
1106
1491
|
submitPlanInputSchema,
|
|
1107
1492
|
async (args) => {
|
|
1108
1493
|
const parsed = submitPlanSchema.parse(args);
|
|
1109
|
-
renderedPlans = parsed.plans.map(
|
|
1494
|
+
renderedPlans = parsed.plans.map(toPlanDraft);
|
|
1110
1495
|
return {
|
|
1111
1496
|
content: [
|
|
1112
1497
|
{
|
|
@@ -1344,7 +1729,7 @@ async function runClaudeCode(opts) {
|
|
|
1344
1729
|
const { mcpServer, collected } = createWidgetTooling();
|
|
1345
1730
|
const { mcpServer: planServer, getPlans } = createPlanTooling();
|
|
1346
1731
|
const { mcpServer: reportServer, getReport } = createReportTooling();
|
|
1347
|
-
for await (const
|
|
1732
|
+
for await (const message2 of query({
|
|
1348
1733
|
prompt: opts.prompt,
|
|
1349
1734
|
options: {
|
|
1350
1735
|
cwd: opts.cwd,
|
|
@@ -1368,8 +1753,8 @@ async function runClaudeCode(opts) {
|
|
|
1368
1753
|
allowedTools: [...WIDGET_TOOL_NAMES, PLAN_TOOL_NAME, REPORT_TOOL_NAME, "Task"]
|
|
1369
1754
|
}
|
|
1370
1755
|
})) {
|
|
1371
|
-
if (
|
|
1372
|
-
const content =
|
|
1756
|
+
if (message2.type === "assistant") {
|
|
1757
|
+
const content = message2.message?.content;
|
|
1373
1758
|
if (Array.isArray(content)) {
|
|
1374
1759
|
for (const block of content) {
|
|
1375
1760
|
if (block && block.type === "text" && typeof block.text === "string") {
|
|
@@ -1380,8 +1765,8 @@ async function runClaudeCode(opts) {
|
|
|
1380
1765
|
}
|
|
1381
1766
|
}
|
|
1382
1767
|
}
|
|
1383
|
-
} else if (
|
|
1384
|
-
const content =
|
|
1768
|
+
} else if (message2.type === "user") {
|
|
1769
|
+
const content = message2.message?.content;
|
|
1385
1770
|
if (Array.isArray(content)) {
|
|
1386
1771
|
for (const block of content) {
|
|
1387
1772
|
if (block && block.type === "tool_result") {
|
|
@@ -1389,10 +1774,10 @@ async function runClaudeCode(opts) {
|
|
|
1389
1774
|
}
|
|
1390
1775
|
}
|
|
1391
1776
|
}
|
|
1392
|
-
} else if (
|
|
1393
|
-
finalText =
|
|
1777
|
+
} else if (message2.type === "result") {
|
|
1778
|
+
finalText = message2.result ?? "";
|
|
1394
1779
|
logEvent("result", finalText);
|
|
1395
|
-
const resultMsg =
|
|
1780
|
+
const resultMsg = message2;
|
|
1396
1781
|
if (resultMsg.usage) {
|
|
1397
1782
|
usageAcc.inputTokens += resultMsg.usage.input_tokens ?? 0;
|
|
1398
1783
|
usageAcc.outputTokens += resultMsg.usage.output_tokens ?? 0;
|
|
@@ -1400,8 +1785,8 @@ async function runClaudeCode(opts) {
|
|
|
1400
1785
|
usageAcc.cacheReadTokens += resultMsg.usage.cache_read_input_tokens ?? 0;
|
|
1401
1786
|
}
|
|
1402
1787
|
usageAcc.costUsd += resultMsg.total_cost_usd ?? 0;
|
|
1403
|
-
} else if (
|
|
1404
|
-
logEvent("system", JSON.stringify(
|
|
1788
|
+
} else if (message2.type === "system") {
|
|
1789
|
+
logEvent("system", JSON.stringify(message2));
|
|
1405
1790
|
}
|
|
1406
1791
|
}
|
|
1407
1792
|
process.stdout.write("\n");
|
|
@@ -1412,7 +1797,7 @@ async function runClaudeCode(opts) {
|
|
|
1412
1797
|
}
|
|
1413
1798
|
|
|
1414
1799
|
// src/codex.ts
|
|
1415
|
-
import { spawn } from "node:child_process";
|
|
1800
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1416
1801
|
import { mkdtempSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
1417
1802
|
import { join as join2 } from "node:path";
|
|
1418
1803
|
import { tmpdir } from "node:os";
|
|
@@ -1422,7 +1807,7 @@ async function runCodex(opts) {
|
|
|
1422
1807
|
const tmpDir = mkdtempSync(join2(tmpdir(), "flume-codex-"));
|
|
1423
1808
|
const outFile = join2(tmpDir, "flume-mcp.jsonl");
|
|
1424
1809
|
writeFileSync2(outFile, "");
|
|
1425
|
-
const child =
|
|
1810
|
+
const child = spawn2(
|
|
1426
1811
|
"codex",
|
|
1427
1812
|
[
|
|
1428
1813
|
"exec",
|
|
@@ -1475,7 +1860,8 @@ async function runCodex(opts) {
|
|
|
1475
1860
|
}
|
|
1476
1861
|
const raw = existsSync2(outFile) ? readFileSync3(outFile, "utf8") : "";
|
|
1477
1862
|
const records = raw.split("\n").filter(Boolean).map(parseJsonLine).filter((r) => r !== null);
|
|
1478
|
-
const
|
|
1863
|
+
const rawPlans = records.filter((r) => r.kind === "plans").flatMap((r) => r.plans ?? []);
|
|
1864
|
+
const plans = rawPlans.map((body2) => ({ body: body2, dependsOn: [] }));
|
|
1479
1865
|
const widgets = records.filter((r) => r.kind === "widget").map((r) => r.widget);
|
|
1480
1866
|
return {
|
|
1481
1867
|
text: "",
|
|
@@ -1499,14 +1885,14 @@ async function checkClaudeCode() {
|
|
|
1499
1885
|
try {
|
|
1500
1886
|
let sawResult = false;
|
|
1501
1887
|
const run = (async () => {
|
|
1502
|
-
for await (const
|
|
1888
|
+
for await (const message2 of query2({
|
|
1503
1889
|
prompt: "Reply with the single word: ok",
|
|
1504
1890
|
options: { permissionMode: "bypassPermissions", maxTurns: 1 }
|
|
1505
1891
|
})) {
|
|
1506
|
-
if (
|
|
1892
|
+
if (message2.type === "result") {
|
|
1507
1893
|
sawResult = true;
|
|
1508
|
-
const isError =
|
|
1509
|
-
const subtype =
|
|
1894
|
+
const isError = message2.is_error === true;
|
|
1895
|
+
const subtype = message2.subtype;
|
|
1510
1896
|
if (isError)
|
|
1511
1897
|
throw new Error(
|
|
1512
1898
|
subtype ? `Claude Code error: ${subtype}` : "Claude Code returned an error"
|
|
@@ -1521,9 +1907,9 @@ async function checkClaudeCode() {
|
|
|
1521
1907
|
return { ready: false, error: errorMessage(err) };
|
|
1522
1908
|
}
|
|
1523
1909
|
}
|
|
1524
|
-
function withTimeout(p, ms,
|
|
1910
|
+
function withTimeout(p, ms, message2) {
|
|
1525
1911
|
return new Promise((resolve, reject) => {
|
|
1526
|
-
const timer = setTimeout(() => reject(new Error(
|
|
1912
|
+
const timer = setTimeout(() => reject(new Error(message2)), ms);
|
|
1527
1913
|
p.then(
|
|
1528
1914
|
(v) => {
|
|
1529
1915
|
clearTimeout(timer);
|
|
@@ -1585,6 +1971,13 @@ function appendRule(lines2, intro, ruleName) {
|
|
|
1585
1971
|
lines2.push("", intro, "", loadRule(ruleName));
|
|
1586
1972
|
}
|
|
1587
1973
|
var WRITING_INTRO = "These technical-writing guidelines apply to the plan and report prose you author in this run:";
|
|
1974
|
+
var SETUP_PREVIEW_MARKER = "<!-- flumecode:setup-preview -->";
|
|
1975
|
+
function setupPreviewDirective(mode) {
|
|
1976
|
+
if (mode === "plan") {
|
|
1977
|
+
return "This request installs FlumeCode's **UI Preview plugin**. Use the `flumecode:setup-preview` skill as your authority: analyse this app read-only to determine exactly what the plugin needs \u2014 the app directory, the install and dev-server commands, any env for a hermetic boot, the globs that count as a UI change, and the showcase strategy \u2014 and fold that into the plan you submit (the precise `.flumecode/plugins/ui-preview.json` fields to write and the `.flumecode/wiki/components/ui-preview.md` record to add). Do not write files; you are read-only.";
|
|
1978
|
+
}
|
|
1979
|
+
return "This request installs FlumeCode's **UI Preview plugin**. The `flumecode:setup-preview` skill is the authority on what to produce \u2014 `.flumecode/plugins/ui-preview.json` (the machine recipe later previews execute) and a `.flumecode/wiki/components/ui-preview.md` record, plus any packages future previews need. As the orchestrator, direct your implementation subagent to follow `flumecode:setup-preview` exactly, and your review subagent to verify the manifest and wiki record against it.";
|
|
1980
|
+
}
|
|
1588
1981
|
var LANGUAGE_DIRECTIVE = "First, determine the dominant natural language of the incoming thread (the request title/body and the user's messages). Use that one language for EVERYTHING you author this run - your reply body, any plan or report fields, AND every clarifying question and its widget options. Never mix languages: if the thread is in English, your questions and options must be in English too. Keep code identifiers, file paths, and quoted code verbatim.";
|
|
1589
1982
|
function turnHeading(turn, agentName) {
|
|
1590
1983
|
if (turn.role === "user") return "User";
|
|
@@ -1621,6 +2014,9 @@ function buildPrompt(ctx) {
|
|
|
1621
2014
|
loadRule("coding-guideline")
|
|
1622
2015
|
);
|
|
1623
2016
|
}
|
|
2017
|
+
if (ctx.request?.body?.includes(SETUP_PREVIEW_MARKER)) {
|
|
2018
|
+
lines2.push("", setupPreviewDirective(ctx.permissionMode));
|
|
2019
|
+
}
|
|
1624
2020
|
appendRule(lines2, WRITING_INTRO, "technical-writing");
|
|
1625
2021
|
lines2.push("", `# Request: ${ctx.request?.title ?? ""}`);
|
|
1626
2022
|
if (ctx.request?.body) {
|
|
@@ -1789,12 +2185,19 @@ function buildCodexPrompt(ctx) {
|
|
|
1789
2185
|
"\n"
|
|
1790
2186
|
);
|
|
1791
2187
|
}
|
|
1792
|
-
function buildPreviewPrompt(ctx, committedFileNames,
|
|
2188
|
+
function buildPreviewPrompt(ctx, committedFileNames, manifest, sentinelPath) {
|
|
1793
2189
|
const lines2 = [
|
|
1794
2190
|
`You are "${ctx.agentName}" authoring an ephemeral UI showcase for ${ctx.repo.fullName}.`,
|
|
1795
|
-
`An implementation just ran and committed UI changes to branch "${ctx.repo.checkoutBranch}". Use the \`flumecode:preview-ui\` skill to create a temporary showcase
|
|
1796
|
-
`
|
|
1797
|
-
`
|
|
2191
|
+
`An implementation just ran and committed UI changes to branch "${ctx.repo.checkoutBranch}". Use the \`flumecode:preview-ui\` skill to create a temporary showcase that renders the changed UI with realistic fake data, following the recipe below for this repo's stack.`,
|
|
2192
|
+
`Record the showcase's URL path (exactly, no trailing newline) in the file \`${sentinelPath}\` \u2014 the runner reads it to navigate to your showcase.`,
|
|
2193
|
+
`Your showcase is EPHEMERAL: the runner reverts the working tree after capturing it, so you may edit whatever the recipe requires (including registering a temporary route in tracked files). Do NOT commit or push.`,
|
|
2194
|
+
"",
|
|
2195
|
+
"## Preview recipe (from this repo's ui-preview plugin manifest)",
|
|
2196
|
+
"",
|
|
2197
|
+
`- App directory: \`${manifest.appDir}\``,
|
|
2198
|
+
`- Showcase strategy: ${manifest.showcase.kind}`,
|
|
2199
|
+
"",
|
|
2200
|
+
manifest.showcase.instructions || "(no recipe instructions were recorded \u2014 infer from the app)",
|
|
1798
2201
|
"",
|
|
1799
2202
|
"These coding guidelines apply to all code produced in this run:",
|
|
1800
2203
|
"",
|
|
@@ -1808,14 +2211,14 @@ function buildPreviewPrompt(ctx, committedFileNames, tmpRoute) {
|
|
|
1808
2211
|
if (committedFileNames.trim()) {
|
|
1809
2212
|
lines2.push(
|
|
1810
2213
|
"",
|
|
1811
|
-
"UI files changed by this implementation (showcase these
|
|
2214
|
+
"UI files changed by this implementation (showcase these):",
|
|
1812
2215
|
"",
|
|
1813
2216
|
committedFileNames.trim()
|
|
1814
2217
|
);
|
|
1815
2218
|
}
|
|
1816
2219
|
lines2.push(
|
|
1817
2220
|
"",
|
|
1818
|
-
"When done, reply with one short sentence confirming what showcase
|
|
2221
|
+
"When done, reply with one short sentence confirming what showcase you created and its URL path."
|
|
1819
2222
|
);
|
|
1820
2223
|
return lines2.join("\n");
|
|
1821
2224
|
}
|
|
@@ -1830,7 +2233,8 @@ function buildInitPrompt(ctx) {
|
|
|
1830
2233
|
|
|
1831
2234
|
// src/types.ts
|
|
1832
2235
|
function jobTitle(ctx) {
|
|
1833
|
-
|
|
2236
|
+
if (ctx.kind === "init") return "Initialize FlumeCode wiki";
|
|
2237
|
+
return ctx.request?.title ?? "request";
|
|
1834
2238
|
}
|
|
1835
2239
|
|
|
1836
2240
|
// src/workspace.ts
|
|
@@ -2093,7 +2497,7 @@ async function openPullRequest(ctx) {
|
|
|
2093
2497
|
"content-type": "application/json"
|
|
2094
2498
|
};
|
|
2095
2499
|
const title = jobTitle(ctx);
|
|
2096
|
-
const
|
|
2500
|
+
const body2 = ctx.kind === "init" ? "Bootstraps the `.flumecode/` wiki for this repository. Opened by the FlumeCode runner." : `Opened by the FlumeCode runner for request "${title}".`;
|
|
2097
2501
|
const res = await fetch(`${apiBase}/pulls`, {
|
|
2098
2502
|
method: "POST",
|
|
2099
2503
|
headers,
|
|
@@ -2101,7 +2505,7 @@ async function openPullRequest(ctx) {
|
|
|
2101
2505
|
title: `FlumeCode: ${title}`,
|
|
2102
2506
|
head: checkoutBranch,
|
|
2103
2507
|
base: mergeBranch,
|
|
2104
|
-
body
|
|
2508
|
+
body: body2
|
|
2105
2509
|
})
|
|
2106
2510
|
});
|
|
2107
2511
|
if (res.status === 201) {
|
|
@@ -2120,15 +2524,28 @@ async function openPullRequest(ctx) {
|
|
|
2120
2524
|
}
|
|
2121
2525
|
throw new Error(`PR creation failed: ${res.status} ${await res.text()}`);
|
|
2122
2526
|
}
|
|
2123
|
-
async function gitCommittedFiles(dir) {
|
|
2527
|
+
async function gitCommittedFiles(ctx, dir) {
|
|
2528
|
+
const { mergeBranch } = ctx.repo;
|
|
2529
|
+
if (!mergeBranch) {
|
|
2530
|
+
const { stdout: stdout3 } = await git([
|
|
2531
|
+
"-C",
|
|
2532
|
+
dir,
|
|
2533
|
+
"--no-pager",
|
|
2534
|
+
"show",
|
|
2535
|
+
"--name-only",
|
|
2536
|
+
"--format=",
|
|
2537
|
+
"HEAD"
|
|
2538
|
+
]);
|
|
2539
|
+
return stdout3;
|
|
2540
|
+
}
|
|
2541
|
+
await git(["-C", dir, "fetch", "--quiet", "origin", mergeBranch]);
|
|
2124
2542
|
const { stdout: stdout2 } = await git([
|
|
2125
2543
|
"-C",
|
|
2126
2544
|
dir,
|
|
2127
2545
|
"--no-pager",
|
|
2128
|
-
"
|
|
2546
|
+
"diff",
|
|
2129
2547
|
"--name-only",
|
|
2130
|
-
"
|
|
2131
|
-
"HEAD"
|
|
2548
|
+
"FETCH_HEAD...HEAD"
|
|
2132
2549
|
]);
|
|
2133
2550
|
return stdout2;
|
|
2134
2551
|
}
|
|
@@ -2192,97 +2609,170 @@ async function prNumbersForCommit(ctx, sha) {
|
|
|
2192
2609
|
}
|
|
2193
2610
|
|
|
2194
2611
|
// src/preview.ts
|
|
2195
|
-
import {
|
|
2196
|
-
import {
|
|
2197
|
-
import {
|
|
2198
|
-
import {
|
|
2612
|
+
import { execFile as execFile2, spawn as spawn3 } from "node:child_process";
|
|
2613
|
+
import { existsSync as existsSync4, mkdtempSync as mkdtempSync2, readFileSync as readFileSync5, rmSync } from "node:fs";
|
|
2614
|
+
import { createServer as createServer2 } from "node:net";
|
|
2615
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
2616
|
+
import { join as join5 } from "node:path";
|
|
2617
|
+
import { promisify as promisify2 } from "node:util";
|
|
2618
|
+
var exec2 = promisify2(execFile2);
|
|
2199
2619
|
var PREVIEW_MAX_TURNS = 60;
|
|
2200
2620
|
var CAPTURE_TIMEOUT_MS = 3e4;
|
|
2201
2621
|
var DEV_SERVER_READY_TIMEOUT_MS = 6e4;
|
|
2622
|
+
var INSTALL_TIMEOUT_MS = 3e5;
|
|
2623
|
+
var MANIFEST_REL = ".flumecode/plugins/ui-preview.json";
|
|
2202
2624
|
var VIEWPORTS = [
|
|
2203
2625
|
{ name: "mobile", width: 375, height: 812 },
|
|
2204
2626
|
{ name: "tablet", width: 768, height: 1024 },
|
|
2205
2627
|
{ name: "desktop", width: 1280, height: 800 }
|
|
2206
2628
|
];
|
|
2207
|
-
var
|
|
2208
|
-
"
|
|
2209
|
-
"
|
|
2210
|
-
"
|
|
2211
|
-
"
|
|
2212
|
-
"
|
|
2213
|
-
"
|
|
2214
|
-
"
|
|
2215
|
-
"
|
|
2216
|
-
"
|
|
2217
|
-
|
|
2629
|
+
var DEFAULT_UI_GLOBS = [
|
|
2630
|
+
"**/*.tsx",
|
|
2631
|
+
"**/*.jsx",
|
|
2632
|
+
"**/*.vue",
|
|
2633
|
+
"**/*.svelte",
|
|
2634
|
+
"**/*.astro",
|
|
2635
|
+
"**/*.css",
|
|
2636
|
+
"**/*.scss",
|
|
2637
|
+
"**/*.sass",
|
|
2638
|
+
"**/*.less",
|
|
2639
|
+
"**/*.html",
|
|
2640
|
+
"**/*.htm",
|
|
2641
|
+
"**/*.erb",
|
|
2642
|
+
"**/*.haml",
|
|
2643
|
+
"**/*.slim"
|
|
2644
|
+
];
|
|
2218
2645
|
async function buildPreview(ctx, dir, committedFileNames, config, abort) {
|
|
2219
|
-
|
|
2646
|
+
let manifest;
|
|
2647
|
+
try {
|
|
2648
|
+
manifest = readPreviewManifest(dir);
|
|
2649
|
+
} catch (err) {
|
|
2650
|
+
return { status: "failed", reason: `UI Preview plugin manifest is invalid: ${message(err)}` };
|
|
2651
|
+
}
|
|
2652
|
+
if (!manifest) {
|
|
2653
|
+
return {
|
|
2654
|
+
status: "skipped",
|
|
2655
|
+
reason: `UI Preview plugin not initialized \u2014 a repo member can enable it in repo settings (${MANIFEST_REL} is absent).`
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
if (!changedFilesTouchUi(committedFileNames, manifest.uiGlobs)) {
|
|
2220
2659
|
return { status: "skipped", reason: "No UI files were changed in this run." };
|
|
2221
2660
|
}
|
|
2661
|
+
if (!manifest.devServer.trim()) {
|
|
2662
|
+
return {
|
|
2663
|
+
status: "blocked",
|
|
2664
|
+
reason: "The UI Preview plugin needs manual setup before previews can run.",
|
|
2665
|
+
...manifest.manualSteps ? { manualSteps: manifest.manualSteps } : {}
|
|
2666
|
+
};
|
|
2667
|
+
}
|
|
2222
2668
|
try {
|
|
2223
2669
|
const { previewId, entrypoint } = await runPreviewPass(
|
|
2224
2670
|
ctx,
|
|
2225
2671
|
dir,
|
|
2226
2672
|
committedFileNames,
|
|
2673
|
+
manifest,
|
|
2227
2674
|
config,
|
|
2228
2675
|
abort
|
|
2229
2676
|
);
|
|
2230
2677
|
return { status: "ready", previewId, entrypoint };
|
|
2231
2678
|
} catch (err) {
|
|
2232
|
-
|
|
2679
|
+
if (manifest.manualSteps) {
|
|
2680
|
+
return { status: "blocked", reason: message(err), manualSteps: manifest.manualSteps };
|
|
2681
|
+
}
|
|
2682
|
+
return { status: "failed", reason: message(err) };
|
|
2233
2683
|
}
|
|
2234
2684
|
}
|
|
2235
|
-
function
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
const dot = trimmed.lastIndexOf(".");
|
|
2240
|
-
if (dot === -1) continue;
|
|
2241
|
-
const ext = trimmed.slice(dot);
|
|
2242
|
-
if (UI_EXTENSIONS.has(ext)) return true;
|
|
2243
|
-
}
|
|
2244
|
-
return false;
|
|
2245
|
-
}
|
|
2246
|
-
function detectFramework(dir) {
|
|
2247
|
-
const pm = detectPackageManager(dir);
|
|
2248
|
-
const pkgPath = join5(dir, "package.json");
|
|
2249
|
-
if (!existsSync4(pkgPath)) return { pm, family: null, devCmd: null };
|
|
2250
|
-
let pkg;
|
|
2685
|
+
function readPreviewManifest(repoRoot) {
|
|
2686
|
+
const path = join5(repoRoot, MANIFEST_REL);
|
|
2687
|
+
if (!existsSync4(path)) return null;
|
|
2688
|
+
let raw;
|
|
2251
2689
|
try {
|
|
2252
|
-
|
|
2253
|
-
} catch {
|
|
2254
|
-
|
|
2690
|
+
raw = JSON.parse(readFileSync5(path, "utf8"));
|
|
2691
|
+
} catch (err) {
|
|
2692
|
+
throw new Error(`${MANIFEST_REL} is not valid JSON: ${message(err)}`);
|
|
2255
2693
|
}
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2694
|
+
if (typeof raw !== "object" || raw === null) {
|
|
2695
|
+
throw new Error(`${MANIFEST_REL} must be a JSON object`);
|
|
2696
|
+
}
|
|
2697
|
+
const o = raw;
|
|
2698
|
+
const devServer = typeof o.devServer === "string" ? o.devServer : "";
|
|
2699
|
+
const manualSteps = typeof o.manualSteps === "string" ? o.manualSteps : null;
|
|
2700
|
+
if (!devServer.trim() && !manualSteps) {
|
|
2701
|
+
throw new Error(`${MANIFEST_REL} must declare either "devServer" or "manualSteps"`);
|
|
2702
|
+
}
|
|
2703
|
+
const showcaseRaw = typeof o.showcase === "object" && o.showcase !== null ? o.showcase : {};
|
|
2704
|
+
return {
|
|
2705
|
+
version: typeof o.version === "number" ? o.version : 1,
|
|
2706
|
+
appDir: typeof o.appDir === "string" && o.appDir.trim() ? o.appDir : ".",
|
|
2707
|
+
install: stringArray(o.install),
|
|
2708
|
+
devServer,
|
|
2709
|
+
readyPath: typeof o.readyPath === "string" && o.readyPath.trim() ? o.readyPath : "/",
|
|
2710
|
+
...isStringRecord(o.env) ? { env: o.env } : {},
|
|
2711
|
+
uiGlobs: stringArray(o.uiGlobs),
|
|
2712
|
+
showcase: {
|
|
2713
|
+
kind: typeof showcaseRaw.kind === "string" ? showcaseRaw.kind : "custom",
|
|
2714
|
+
instructions: typeof showcaseRaw.instructions === "string" ? showcaseRaw.instructions : ""
|
|
2715
|
+
},
|
|
2716
|
+
manualSteps
|
|
2259
2717
|
};
|
|
2260
|
-
const family = pickFamily(deps);
|
|
2261
|
-
const port = 0;
|
|
2262
|
-
const devCmd = family ? pickDevCmd(family, port) : null;
|
|
2263
|
-
return { pm, family, devCmd };
|
|
2264
2718
|
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2719
|
+
function stringArray(v) {
|
|
2720
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
2721
|
+
}
|
|
2722
|
+
function isStringRecord(v) {
|
|
2723
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && Object.values(v).every((x) => typeof x === "string");
|
|
2724
|
+
}
|
|
2725
|
+
function changedFilesTouchUi(stat, uiGlobs = []) {
|
|
2726
|
+
const files = stat.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
2727
|
+
const regexes = (uiGlobs.length ? uiGlobs : DEFAULT_UI_GLOBS).map(globToRegExp);
|
|
2728
|
+
return files.some((file) => regexes.some((re) => re.test(file)));
|
|
2729
|
+
}
|
|
2730
|
+
function globToRegExp(glob) {
|
|
2731
|
+
let re = "";
|
|
2732
|
+
for (let i = 0; i < glob.length; i++) {
|
|
2733
|
+
const c = glob[i];
|
|
2734
|
+
if (c === "*") {
|
|
2735
|
+
if (glob[i + 1] === "*") {
|
|
2736
|
+
i++;
|
|
2737
|
+
if (glob[i + 1] === "/") {
|
|
2738
|
+
i++;
|
|
2739
|
+
re += "(?:.*/)?";
|
|
2740
|
+
} else {
|
|
2741
|
+
re += ".*";
|
|
2742
|
+
}
|
|
2743
|
+
} else {
|
|
2744
|
+
re += "[^/]*";
|
|
2745
|
+
}
|
|
2746
|
+
} else if (c === "?") {
|
|
2747
|
+
re += "[^/]";
|
|
2748
|
+
} else if (".+^${}()|[]\\".includes(c)) {
|
|
2749
|
+
re += `\\${c}`;
|
|
2750
|
+
} else {
|
|
2751
|
+
re += c;
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
return new RegExp(`^${re}$`);
|
|
2755
|
+
}
|
|
2756
|
+
async function runPreviewPass(ctx, dir, committedFileNames, manifest, config, abort) {
|
|
2757
|
+
const appDir = manifest.appDir === "." ? dir : join5(dir, manifest.appDir);
|
|
2758
|
+
await assertCleanTree(dir);
|
|
2759
|
+
const scratch = mkdtempSync2(join5(tmpdir3(), "flume-showcase-"));
|
|
2760
|
+
const sentinelPath = join5(scratch, ".showcase-path");
|
|
2267
2761
|
try {
|
|
2268
|
-
gitExclude(dir, tmpRoute);
|
|
2269
2762
|
await runClaudeCode({
|
|
2270
2763
|
cwd: dir,
|
|
2271
|
-
prompt: buildPreviewPrompt(ctx, committedFileNames,
|
|
2764
|
+
prompt: buildPreviewPrompt(ctx, committedFileNames, manifest, sentinelPath),
|
|
2272
2765
|
permissionMode: ctx.permissionMode,
|
|
2273
2766
|
abortController: abort,
|
|
2274
2767
|
maxTurns: PREVIEW_MAX_TURNS
|
|
2275
2768
|
});
|
|
2276
|
-
const showcasePath =
|
|
2769
|
+
const showcasePath = readSentinel(sentinelPath);
|
|
2277
2770
|
if (!showcasePath) {
|
|
2278
|
-
throw new Error("preview-ui skill did not
|
|
2279
|
-
}
|
|
2280
|
-
const framework = detectFramework(dir);
|
|
2281
|
-
if (!framework.family) {
|
|
2282
|
-
throw new Error("unsupported framework \u2014 skipping preview");
|
|
2771
|
+
throw new Error("preview-ui skill did not record a showcase URL path \u2014 skipping preview");
|
|
2283
2772
|
}
|
|
2773
|
+
await runInstall(appDir, manifest.install);
|
|
2284
2774
|
const port = await findFreePort();
|
|
2285
|
-
const devServer = await startDevServer(
|
|
2775
|
+
const devServer = await startDevServer(appDir, manifest, port);
|
|
2286
2776
|
try {
|
|
2287
2777
|
const url = `http://localhost:${port}${showcasePath}`;
|
|
2288
2778
|
const shots = await captureWithChromium(url, VIEWPORTS);
|
|
@@ -2293,111 +2783,52 @@ async function runPreviewPass(ctx, dir, committedFileNames, config, abort) {
|
|
|
2293
2783
|
await devServer.stop();
|
|
2294
2784
|
}
|
|
2295
2785
|
} finally {
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
}
|
|
2299
|
-
function pickFamily(deps) {
|
|
2300
|
-
if (deps["next"]) return "next";
|
|
2301
|
-
if (deps["@sveltejs/kit"]) return "svelte-kit";
|
|
2302
|
-
if (deps["nuxt"]) return "nuxt";
|
|
2303
|
-
if (deps["astro"]) return "astro";
|
|
2304
|
-
if (deps["react-scripts"]) return "cra";
|
|
2305
|
-
if (deps["@remix-run/react"] || deps["@remix-run/node"]) return "remix";
|
|
2306
|
-
if (deps["vite"]) {
|
|
2307
|
-
if (deps["vue"]) return "vite-vue";
|
|
2308
|
-
if (deps["svelte"]) return "vite-svelte";
|
|
2309
|
-
if (deps["react"]) return "vite-react";
|
|
2310
|
-
return "vite-react";
|
|
2311
|
-
}
|
|
2312
|
-
return null;
|
|
2313
|
-
}
|
|
2314
|
-
function pickDevCmd(family, port) {
|
|
2315
|
-
switch (family) {
|
|
2316
|
-
case "next":
|
|
2317
|
-
return ["next", "dev", "-p", String(port)];
|
|
2318
|
-
case "vite-react":
|
|
2319
|
-
case "vite-vue":
|
|
2320
|
-
case "vite-svelte":
|
|
2321
|
-
return ["vite", "--port", String(port)];
|
|
2322
|
-
case "nuxt":
|
|
2323
|
-
return ["nuxt", "dev", "--port", String(port)];
|
|
2324
|
-
case "svelte-kit":
|
|
2325
|
-
return ["vite", "--port", String(port)];
|
|
2326
|
-
case "astro":
|
|
2327
|
-
return ["astro", "dev", "--port", String(port)];
|
|
2328
|
-
case "cra":
|
|
2329
|
-
return ["react-scripts", "start"];
|
|
2330
|
-
case "remix":
|
|
2331
|
-
return ["remix", "dev", "--port", String(port)];
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
function mkTempRouteDir(dir) {
|
|
2335
|
-
const suffix = Math.random().toString(36).slice(2, 10);
|
|
2336
|
-
const tmpRoute = join5(dir, `__flumecode_preview_${suffix}`);
|
|
2337
|
-
mkdirSync2(tmpRoute, { recursive: true });
|
|
2338
|
-
return tmpRoute;
|
|
2339
|
-
}
|
|
2340
|
-
function gitExclude(dir, tmpRoute) {
|
|
2341
|
-
const excludePath = join5(dir, ".git", "info", "exclude");
|
|
2342
|
-
const rel = relative(dir, tmpRoute);
|
|
2343
|
-
appendFileSync(excludePath, `
|
|
2344
|
-
${rel}
|
|
2345
|
-
`);
|
|
2786
|
+
await restoreTree(dir);
|
|
2787
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
2788
|
+
}
|
|
2346
2789
|
}
|
|
2347
|
-
function
|
|
2348
|
-
const
|
|
2349
|
-
if (
|
|
2350
|
-
|
|
2351
|
-
|
|
2790
|
+
async function assertCleanTree(dir) {
|
|
2791
|
+
const { stdout: stdout2 } = await exec2("git", ["-C", dir, "status", "--porcelain"]);
|
|
2792
|
+
if (stdout2.trim()) {
|
|
2793
|
+
throw new Error("working tree is not clean before the showcase pass \u2014 refusing to restore");
|
|
2794
|
+
}
|
|
2352
2795
|
}
|
|
2353
|
-
function
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
server.on("error", reject);
|
|
2370
|
-
});
|
|
2796
|
+
async function restoreTree(dir) {
|
|
2797
|
+
try {
|
|
2798
|
+
await exec2("git", ["-C", dir, "checkout", "--", "."]);
|
|
2799
|
+
await exec2("git", ["-C", dir, "clean", "-fd"]);
|
|
2800
|
+
} catch (err) {
|
|
2801
|
+
console.warn(` preview: working-tree restore failed: ${message(err)}`);
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
async function runInstall(appDir, commands) {
|
|
2805
|
+
for (const cmd of commands) {
|
|
2806
|
+
try {
|
|
2807
|
+
await runShell(cmd, appDir, {}, INSTALL_TIMEOUT_MS);
|
|
2808
|
+
} catch (err) {
|
|
2809
|
+
console.warn(` preview: install step "${cmd}" failed: ${message(err)}`);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2371
2812
|
}
|
|
2372
|
-
function startDevServer(
|
|
2813
|
+
function startDevServer(appDir, manifest, port) {
|
|
2373
2814
|
return new Promise((resolve, reject) => {
|
|
2374
|
-
const
|
|
2375
|
-
const
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
cwd: dir,
|
|
2381
|
-
env: { ...process.env, PORT: String(port) },
|
|
2382
|
-
stdio: "ignore"
|
|
2815
|
+
const command2 = manifest.devServer.replaceAll("{PORT}", String(port));
|
|
2816
|
+
const proc = spawn3(command2, {
|
|
2817
|
+
cwd: appDir,
|
|
2818
|
+
shell: true,
|
|
2819
|
+
stdio: "ignore",
|
|
2820
|
+
env: { ...process.env, PORT: String(port), ...manifest.env ?? {} }
|
|
2383
2821
|
});
|
|
2384
2822
|
proc.on("error", reject);
|
|
2385
|
-
const
|
|
2823
|
+
const readyUrl = `http://127.0.0.1:${port}${manifest.readyPath}`;
|
|
2386
2824
|
const deadline = Date.now() + DEV_SERVER_READY_TIMEOUT_MS;
|
|
2825
|
+
const stop = () => new Promise((res) => {
|
|
2826
|
+
proc.kill();
|
|
2827
|
+
proc.on("close", () => res());
|
|
2828
|
+
setTimeout(res, 500);
|
|
2829
|
+
});
|
|
2387
2830
|
const poll = () => {
|
|
2388
|
-
fetch(
|
|
2389
|
-
if (res.ok || res.status < 500) {
|
|
2390
|
-
resolve({
|
|
2391
|
-
stop: () => new Promise((res2) => {
|
|
2392
|
-
proc.kill();
|
|
2393
|
-
proc.on("close", () => res2());
|
|
2394
|
-
setTimeout(res2, 500);
|
|
2395
|
-
})
|
|
2396
|
-
});
|
|
2397
|
-
} else {
|
|
2398
|
-
scheduleNext();
|
|
2399
|
-
}
|
|
2400
|
-
}).catch(() => scheduleNext());
|
|
2831
|
+
fetch(readyUrl).then((r) => r.ok || r.status < 500 ? resolve({ stop }) : scheduleNext()).catch(() => scheduleNext());
|
|
2401
2832
|
};
|
|
2402
2833
|
const scheduleNext = () => {
|
|
2403
2834
|
if (Date.now() > deadline) {
|
|
@@ -2412,6 +2843,50 @@ function startDevServer(dir, framework, port) {
|
|
|
2412
2843
|
setTimeout(poll, 2e3);
|
|
2413
2844
|
});
|
|
2414
2845
|
}
|
|
2846
|
+
function runShell(command2, cwd, env, timeoutMs) {
|
|
2847
|
+
return new Promise((resolve, reject) => {
|
|
2848
|
+
const proc = spawn3(command2, {
|
|
2849
|
+
cwd,
|
|
2850
|
+
shell: true,
|
|
2851
|
+
stdio: "ignore",
|
|
2852
|
+
env: { ...process.env, ...env }
|
|
2853
|
+
});
|
|
2854
|
+
const timer = setTimeout(() => {
|
|
2855
|
+
proc.kill();
|
|
2856
|
+
reject(new Error(`"${command2}" timed out after ${timeoutMs}ms`));
|
|
2857
|
+
}, timeoutMs);
|
|
2858
|
+
proc.on("error", (err) => {
|
|
2859
|
+
clearTimeout(timer);
|
|
2860
|
+
reject(err);
|
|
2861
|
+
});
|
|
2862
|
+
proc.on("close", (code) => {
|
|
2863
|
+
clearTimeout(timer);
|
|
2864
|
+
if (code === 0) resolve();
|
|
2865
|
+
else reject(new Error(`"${command2}" exited with code ${code}`));
|
|
2866
|
+
});
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
function readSentinel(sentinelPath) {
|
|
2870
|
+
if (!existsSync4(sentinelPath)) return null;
|
|
2871
|
+
const content = readFileSync5(sentinelPath, "utf8").trim();
|
|
2872
|
+
return content.length > 0 ? content : null;
|
|
2873
|
+
}
|
|
2874
|
+
function findFreePort() {
|
|
2875
|
+
return new Promise((resolve, reject) => {
|
|
2876
|
+
const server = createServer2();
|
|
2877
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2878
|
+
const addr = server.address();
|
|
2879
|
+
if (!addr || typeof addr === "string") {
|
|
2880
|
+
server.close();
|
|
2881
|
+
reject(new Error("Could not determine free port"));
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
const port = addr.port;
|
|
2885
|
+
server.close((err) => err ? reject(err) : resolve(port));
|
|
2886
|
+
});
|
|
2887
|
+
server.on("error", reject);
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2415
2890
|
async function captureWithChromium(url, viewports) {
|
|
2416
2891
|
const { chromium } = await import("playwright");
|
|
2417
2892
|
const browser = await chromium.launch({ headless: true });
|
|
@@ -2451,6 +2926,9 @@ async function uploadAll(urls, keys, bytes) {
|
|
|
2451
2926
|
})
|
|
2452
2927
|
);
|
|
2453
2928
|
}
|
|
2929
|
+
function message(err) {
|
|
2930
|
+
return err instanceof Error ? err.message : String(err);
|
|
2931
|
+
}
|
|
2454
2932
|
|
|
2455
2933
|
// src/run.ts
|
|
2456
2934
|
var IDLE_MS = 5e3;
|
|
@@ -2775,7 +3253,7 @@ ${reply}`;
|
|
|
2775
3253
|
let preview;
|
|
2776
3254
|
if (outcome.kind !== "none") {
|
|
2777
3255
|
console.log(` \u2026checking UI preview for implement ${ctx.jobId}`);
|
|
2778
|
-
const committedFileNames = await gitCommittedFiles(dir);
|
|
3256
|
+
const committedFileNames = await gitCommittedFiles(ctx, dir);
|
|
2779
3257
|
preview = await buildPreview(ctx, dir, committedFileNames, config, abort);
|
|
2780
3258
|
if (preview.status !== "ready")
|
|
2781
3259
|
console.log(` UI preview ${preview.status}: ${preview.reason}`);
|
|
@@ -2802,7 +3280,7 @@ async function processReviseJob(ctx, dir, resumed, config, abort) {
|
|
|
2802
3280
|
});
|
|
2803
3281
|
const report = result.report ?? void 0;
|
|
2804
3282
|
let reply = (report ? renderReport(report) : result.text.trim()) || "(the agent produced no reply)";
|
|
2805
|
-
if (result.plans?.length) reply = result.plans[0] ?? reply;
|
|
3283
|
+
if (result.plans?.length) reply = result.plans[0]?.body ?? reply;
|
|
2806
3284
|
if (installResult.status === "failed") {
|
|
2807
3285
|
reply += `
|
|
2808
3286
|
|
|
@@ -2842,7 +3320,7 @@ async function processReviseJob(ctx, dir, resumed, config, abort) {
|
|
|
2842
3320
|
let preview;
|
|
2843
3321
|
if (outcome.kind !== "none") {
|
|
2844
3322
|
console.log(` \u2026checking UI preview for revise ${ctx.jobId}`);
|
|
2845
|
-
const committedFileNames = await gitCommittedFiles(dir);
|
|
3323
|
+
const committedFileNames = await gitCommittedFiles(ctx, dir);
|
|
2846
3324
|
preview = await buildPreview(ctx, dir, committedFileNames, config, abort);
|
|
2847
3325
|
if (preview.status !== "ready")
|
|
2848
3326
|
console.log(` UI preview ${preview.status}: ${preview.reason}`);
|
|
@@ -3055,37 +3533,55 @@ function formatJobError(ctx, err) {
|
|
|
3055
3533
|
|
|
3056
3534
|
// src/cli.ts
|
|
3057
3535
|
init_version();
|
|
3058
|
-
import
|
|
3536
|
+
import React6 from "react";
|
|
3059
3537
|
var DEFAULT_SERVER = process.env.FLUME_SERVER || "http://localhost:3000";
|
|
3060
3538
|
async function login(args) {
|
|
3061
3539
|
const flags = parseFlags(args);
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
serverUrl = answer || DEFAULT_SERVER;
|
|
3069
|
-
}
|
|
3070
|
-
if (!token) {
|
|
3071
|
-
token = (await rl.question("Runner token (flrun_\u2026): ")).trim();
|
|
3072
|
-
}
|
|
3073
|
-
rl.close();
|
|
3540
|
+
const serverUrl = (flags.server ?? DEFAULT_SERVER).replace(/\/+$/, "");
|
|
3541
|
+
const explicitRunner = flags.runnerToken ?? flags.token;
|
|
3542
|
+
const explicitCli = flags.cliToken;
|
|
3543
|
+
if (explicitRunner || explicitCli) {
|
|
3544
|
+
saveCredentials(serverUrl, { runnerToken: explicitRunner, cliToken: explicitCli });
|
|
3545
|
+
return;
|
|
3074
3546
|
}
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3547
|
+
const scope = flags.cliOnly ? "cli" : flags.runnerOnly ? "runner" : "cli,runner";
|
|
3548
|
+
console.log(`Logging in to ${serverUrl} (override with --server)
|
|
3549
|
+
`);
|
|
3550
|
+
let result;
|
|
3551
|
+
try {
|
|
3552
|
+
result = await browserLogin({ serverUrl, scope });
|
|
3553
|
+
} catch (err) {
|
|
3554
|
+
console.error(`
|
|
3555
|
+
Login failed: ${err.message}`);
|
|
3556
|
+
console.error("\nOr log in non-interactively with tokens from the web app:");
|
|
3557
|
+
console.error(" flumecode login --server <url> --runner-token flrun_\u2026 --cli-token flcli_\u2026");
|
|
3078
3558
|
process.exit(1);
|
|
3079
3559
|
}
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3560
|
+
saveCredentials(serverUrl, result);
|
|
3561
|
+
}
|
|
3562
|
+
function saveCredentials(serverUrl, creds) {
|
|
3563
|
+
const existing = readConfig();
|
|
3564
|
+
const config = { serverUrl, token: creds.runnerToken ?? existing?.token ?? "" };
|
|
3565
|
+
const apiToken = creds.cliToken ?? existing?.apiToken;
|
|
3566
|
+
if (apiToken) config.apiToken = apiToken;
|
|
3567
|
+
writeConfig(config);
|
|
3568
|
+
const have = [];
|
|
3569
|
+
if (config.token) have.push("runner token");
|
|
3570
|
+
if (config.apiToken) have.push("CLI token");
|
|
3571
|
+
console.log(`Saved ${have.join(" + ") || "server URL"} to ${configPath}.`);
|
|
3572
|
+
if (config.token && config.apiToken) {
|
|
3573
|
+
console.log("Next: `flumecode start --tui` (worker + TUI together on this machine)");
|
|
3574
|
+
} else if (config.apiToken) {
|
|
3575
|
+
console.log("Next: `flumecode tui`");
|
|
3576
|
+
} else if (config.token) {
|
|
3577
|
+
console.log("Next: `flumecode start`");
|
|
3578
|
+
}
|
|
3083
3579
|
}
|
|
3084
3580
|
async function start() {
|
|
3085
3581
|
process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY = "1";
|
|
3086
3582
|
const config = readConfig();
|
|
3087
|
-
if (!config) {
|
|
3088
|
-
console.error("
|
|
3583
|
+
if (!config || !config.token) {
|
|
3584
|
+
console.error("No runner token configured. Run: flumecode login");
|
|
3089
3585
|
process.exit(1);
|
|
3090
3586
|
}
|
|
3091
3587
|
await pollLoop(config);
|
|
@@ -3103,13 +3599,57 @@ async function runTui(args) {
|
|
|
3103
3599
|
writeConfig(config);
|
|
3104
3600
|
const { render } = await import("ink");
|
|
3105
3601
|
const { App: App2 } = await Promise.resolve().then(() => (init_App(), App_exports));
|
|
3106
|
-
render(
|
|
3602
|
+
render(React6.createElement(App2, { config }));
|
|
3603
|
+
}
|
|
3604
|
+
async function startWithTui(args) {
|
|
3605
|
+
process.env.CLAUDE_CODE_DISABLE_AUTO_MEMORY = "1";
|
|
3606
|
+
const flags = parseFlags(args);
|
|
3607
|
+
const existing = readConfig();
|
|
3608
|
+
if (!existing || !existing.token) {
|
|
3609
|
+
console.error("No runner token configured. Run `flumecode login` first.");
|
|
3610
|
+
process.exit(1);
|
|
3611
|
+
}
|
|
3612
|
+
const config = { ...existing };
|
|
3613
|
+
if (flags.server) config.serverUrl = flags.server.replace(/\/+$/, "");
|
|
3614
|
+
if (flags.token) config.apiToken = flags.token;
|
|
3615
|
+
if (!config.apiToken) {
|
|
3616
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
3617
|
+
config.apiToken = (await rl.question("CLI token (flcli_\u2026): ")).trim();
|
|
3618
|
+
rl.close();
|
|
3619
|
+
}
|
|
3620
|
+
writeConfig(config);
|
|
3621
|
+
const logPath = join7(homedir2(), ".flume", "runner.log");
|
|
3622
|
+
const fd = openSync(logPath, "a");
|
|
3623
|
+
const entry = process.argv[1];
|
|
3624
|
+
const worker = entry ? spawn4(process.execPath, [...process.execArgv, entry, "start"], {
|
|
3625
|
+
stdio: ["ignore", fd, fd],
|
|
3626
|
+
env: process.env
|
|
3627
|
+
}) : null;
|
|
3628
|
+
const cleanup2 = () => {
|
|
3629
|
+
try {
|
|
3630
|
+
worker?.kill();
|
|
3631
|
+
} catch {
|
|
3632
|
+
}
|
|
3633
|
+
};
|
|
3634
|
+
process.on("exit", cleanup2);
|
|
3635
|
+
console.log(`Runner worker started in the background (logs: ${logPath}).`);
|
|
3636
|
+
const { render } = await import("ink");
|
|
3637
|
+
const { App: App2 } = await Promise.resolve().then(() => (init_App(), App_exports));
|
|
3638
|
+
const app = render(React6.createElement(App2, { config }));
|
|
3639
|
+
await app.waitUntilExit();
|
|
3640
|
+
cleanup2();
|
|
3641
|
+
process.exit(0);
|
|
3107
3642
|
}
|
|
3108
3643
|
function parseFlags(args) {
|
|
3109
3644
|
const out = {};
|
|
3110
3645
|
for (let i = 0; i < args.length; i++) {
|
|
3111
|
-
|
|
3112
|
-
|
|
3646
|
+
const a = args[i];
|
|
3647
|
+
if (a === "--server") out.server = args[++i];
|
|
3648
|
+
else if (a === "--token") out.token = args[++i];
|
|
3649
|
+
else if (a === "--cli-token") out.cliToken = args[++i];
|
|
3650
|
+
else if (a === "--runner-token") out.runnerToken = args[++i];
|
|
3651
|
+
else if (a === "--cli-only") out.cliOnly = true;
|
|
3652
|
+
else if (a === "--runner-only") out.runnerOnly = true;
|
|
3113
3653
|
}
|
|
3114
3654
|
return out;
|
|
3115
3655
|
}
|
|
@@ -3121,15 +3661,37 @@ if (command === "--version" || command === "-v" || command === "version") {
|
|
|
3121
3661
|
} else if (command === "login") {
|
|
3122
3662
|
void login(rest);
|
|
3123
3663
|
} else if (command === "start") {
|
|
3124
|
-
void
|
|
3664
|
+
if (rest.includes("--tui")) void startWithTui(rest);
|
|
3665
|
+
else void start();
|
|
3125
3666
|
} else if (command === "tui") {
|
|
3126
3667
|
void runTui(rest);
|
|
3127
3668
|
} else {
|
|
3128
3669
|
console.log(`FlumeCode runner v${RUNNER_VERSION}`);
|
|
3129
|
-
console.log("
|
|
3130
|
-
console.log("
|
|
3131
|
-
console.log("
|
|
3132
|
-
console.log("
|
|
3133
|
-
console.log("
|
|
3670
|
+
console.log("");
|
|
3671
|
+
console.log("Usage: flumecode <command> [--server URL] [--token TOKEN]");
|
|
3672
|
+
console.log("");
|
|
3673
|
+
console.log("Commands:");
|
|
3674
|
+
console.log(" login Authorize this machine in the browser (GitHub sign-in), provisioning");
|
|
3675
|
+
console.log(" both the runner token (flrun_\u2026) and CLI token (flcli_\u2026) into config.");
|
|
3676
|
+
console.log(
|
|
3677
|
+
" Flags: --cli-only / --runner-only \xB7 or non-interactive --runner-token /"
|
|
3678
|
+
);
|
|
3679
|
+
console.log(" --cli-token to paste tokens straight from the web app.");
|
|
3680
|
+
console.log(" start Run the worker: claim queued jobs and drive your local Claude Code");
|
|
3681
|
+
console.log(" start --tui All-in-one (single machine): run the worker in the background +");
|
|
3682
|
+
console.log(" the interactive TUI in the foreground");
|
|
3683
|
+
console.log(
|
|
3684
|
+
" tui Open the interactive terminal UI \u2014 talk through a repo, shape a request,"
|
|
3685
|
+
);
|
|
3686
|
+
console.log(
|
|
3687
|
+
" answer the agent's questions, and start implementing. Authenticates as"
|
|
3688
|
+
);
|
|
3689
|
+
console.log(" you with a CLI token (flcli_\u2026) from the web app's Settings.");
|
|
3690
|
+
console.log(" --version Print the runner version");
|
|
3691
|
+
console.log("");
|
|
3692
|
+
console.log(
|
|
3693
|
+
"`start` (the worker) and `tui` (a client) are separate roles with different tokens;"
|
|
3694
|
+
);
|
|
3695
|
+
console.log("`start --tui` just runs both together on one machine. See apps/runner/docs/tui.md.");
|
|
3134
3696
|
process.exit(command ? 1 : 0);
|
|
3135
3697
|
}
|