@flue/cli 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/flue.js +123 -0
  2. package/package.json +2 -2
package/dist/flue.js CHANGED
@@ -6,6 +6,7 @@ import { pathToFileURL } from "node:url";
6
6
  //#region bin/flue.js
7
7
  const OPENCODE_URL = "http://localhost:48765";
8
8
  let openCodeProcess = null;
9
+ let eventStreamAbort = null;
9
10
  function printUsage() {
10
11
  console.error("Usage: flue run <workflowPath> [--args <json>] [--branch <name>] [--model <provider/model>]");
11
12
  }
@@ -70,6 +71,122 @@ function parseModel(modelStr) {
70
71
  modelID: modelStr.slice(slashIndex + 1)
71
72
  };
72
73
  }
74
+ /**
75
+ * Connect to OpenCode's SSE /event endpoint and log events to stderr.
76
+ * Returns an object with an abort() method to close the connection.
77
+ */
78
+ function startEventStream(workdir) {
79
+ const controller = new AbortController();
80
+ const sessionNames = /* @__PURE__ */ new Map();
81
+ const textBuffers = /* @__PURE__ */ new Map();
82
+ _consumeEventStream(controller.signal, workdir, sessionNames, textBuffers).catch((err) => {
83
+ if (err.name !== "AbortError") console.error(`[opencode] event stream error: ${err.message}`);
84
+ });
85
+ return { abort() {
86
+ for (const [sessionId, text] of textBuffers) if (text) {
87
+ const name = sessionNames.get(sessionId) ?? sessionId.slice(0, 12);
88
+ for (const line of text.split("\n")) if (line) console.error(`[opencode] (${name}) > ${line}`);
89
+ }
90
+ controller.abort();
91
+ } };
92
+ }
93
+ async function _consumeEventStream(signal, workdir, sessionNames, textBuffers) {
94
+ const { transformEvent } = await import("@flue/client");
95
+ const url = `${OPENCODE_URL}/event?directory=${encodeURIComponent(workdir)}`;
96
+ const res = await fetch(url, { signal });
97
+ if (!res.ok || !res.body) {
98
+ console.error(`[opencode] failed to connect to event stream (HTTP ${res.status})`);
99
+ return;
100
+ }
101
+ const decoder = new TextDecoder();
102
+ let buffer = "";
103
+ for await (const chunk of res.body) {
104
+ if (signal.aborted) break;
105
+ buffer += decoder.decode(chunk, { stream: true });
106
+ const parts = buffer.split("\n\n");
107
+ buffer = parts.pop() ?? "";
108
+ for (const part of parts) {
109
+ if (!part.trim()) continue;
110
+ const dataLines = [];
111
+ for (const line of part.split("\n")) if (line.startsWith("data: ")) dataLines.push(line.slice(6));
112
+ else if (line.startsWith("data:")) dataLines.push(line.slice(5));
113
+ if (dataLines.length === 0) continue;
114
+ let raw;
115
+ try {
116
+ raw = JSON.parse(dataLines.join("\n"));
117
+ } catch {
118
+ continue;
119
+ }
120
+ if (raw.type === "session.created" || raw.type === "session.updated") {
121
+ const info = raw.properties?.info;
122
+ if (info?.id && info?.title) sessionNames.set(info.id, info.title);
123
+ continue;
124
+ }
125
+ const event = transformEvent(raw);
126
+ if (!event) continue;
127
+ logEvent(event, sessionNames.get(event.sessionId) ?? event.sessionId.slice(0, 12), textBuffers);
128
+ }
129
+ }
130
+ }
131
+ /**
132
+ * Format and log a single FlueEvent to stderr.
133
+ */
134
+ function logEvent(event, sessionName, textBuffers) {
135
+ const prefix = `[opencode] (${sessionName})`;
136
+ switch (event.type) {
137
+ case "tool.pending":
138
+ console.error(`${prefix} tool:pending ${event.tool} — ${event.input}`);
139
+ break;
140
+ case "tool.running":
141
+ flushTextBuffer(textBuffers, event.sessionId, sessionName);
142
+ console.error(`${prefix} tool:running ${event.tool} — ${event.input}`);
143
+ break;
144
+ case "tool.complete": {
145
+ const dur = event.duration ? ` (${(event.duration / 1e3).toFixed(1)}s)` : "";
146
+ console.error(`${prefix} tool:complete ${event.tool}${dur} — ${event.input}`);
147
+ break;
148
+ }
149
+ case "tool.error": {
150
+ const dur = event.duration ? ` (${(event.duration / 1e3).toFixed(1)}s)` : "";
151
+ console.error(`${prefix} tool:error ${event.tool}${dur} — ${event.input} — ${event.error}`);
152
+ break;
153
+ }
154
+ case "text": {
155
+ const lines = ((textBuffers.get(event.sessionId) ?? "") + event.text).split("\n");
156
+ textBuffers.set(event.sessionId, lines.pop() ?? "");
157
+ for (const line of lines) console.error(`${prefix} > ${line}`);
158
+ break;
159
+ }
160
+ case "status":
161
+ flushTextBuffer(textBuffers, event.sessionId, sessionName);
162
+ if (event.message) console.error(`${prefix} status:${event.status} — ${event.message}`);
163
+ else console.error(`${prefix} status:${event.status}`);
164
+ break;
165
+ case "step.start":
166
+ console.error(`${prefix} step:start`);
167
+ break;
168
+ case "step.finish": {
169
+ const tokens = `${event.tokens.input} in / ${event.tokens.output} out`;
170
+ const cost = event.cost > 0 ? `, $${event.cost.toFixed(4)}` : "";
171
+ console.error(`${prefix} step:finish — ${tokens}${cost} — ${event.reason}`);
172
+ break;
173
+ }
174
+ case "error":
175
+ console.error(`${prefix} ERROR: ${event.message}`);
176
+ break;
177
+ }
178
+ }
179
+ /**
180
+ * Flush any remaining buffered text for a session.
181
+ */
182
+ function flushTextBuffer(textBuffers, sessionId, sessionName) {
183
+ const remaining = textBuffers.get(sessionId);
184
+ if (remaining) {
185
+ const prefix = `[opencode] (${sessionName})`;
186
+ console.error(`${prefix} > ${remaining}`);
187
+ textBuffers.set(sessionId, "");
188
+ }
189
+ }
73
190
  async function run() {
74
191
  const { workflowPath, argsJson, branch, model: modelStr } = parseArgs(process.argv.slice(2));
75
192
  const workdir = process.cwd();
@@ -98,6 +215,8 @@ async function run() {
98
215
  }
99
216
  }
100
217
  await preflight(workdir, modelStr);
218
+ const eventStream = startEventStream(workdir);
219
+ eventStreamAbort = eventStream;
101
220
  const model = modelStr ? parseModel(modelStr) : void 0;
102
221
  const flue = new Flue({
103
222
  workdir,
@@ -117,6 +236,8 @@ async function run() {
117
236
  console.error(error instanceof Error ? error.message : String(error));
118
237
  process.exit(1);
119
238
  } finally {
239
+ eventStream.abort();
240
+ eventStreamAbort = null;
120
241
  await flue.close();
121
242
  if (startedOpenCode) stopOpenCodeServer(startedOpenCode);
122
243
  openCodeProcess = null;
@@ -179,10 +300,12 @@ function stopOpenCodeServer(child) {
179
300
  child.kill("SIGTERM");
180
301
  }
181
302
  process.on("SIGINT", () => {
303
+ if (eventStreamAbort) eventStreamAbort.abort();
182
304
  if (openCodeProcess) stopOpenCodeServer(openCodeProcess);
183
305
  process.exit(130);
184
306
  });
185
307
  process.on("SIGTERM", () => {
308
+ if (eventStreamAbort) eventStreamAbort.abort();
186
309
  if (openCodeProcess) stopOpenCodeServer(openCodeProcess);
187
310
  process.exit(143);
188
311
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flue/cli",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "flue": "dist/flue.js"
@@ -13,7 +13,7 @@
13
13
  "check:types": "tsc --noEmit"
14
14
  },
15
15
  "dependencies": {
16
- "@flue/client": "^0.0.5"
16
+ "@flue/client": "^0.0.6"
17
17
  },
18
18
  "devDependencies": {
19
19
  "tsdown": "^0.20.3"