lilflow 0.1.1 → 0.2.1

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.
@@ -0,0 +1,152 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { resolveAgentBinary } from "./agents/index.js";
5
+ import { buildSessionSystemPrompt } from "./session-prompt.js";
6
+
7
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ /**
10
+ * Path to the bundled plugin that exposes the lilflow skill. The plugin lives
11
+ * at the repo/package root — `.claude-plugin/plugin.json` is the marker file,
12
+ * sibling `skills/`, `commands/`, etc. are auto-discovered. Works for both
13
+ * providers: Claude Code loads it natively, and OpenCode picks it up through
14
+ * oh-my-opencode's Claude Code compatibility layer.
15
+ *
16
+ * @returns {string} Absolute directory path (repo/package root).
17
+ */
18
+ export function getBundledPluginPath() {
19
+ return path.resolve(HERE, "..");
20
+ }
21
+
22
+ /**
23
+ * Execute a workflow in session mode. Spawns one long-lived agent process,
24
+ * injects the workflow-aware system prompt, and lets the agent drive the bridge.
25
+ *
26
+ * @param {object} options - Runtime options.
27
+ * @param {object} options.workflow - Loaded workflow with `mode: "session"`.
28
+ * @param {string} options.workflowPath - Absolute workflow path.
29
+ * @param {string} options.cwd - Working directory.
30
+ * @param {object} options.env - Process environment.
31
+ * @param {(message: string) => void} options.stdout - Output writer.
32
+ * @param {(message: string) => void} options.stderr - Error writer.
33
+ * @param {object} options.flowConfig - Resolved flow config.
34
+ * @param {{runId: string}} options.eventLogger - Already-initialized logger.
35
+ * @param {typeof spawn} [options.spawnProcess=spawn] - Injection for tests.
36
+ * @returns {Promise<{exitCode: number, runId: string}>} Session result.
37
+ */
38
+ export async function executeSessionWorkflow(options) {
39
+ const {
40
+ workflow,
41
+ workflowPath,
42
+ cwd,
43
+ env,
44
+ stdout,
45
+ stderr,
46
+ flowConfig,
47
+ eventLogger,
48
+ spawnProcess = spawn
49
+ } = options;
50
+
51
+ if (workflow.mode !== "session") {
52
+ throw new Error("executeSessionWorkflow requires workflow.mode === 'session'.");
53
+ }
54
+
55
+ if (!workflow.session || (workflow.session.provider !== "claude-code" && workflow.session.provider !== "opencode")) {
56
+ throw new Error("Session mode supports provider 'claude-code' or 'opencode'.");
57
+ }
58
+
59
+ const runId = eventLogger.runId;
60
+ const systemPrompt = buildSessionSystemPrompt(workflow, runId);
61
+ const pluginPath = getBundledPluginPath();
62
+
63
+ const childEnv = {
64
+ ...env,
65
+ FLOW_RUN_ID: runId,
66
+ FLOW_WORKFLOW_PATH: workflowPath,
67
+ // Both providers recognize CLAUDE_PLUGINS_PATH when oh-my-opencode loads
68
+ // Claude Code plugins.
69
+ CLAUDE_PLUGINS_PATH: [pluginPath, env.CLAUDE_PLUGINS_PATH].filter(Boolean).join(path.delimiter)
70
+ };
71
+
72
+ const provider = workflow.session.provider;
73
+ const { binary, args, interactive } = provider === "claude-code"
74
+ ? await buildClaudeArgs(workflow, systemPrompt, flowConfig, env)
75
+ : await buildOpencodeArgs(workflow, systemPrompt, flowConfig, env);
76
+
77
+ stdout(`Starting session-mode workflow '${workflow.name}' (run ${runId}, provider ${provider}).`);
78
+
79
+ if (provider === "opencode" && interactive) {
80
+ // OpenCode does not expose a --append-system-prompt flag, so we surface the
81
+ // system prompt to the user before the TUI takes over. The user pastes it
82
+ // (or the skill picks it up from FLOW_RUN_ID and re-fetches workflow state).
83
+ stdout("");
84
+ stdout("=== Workflow system prompt (paste into opencode if not auto-loaded) ===");
85
+ stdout(systemPrompt);
86
+ stdout("=== End system prompt ===");
87
+ stdout("");
88
+ }
89
+
90
+ return new Promise((resolve, reject) => {
91
+ const child = spawnProcess(binary, args, {
92
+ cwd,
93
+ env: childEnv,
94
+ stdio: "inherit"
95
+ });
96
+
97
+ child.on("error", (error) => {
98
+ reject(new Error(`Failed to start ${provider} agent: ${error.message}`));
99
+ });
100
+
101
+ child.on("close", (code, signal) => {
102
+ if (signal) {
103
+ stderr(`${provider} agent exited due to signal ${signal}`);
104
+ }
105
+ resolve({ exitCode: code ?? 1, runId });
106
+ });
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Build claude CLI invocation arguments.
112
+ */
113
+ async function buildClaudeArgs(workflow, systemPrompt, flowConfig, env) {
114
+ const binary = await resolveAgentBinary("claude-code", flowConfig, env);
115
+ const args = [];
116
+
117
+ if (workflow.session.model) {
118
+ args.push("--model", workflow.session.model);
119
+ }
120
+
121
+ if (Array.isArray(workflow.session.allowTools) && workflow.session.allowTools.length > 0) {
122
+ args.push("--allowedTools", ...workflow.session.allowTools);
123
+ }
124
+
125
+ args.push("--append-system-prompt", systemPrompt);
126
+ args.push("--dangerously-skip-permissions");
127
+
128
+ return { binary, args, interactive: true };
129
+ }
130
+
131
+ /**
132
+ * Build opencode CLI invocation arguments. Opencode has no
133
+ * `--append-system-prompt` equivalent — the system prompt is surfaced to the
134
+ * user above the TUI and the skill (loaded via oh-my-opencode's Claude Code
135
+ * compatibility layer) picks up the rest from FLOW_RUN_ID.
136
+ */
137
+ async function buildOpencodeArgs(workflow, _systemPrompt, flowConfig, env) {
138
+ const binary = await resolveAgentBinary("opencode", flowConfig, env);
139
+ const args = [];
140
+
141
+ if (workflow.session.model) {
142
+ args.push("--model", workflow.session.model);
143
+ }
144
+
145
+ if (Array.isArray(workflow.session.plugins)) {
146
+ for (const plugin of workflow.session.plugins) {
147
+ args.push("--plugin", plugin);
148
+ }
149
+ }
150
+
151
+ return { binary, args, interactive: true };
152
+ }