@flue/client 0.0.5 → 0.0.7
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/dist/index.d.mts +58 -1
- package/dist/index.mjs +172 -37
- package/package.json +27 -27
package/dist/index.d.mts
CHANGED
|
@@ -15,6 +15,63 @@ declare class SkillOutputError extends Error {
|
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
//#endregion
|
|
18
|
+
//#region src/events.d.ts
|
|
19
|
+
/**
|
|
20
|
+
* Flue event types and transform utilities.
|
|
21
|
+
*
|
|
22
|
+
* Converts raw OpenCode SSE events into a stable, simplified Flue event
|
|
23
|
+
* format suitable for logging and streaming to external consumers.
|
|
24
|
+
*/
|
|
25
|
+
type FlueEvent = {
|
|
26
|
+
timestamp: number;
|
|
27
|
+
sessionId: string;
|
|
28
|
+
} & ({
|
|
29
|
+
type: 'tool.pending';
|
|
30
|
+
tool: string;
|
|
31
|
+
input: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'tool.running';
|
|
34
|
+
tool: string;
|
|
35
|
+
input: string;
|
|
36
|
+
} | {
|
|
37
|
+
type: 'tool.complete';
|
|
38
|
+
tool: string;
|
|
39
|
+
input: string;
|
|
40
|
+
output: string;
|
|
41
|
+
duration: number;
|
|
42
|
+
} | {
|
|
43
|
+
type: 'tool.error';
|
|
44
|
+
tool: string;
|
|
45
|
+
input: string;
|
|
46
|
+
error: string;
|
|
47
|
+
duration: number;
|
|
48
|
+
} | {
|
|
49
|
+
type: 'text';
|
|
50
|
+
text: string;
|
|
51
|
+
} | {
|
|
52
|
+
type: 'status';
|
|
53
|
+
status: 'busy' | 'idle' | 'compacted' | 'retry';
|
|
54
|
+
message?: string;
|
|
55
|
+
} | {
|
|
56
|
+
type: 'step.start';
|
|
57
|
+
} | {
|
|
58
|
+
type: 'step.finish';
|
|
59
|
+
reason: string;
|
|
60
|
+
tokens: {
|
|
61
|
+
input: number;
|
|
62
|
+
output: number;
|
|
63
|
+
};
|
|
64
|
+
cost: number;
|
|
65
|
+
} | {
|
|
66
|
+
type: 'error';
|
|
67
|
+
message: string;
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* Attempt to parse a raw OpenCode SSE event and convert it to a FlueEvent.
|
|
71
|
+
* Returns null if the event should be filtered out (not relevant for logs).
|
|
72
|
+
*/
|
|
73
|
+
declare function transformEvent(raw: any): FlueEvent | null;
|
|
74
|
+
//#endregion
|
|
18
75
|
//#region src/types.d.ts
|
|
19
76
|
interface FlueOptions {
|
|
20
77
|
/** OpenCode server URL (default: 'http://localhost:48765'). */
|
|
@@ -101,4 +158,4 @@ declare class Flue {
|
|
|
101
158
|
close(): Promise<void>;
|
|
102
159
|
}
|
|
103
160
|
//#endregion
|
|
104
|
-
export { Flue, type FlueOptions, type PromptOptions, type ShellOptions, type ShellResult, type SkillOptions, SkillOutputError };
|
|
161
|
+
export { Flue, type FlueEvent, type FlueOptions, type PromptOptions, type ShellOptions, type ShellResult, type SkillOptions, SkillOutputError, transformEvent };
|
package/dist/index.mjs
CHANGED
|
@@ -20,6 +20,148 @@ var SkillOutputError = class extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/events.ts
|
|
25
|
+
/**
|
|
26
|
+
* Summarize tool input into a short human-readable string.
|
|
27
|
+
*/
|
|
28
|
+
function summarizeInput(tool, input) {
|
|
29
|
+
if (input.command) return String(input.command).slice(0, 500);
|
|
30
|
+
if (input.filePath) return String(input.filePath);
|
|
31
|
+
if (input.pattern) return String(input.pattern);
|
|
32
|
+
if (input.url) return String(input.url);
|
|
33
|
+
if (input.name) return String(input.name);
|
|
34
|
+
return tool;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Attempt to parse a raw OpenCode SSE event and convert it to a FlueEvent.
|
|
38
|
+
* Returns null if the event should be filtered out (not relevant for logs).
|
|
39
|
+
*/
|
|
40
|
+
function transformEvent(raw) {
|
|
41
|
+
const type = raw?.type;
|
|
42
|
+
if (!type) return null;
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
if (type === "message.part.updated") {
|
|
45
|
+
const part = raw.properties?.part;
|
|
46
|
+
if (!part) return null;
|
|
47
|
+
const sessionId = part.sessionID ?? "";
|
|
48
|
+
if (part.type === "tool") {
|
|
49
|
+
const tool = part.tool ?? "?";
|
|
50
|
+
const state = part.state;
|
|
51
|
+
if (!state) return null;
|
|
52
|
+
const input = summarizeInput(tool, state.input ?? {});
|
|
53
|
+
switch (state.status) {
|
|
54
|
+
case "pending": return {
|
|
55
|
+
timestamp: now,
|
|
56
|
+
sessionId,
|
|
57
|
+
type: "tool.pending",
|
|
58
|
+
tool,
|
|
59
|
+
input
|
|
60
|
+
};
|
|
61
|
+
case "running": return {
|
|
62
|
+
timestamp: now,
|
|
63
|
+
sessionId,
|
|
64
|
+
type: "tool.running",
|
|
65
|
+
tool,
|
|
66
|
+
input
|
|
67
|
+
};
|
|
68
|
+
case "completed": return {
|
|
69
|
+
timestamp: now,
|
|
70
|
+
sessionId,
|
|
71
|
+
type: "tool.complete",
|
|
72
|
+
tool,
|
|
73
|
+
input,
|
|
74
|
+
output: (state.output ?? "").slice(0, 1e3),
|
|
75
|
+
duration: state.time?.end && state.time?.start ? state.time.end - state.time.start : 0
|
|
76
|
+
};
|
|
77
|
+
case "error": {
|
|
78
|
+
const duration = state.time?.end && state.time?.start ? state.time.end - state.time.start : 0;
|
|
79
|
+
return {
|
|
80
|
+
timestamp: now,
|
|
81
|
+
sessionId,
|
|
82
|
+
type: "tool.error",
|
|
83
|
+
tool,
|
|
84
|
+
input,
|
|
85
|
+
error: state.error ?? "unknown error",
|
|
86
|
+
duration
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (part.type === "text") {
|
|
93
|
+
const delta = raw.properties?.delta;
|
|
94
|
+
if (delta) return {
|
|
95
|
+
timestamp: now,
|
|
96
|
+
sessionId,
|
|
97
|
+
type: "text",
|
|
98
|
+
text: delta
|
|
99
|
+
};
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
if (part.type === "step-start") return {
|
|
103
|
+
timestamp: now,
|
|
104
|
+
sessionId: part.sessionID ?? "",
|
|
105
|
+
type: "step.start"
|
|
106
|
+
};
|
|
107
|
+
if (part.type === "step-finish") return {
|
|
108
|
+
timestamp: now,
|
|
109
|
+
sessionId: part.sessionID ?? "",
|
|
110
|
+
type: "step.finish",
|
|
111
|
+
reason: part.reason ?? "",
|
|
112
|
+
tokens: {
|
|
113
|
+
input: part.tokens?.input ?? 0,
|
|
114
|
+
output: part.tokens?.output ?? 0
|
|
115
|
+
},
|
|
116
|
+
cost: part.cost ?? 0
|
|
117
|
+
};
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (type === "session.status") {
|
|
121
|
+
const sessionId = raw.properties?.sessionID ?? "";
|
|
122
|
+
const status = raw.properties?.status;
|
|
123
|
+
if (status?.type === "busy") return {
|
|
124
|
+
timestamp: now,
|
|
125
|
+
sessionId,
|
|
126
|
+
type: "status",
|
|
127
|
+
status: "busy"
|
|
128
|
+
};
|
|
129
|
+
if (status?.type === "idle") return {
|
|
130
|
+
timestamp: now,
|
|
131
|
+
sessionId,
|
|
132
|
+
type: "status",
|
|
133
|
+
status: "idle"
|
|
134
|
+
};
|
|
135
|
+
if (status?.type === "retry") return {
|
|
136
|
+
timestamp: now,
|
|
137
|
+
sessionId,
|
|
138
|
+
type: "status",
|
|
139
|
+
status: "retry",
|
|
140
|
+
message: status.message
|
|
141
|
+
};
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (type === "session.idle") return {
|
|
145
|
+
timestamp: now,
|
|
146
|
+
sessionId: raw.properties?.sessionID ?? "",
|
|
147
|
+
type: "status",
|
|
148
|
+
status: "idle"
|
|
149
|
+
};
|
|
150
|
+
if (type === "session.compacted") return {
|
|
151
|
+
timestamp: now,
|
|
152
|
+
sessionId: raw.properties?.sessionID ?? "",
|
|
153
|
+
type: "status",
|
|
154
|
+
status: "compacted"
|
|
155
|
+
};
|
|
156
|
+
if (type === "session.error") return {
|
|
157
|
+
timestamp: now,
|
|
158
|
+
sessionId: raw.properties?.sessionID ?? "",
|
|
159
|
+
type: "error",
|
|
160
|
+
message: raw.properties?.error ?? "unknown error"
|
|
161
|
+
};
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
23
165
|
//#endregion
|
|
24
166
|
//#region src/shell.ts
|
|
25
167
|
async function runShell(command, options) {
|
|
@@ -49,12 +191,6 @@ async function runShell(command, options) {
|
|
|
49
191
|
//#endregion
|
|
50
192
|
//#region src/prompt.ts
|
|
51
193
|
/**
|
|
52
|
-
* Checks if a Valibot schema represents a plain string type.
|
|
53
|
-
*/
|
|
54
|
-
function isStringSchema(schema) {
|
|
55
|
-
return schema.type === "string";
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
194
|
* Checks if a skill name is a file path (contains '/' or ends with '.md').
|
|
59
195
|
*/
|
|
60
196
|
function isFilePath(name) {
|
|
@@ -80,10 +216,9 @@ function buildSkillPrompt(name, args, schema) {
|
|
|
80
216
|
isFilePath(name) ? `Read and use the .opencode/skills/${name} skill.` : `Use the ${name} skill.`
|
|
81
217
|
];
|
|
82
218
|
if (args && Object.keys(args).length > 0) parts.push(`\nArguments:\n${JSON.stringify(args, null, 2)}`);
|
|
83
|
-
if (schema)
|
|
84
|
-
else {
|
|
219
|
+
if (schema) {
|
|
85
220
|
const { $schema: _, ...schemaWithoutMeta } = toJsonSchema(schema, { errorMode: "ignore" });
|
|
86
|
-
parts.push("\nWhen complete, output your result between these exact delimiters
|
|
221
|
+
parts.push("\nWhen complete, you MUST output your result between these exact delimiters conforming to this schema:", "```json", JSON.stringify(schemaWithoutMeta, null, 2), "```", "", "Example: (Object)", "---RESULT_START---", "{\"key\": \"value\"}", "---RESULT_END---", "", "Example: (String)", "---RESULT_START---", "Hello, world!", "---RESULT_END---");
|
|
87
222
|
}
|
|
88
223
|
return parts.join("\n");
|
|
89
224
|
}
|
|
@@ -113,35 +248,10 @@ function extractResult(parts, schema, sessionId) {
|
|
|
113
248
|
rawOutput: allText
|
|
114
249
|
});
|
|
115
250
|
}
|
|
116
|
-
|
|
117
|
-
if (schema.type === "string") {
|
|
118
|
-
const parseResult = v.safeParse(schema, resultBlock);
|
|
119
|
-
if (!parseResult.success) {
|
|
120
|
-
console.error("[flue] extractResult: string validation failed", parseResult.issues);
|
|
121
|
-
throw new SkillOutputError("Result validation failed for string schema.", {
|
|
122
|
-
sessionId,
|
|
123
|
-
rawOutput: resultBlock,
|
|
124
|
-
validationErrors: parseResult.issues
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
console.log(`[flue] extractResult: validated string result (${resultBlock.length} chars)`);
|
|
128
|
-
return parseResult.output;
|
|
129
|
-
}
|
|
130
|
-
let parsed;
|
|
131
|
-
try {
|
|
132
|
-
parsed = JSON.parse(resultBlock);
|
|
133
|
-
} catch (err) {
|
|
134
|
-
console.error(`[flue] extractResult: JSON parse failed for block: ${resultBlock.slice(0, 200)}`);
|
|
135
|
-
throw new SkillOutputError("Failed to parse result block as JSON.", {
|
|
136
|
-
sessionId,
|
|
137
|
-
rawOutput: resultBlock,
|
|
138
|
-
validationErrors: err
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
const parseResult = v.safeParse(schema, parsed);
|
|
251
|
+
const parseResult = v.safeParse(schema, resultBlock);
|
|
142
252
|
if (!parseResult.success) {
|
|
143
253
|
console.error("[flue] extractResult: schema validation failed", parseResult.issues);
|
|
144
|
-
console.error("[flue] extractResult: parsed value was:", JSON.stringify(
|
|
254
|
+
console.error("[flue] extractResult: parsed value was:", JSON.stringify(resultBlock));
|
|
145
255
|
throw new SkillOutputError("Result does not match the expected schema.", {
|
|
146
256
|
sessionId,
|
|
147
257
|
rawOutput: resultBlock,
|
|
@@ -208,6 +318,7 @@ async function runSkill(client, workdir, name, options) {
|
|
|
208
318
|
data: asyncResult.data
|
|
209
319
|
});
|
|
210
320
|
if (asyncResult.error) throw new Error(`Failed to send prompt for skill "${name}" (session ${sessionId}): ${JSON.stringify(asyncResult.error)}`);
|
|
321
|
+
await confirmSessionStarted(client, sessionId, workdir, name);
|
|
211
322
|
console.log(`[flue] skill("${name}"): starting polling`);
|
|
212
323
|
const parts = await pollUntilIdle(client, sessionId, workdir, name, promptStart);
|
|
213
324
|
const promptElapsed = ((Date.now() - promptStart) / 1e3).toFixed(1);
|
|
@@ -215,6 +326,30 @@ async function runSkill(client, workdir, name, options) {
|
|
|
215
326
|
if (!schema) return;
|
|
216
327
|
return extractResult(parts, schema, sessionId);
|
|
217
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* After promptAsync, confirm that OpenCode actually started processing the session.
|
|
331
|
+
* Polls quickly (1s) to detect the session appearing as "busy" or a user message being recorded.
|
|
332
|
+
* Fails fast (~15s) instead of letting the poll loop run for 5 minutes.
|
|
333
|
+
*/
|
|
334
|
+
async function confirmSessionStarted(client, sessionId, workdir, skillName) {
|
|
335
|
+
const maxAttempts = 15;
|
|
336
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
337
|
+
await sleep(1e3);
|
|
338
|
+
if (((await client.session.status({ query: { directory: workdir } })).data?.[sessionId])?.type === "busy") {
|
|
339
|
+
console.log(`[flue] skill("${skillName}"): session confirmed running`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const messages = (await client.session.messages({
|
|
343
|
+
path: { id: sessionId },
|
|
344
|
+
query: { directory: workdir }
|
|
345
|
+
})).data;
|
|
346
|
+
if (messages && messages.length > 0) {
|
|
347
|
+
console.log(`[flue] skill("${skillName}"): session confirmed (${messages.length} messages)`);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
throw new Error(`Skill "${skillName}" failed to start: session ${sessionId} has no messages after 15s.\nThe prompt was accepted but OpenCode never began processing it.\nThis usually means no model is configured. Pass --model to the flue CLI or set "model" in opencode.json.`);
|
|
352
|
+
}
|
|
218
353
|
async function pollUntilIdle(client, sessionId, workdir, skillName, startTime) {
|
|
219
354
|
let emptyPolls = 0;
|
|
220
355
|
let pollCount = 0;
|
|
@@ -329,4 +464,4 @@ var Flue = class {
|
|
|
329
464
|
};
|
|
330
465
|
|
|
331
466
|
//#endregion
|
|
332
|
-
export { Flue, SkillOutputError };
|
|
467
|
+
export { Flue, SkillOutputError, transformEvent };
|
package/package.json
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
2
|
+
"name": "@flue/client",
|
|
3
|
+
"version": "0.0.7",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.mts",
|
|
8
|
+
"import": "./dist/index.mjs"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.mjs",
|
|
12
|
+
"types": "./dist/index.d.mts",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@opencode-ai/sdk": "^1.1.56",
|
|
18
|
+
"@valibot/to-json-schema": "^1.0.0",
|
|
19
|
+
"valibot": "^1.0.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"tsdown": "^0.20.3"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsdown",
|
|
26
|
+
"check:types": "tsc --noEmit"
|
|
27
|
+
}
|
|
28
|
+
}
|