lilflow 0.1.0
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/AGENTS.md +140 -0
- package/README.md +112 -0
- package/package.json +50 -0
- package/src/AGENTS.md +27 -0
- package/src/agents/claude-code.js +352 -0
- package/src/agents/index.js +228 -0
- package/src/agents/ndjson.js +67 -0
- package/src/agents/opencode.js +290 -0
- package/src/agents/prompt.js +91 -0
- package/src/agents/session-store.js +91 -0
- package/src/cli.js +204 -0
- package/src/config/AGENTS.md +23 -0
- package/src/config.js +776 -0
- package/src/init-project.js +573 -0
- package/src/run-workflow.js +6274 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Absolute path to the session store file for a given run.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} cwd - Workflow working directory.
|
|
9
|
+
* @param {string} runId - Workflow run identifier.
|
|
10
|
+
* @returns {string} Session store path.
|
|
11
|
+
*/
|
|
12
|
+
export function getSessionStorePath(cwd, runId) {
|
|
13
|
+
return path.join(cwd, ".flow", "sessions", `${runId}.json`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load the session store for a run. Returns an empty store if the file is absent.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} cwd - Workflow working directory.
|
|
20
|
+
* @param {string} runId - Workflow run identifier.
|
|
21
|
+
* @returns {Promise<{sessions: Record<string, {provider: string, sessionId: string, updatedAt: string}>}>} Parsed store.
|
|
22
|
+
*/
|
|
23
|
+
export async function loadSessionStore(cwd, runId) {
|
|
24
|
+
const storePath = getSessionStorePath(cwd, runId);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const raw = await readFile(storePath, "utf8");
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
|
|
30
|
+
if (parsed && typeof parsed === "object" && parsed.sessions && typeof parsed.sessions === "object") {
|
|
31
|
+
return { sessions: parsed.sessions };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { sessions: {} };
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error.code === "ENOENT") {
|
|
37
|
+
return { sessions: {} };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Look up the prior session ID for a step within a run.
|
|
46
|
+
*
|
|
47
|
+
* @param {{sessions: Record<string, {provider: string, sessionId: string, updatedAt: string}>}} store - Loaded store.
|
|
48
|
+
* @param {string} stepName - Step name used as the session key.
|
|
49
|
+
* @returns {string | null} Session ID or null when absent.
|
|
50
|
+
*/
|
|
51
|
+
export function getSessionId(store, stepName) {
|
|
52
|
+
const entry = store.sessions[stepName];
|
|
53
|
+
|
|
54
|
+
if (!entry || typeof entry.sessionId !== "string" || entry.sessionId === "") {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return entry.sessionId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Persist a session ID captured from an agent result event.
|
|
63
|
+
*
|
|
64
|
+
* Writes atomically via a uuid-suffixed temp file + rename to avoid torn writes
|
|
65
|
+
* when the workflow crashes mid-save.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} cwd - Workflow working directory.
|
|
68
|
+
* @param {string} runId - Workflow run identifier.
|
|
69
|
+
* @param {string} stepName - Step name used as the session key.
|
|
70
|
+
* @param {string} provider - Provider name (e.g. `opencode`, `claude-code`).
|
|
71
|
+
* @param {string} sessionId - Session identifier returned by the agent.
|
|
72
|
+
* @returns {Promise<void>} Resolves when the store is durable on disk.
|
|
73
|
+
*/
|
|
74
|
+
export async function saveSessionId(cwd, runId, stepName, provider, sessionId) {
|
|
75
|
+
const store = await loadSessionStore(cwd, runId);
|
|
76
|
+
|
|
77
|
+
store.sessions[stepName] = {
|
|
78
|
+
provider,
|
|
79
|
+
sessionId,
|
|
80
|
+
updatedAt: new Date().toISOString()
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const storePath = getSessionStorePath(cwd, runId);
|
|
84
|
+
const storeDir = path.dirname(storePath);
|
|
85
|
+
|
|
86
|
+
await mkdir(storeDir, { recursive: true });
|
|
87
|
+
|
|
88
|
+
const tempPath = `${storePath}.${randomUUID()}.tmp`;
|
|
89
|
+
await writeFile(tempPath, `${JSON.stringify(store, null, 2)}\n`, "utf8");
|
|
90
|
+
await rename(tempPath, storePath);
|
|
91
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { getInitHelpText, runInitCommand } from "./init-project.js";
|
|
4
|
+
import {
|
|
5
|
+
ConfigError,
|
|
6
|
+
DEFAULT_CONFIG,
|
|
7
|
+
ensureConfigFile,
|
|
8
|
+
getConfigHelpText,
|
|
9
|
+
getGlobalConfigPath,
|
|
10
|
+
getProjectConfigPath,
|
|
11
|
+
loadConfig,
|
|
12
|
+
parseConfigCommandArgs,
|
|
13
|
+
renderConfig,
|
|
14
|
+
renderConfigWithSources
|
|
15
|
+
} from "./config.js";
|
|
16
|
+
import {
|
|
17
|
+
getListHelpText,
|
|
18
|
+
getLogsHelpText,
|
|
19
|
+
getResumeHelpText,
|
|
20
|
+
getRunHelpText,
|
|
21
|
+
getSetStepHelpText,
|
|
22
|
+
getSignalHelpText,
|
|
23
|
+
runWorkflowListCommand,
|
|
24
|
+
runWorkflowLogsCommand,
|
|
25
|
+
getStatusHelpText,
|
|
26
|
+
runWorkflowCommand,
|
|
27
|
+
runWorkflowResumeCommand,
|
|
28
|
+
runWorkflowSetStepCommand,
|
|
29
|
+
runWorkflowSignalCommand,
|
|
30
|
+
runWorkflowStatusCommand
|
|
31
|
+
} from "./run-workflow.js";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Return the CLI help text for `flow`.
|
|
35
|
+
*
|
|
36
|
+
* @returns {string} Help text shown to users.
|
|
37
|
+
*/
|
|
38
|
+
export function getHelpText() {
|
|
39
|
+
return `flow
|
|
40
|
+
|
|
41
|
+
Usage:
|
|
42
|
+
flow init [--template <name>]
|
|
43
|
+
flow config
|
|
44
|
+
flow run
|
|
45
|
+
flow resume
|
|
46
|
+
flow set-step
|
|
47
|
+
flow signal <run-id> <step-name> [--data '{}']
|
|
48
|
+
flow status
|
|
49
|
+
flow list
|
|
50
|
+
flow logs
|
|
51
|
+
flow --help
|
|
52
|
+
|
|
53
|
+
Commands:
|
|
54
|
+
init Initialize flow in the current repository or from a reusable template
|
|
55
|
+
config Show or initialize flow configuration
|
|
56
|
+
run Execute a workflow from YAML
|
|
57
|
+
resume Resume a failed persisted workflow run
|
|
58
|
+
set-step Manually set which persisted workflow step to resume from
|
|
59
|
+
signal Deliver a signal to a waiting workflow step
|
|
60
|
+
status Show the status of a persisted workflow run
|
|
61
|
+
list List persisted workflow runs
|
|
62
|
+
logs Show persisted logs for a workflow run`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Execute the `flow` command line interface.
|
|
67
|
+
*
|
|
68
|
+
* @param {string[]} argv - Process arguments excluding the Node executable.
|
|
69
|
+
* @param {(message: string) => void} [stdout=console.log] - Standard output writer.
|
|
70
|
+
* @param {(message: string) => void} [stderr=console.error] - Standard error writer.
|
|
71
|
+
* @param {object} [options={}] - Runtime overrides used by tests.
|
|
72
|
+
* @param {string} [options.cwd=process.cwd()] - Working directory override.
|
|
73
|
+
* @param {object} [options.env=process.env] - Environment override.
|
|
74
|
+
* @param {string} [options.homeDir] - Home directory override.
|
|
75
|
+
* @returns {Promise<number>} Process exit code.
|
|
76
|
+
*/
|
|
77
|
+
export async function runCli(argv, stdout = console.log, stderr = console.error, options = {}) {
|
|
78
|
+
const [command, ...commandArgs] = argv;
|
|
79
|
+
const {
|
|
80
|
+
cwd = process.cwd(),
|
|
81
|
+
env = process.env,
|
|
82
|
+
homeDir
|
|
83
|
+
} = options;
|
|
84
|
+
|
|
85
|
+
if (!command || command === "--help" || command === "-h") {
|
|
86
|
+
stdout(getHelpText());
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (command === "config") {
|
|
91
|
+
return runConfigCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (command === "init") {
|
|
95
|
+
return runInitCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (command === "run") {
|
|
99
|
+
return runWorkflowCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (command === "resume") {
|
|
103
|
+
return runWorkflowResumeCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (command === "set-step") {
|
|
107
|
+
return runWorkflowSetStepCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (command === "status") {
|
|
111
|
+
return runWorkflowStatusCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (command === "list") {
|
|
115
|
+
return runWorkflowListCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (command === "logs") {
|
|
119
|
+
return runWorkflowLogsCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (command === "signal") {
|
|
123
|
+
return runWorkflowSignalCommand(commandArgs, stdout, stderr, { cwd, env, homeDir });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
stderr(`Unknown command: ${command}`);
|
|
127
|
+
stderr("Run 'flow --help' to see available commands.");
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Execute `flow config`.
|
|
133
|
+
*
|
|
134
|
+
* @param {string[]} args - CLI args after `config`.
|
|
135
|
+
* @param {(message: string) => void} stdout - Standard output writer.
|
|
136
|
+
* @param {(message: string) => void} stderr - Standard error writer.
|
|
137
|
+
* @param {object} options - Runtime overrides used by tests.
|
|
138
|
+
* @param {string} options.cwd - Working directory override.
|
|
139
|
+
* @param {object} options.env - Environment override.
|
|
140
|
+
* @param {string} [options.homeDir] - Home directory override.
|
|
141
|
+
* @returns {Promise<number>} Process exit code.
|
|
142
|
+
*/
|
|
143
|
+
async function runConfigCommand(args, stdout, stderr, options) {
|
|
144
|
+
try {
|
|
145
|
+
const { commandFlags, overrideFlags } = parseConfigCommandArgs(args);
|
|
146
|
+
|
|
147
|
+
if (commandFlags.help) {
|
|
148
|
+
stdout(getConfigHelpText());
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (commandFlags.defaults) {
|
|
153
|
+
stdout(renderConfig(DEFAULT_CONFIG));
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (commandFlags.global) {
|
|
158
|
+
const globalConfigPath = getGlobalConfigPath(options.homeDir);
|
|
159
|
+
const created = await ensureConfigFile(globalConfigPath);
|
|
160
|
+
stdout(`${created ? "Created" : "Global config already exists at"} ${globalConfigPath}`);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (commandFlags.project) {
|
|
165
|
+
const projectConfigPath = getProjectConfigPath(options.cwd);
|
|
166
|
+
const created = await ensureConfigFile(projectConfigPath);
|
|
167
|
+
stdout(`${created ? "Created" : "Project config already exists at"} ${projectConfigPath}`);
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const { config, sources } = await loadConfig({
|
|
172
|
+
cwd: options.cwd,
|
|
173
|
+
env: options.env,
|
|
174
|
+
flags: overrideFlags,
|
|
175
|
+
homeDir: options.homeDir
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
stdout(commandFlags.show ? renderConfigWithSources(config, sources) : renderConfig(config));
|
|
179
|
+
return 0;
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (error instanceof ConfigError) {
|
|
182
|
+
stderr(`Error: ${error.message}`);
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export {
|
|
191
|
+
getInitHelpText,
|
|
192
|
+
getListHelpText,
|
|
193
|
+
getLogsHelpText,
|
|
194
|
+
getResumeHelpText,
|
|
195
|
+
getRunHelpText,
|
|
196
|
+
getSetStepHelpText,
|
|
197
|
+
getSignalHelpText,
|
|
198
|
+
getStatusHelpText
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
202
|
+
const exitCode = await runCli(process.argv.slice(2));
|
|
203
|
+
process.exit(exitCode);
|
|
204
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# src/config
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Contains flow configuration loading, validation, rendering, and file initialization.
|
|
6
|
+
|
|
7
|
+
## Modules
|
|
8
|
+
|
|
9
|
+
- `../config.js`: config defaults, parsing, merge precedence, and `flow config` helpers
|
|
10
|
+
|
|
11
|
+
## Conventions
|
|
12
|
+
|
|
13
|
+
- Keep the precedence order exact: defaults, global, project, workflow, workflow-local, env, flag
|
|
14
|
+
- Validate both keys and values before merging user-provided config
|
|
15
|
+
- Keep YAML output stable for tests and proof artifacts
|
|
16
|
+
- Prefer explicit supported-key handling over generic deep merges
|
|
17
|
+
|
|
18
|
+
## Verification
|
|
19
|
+
|
|
20
|
+
- Run `npm test`
|
|
21
|
+
- Run `npm run coverage`
|
|
22
|
+
- Run `npx eslint src/ --fix`
|
|
23
|
+
- Run `npx eslint src/`
|