@operator-labs/operator-cli 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/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # @operator-labs/operator-cli
2
+
3
+ Self-calling workflow primitives for [OpenClaw](https://github.com/openclaw/openclaw). A Node.js script controls the flow; `invoke()` spawns agent sessions when intelligence is needed.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @operator-labs/operator-cli
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ ```bash
14
+ operator setup
15
+ ```
16
+
17
+ Detects your `openclaw.json`, enables hooks, generates tokens, and enables the `llm-task` plugin. Safe to run multiple times. Use `--dry-run` to preview.
18
+
19
+ ```
20
+ $ operator setup
21
+ config: ~/.openclaw/openclaw.json
22
+ hooks.enabled: true (already set)
23
+ hooks.token: exists
24
+ hooks.mappings: operator mapping exists
25
+ llm-task plugin: enabled
26
+ tools.allow: llm-task present
27
+ gateway.auth.token: found
28
+
29
+ ready (no changes needed)
30
+ ```
31
+
32
+ ## CLI
33
+
34
+ ```bash
35
+ operator-cli setup # configure gateway
36
+ operator-cli setup --dry-run # preview changes
37
+ operator-cli examples # list example workflows
38
+ operator-cli examples batch-analyze # view a specific example
39
+ ```
40
+
41
+ ## How it works
42
+
43
+ ```js
44
+ import { invoke, think } from "@operator-labs/operator-cli";
45
+
46
+ await invoke("do step 1"); // → fresh agent session (full tools)
47
+ // poll for results
48
+ const r = await think("classify this"); // → llm-task (JSON, no tools)
49
+ // branch based on result
50
+ await invoke("do step 2"); // → fresh agent session
51
+ ```
52
+
53
+ The script is deterministic. It handles control flow, loops, conditionals, data reading. Each `invoke()` spawns an isolated agent session with fresh context. Each `think()` calls the LLM without a session for lightweight reasoning.
54
+
55
+ ## Examples
56
+
57
+ ```bash
58
+ operator examples # list available examples
59
+ operator examples batch-analyze # view a specific example
60
+ ```
61
+
62
+ Three reference workflows are included:
63
+
64
+ - **batch-analyze** — process N items with checkpointing and resume
65
+ - **research-pipeline** — sequential gather → analyze → synthesize
66
+ - **monitor-and-act** — conditional branching with CSV state
67
+
68
+ ## JS API
69
+
70
+ ```js
71
+ import { invoke, think } from "@operator-labs/operator-cli";
72
+
73
+ await invoke("Analyze example.com, write to /tmp/report.md");
74
+ const result = await think("What is 2+2? Return {answer: number}");
75
+ ```
76
+
77
+ After `operator-cli setup`, imports resolve from any script under `~/.openclaw/`.
78
+
79
+ ## Safety
80
+
81
+ `invoke()` prepends safety rules by default to prevent spawned sessions from writing to `~/.openclaw/skills/`, `~/.openclaw/hooks/`, or `openclaw.json`. This blocks self-replicating skill creation.
82
+
83
+ Disable with `invoke(message, { safety: false })`.
84
+
85
+ See [references/safety.md](references/safety.md) for details.
86
+
87
+ ## Reference docs
88
+
89
+ - [references/patterns.md](references/patterns.md) — batch, pipeline, fan-out patterns
90
+ - [references/error-handling.md](references/error-handling.md) — retries, timeouts, checkpoints
91
+ - [references/safety.md](references/safety.md) — safety rules and limitations
package/SKILL.md ADDED
@@ -0,0 +1,172 @@
1
+ ---
2
+ name: operator
3
+ description: Spawn autonomous agent sessions from workflow scripts. Use when a workflow needs the agent to reason, use tools, browse, or write files at each step.
4
+ license: Apache-2.0
5
+ compatibility: Requires Node.js 18+ and OpenClaw.
6
+ metadata:
7
+ author: Operator Labs
8
+ version: "2.0"
9
+ ---
10
+
11
+ # Operator
12
+
13
+ Two primitives for workflow scripts:
14
+
15
+ - `invoke()` spawns a full agent session (tools, browser, filesystem)
16
+ - `think()` calls the LLM directly (no session, returns JSON)
17
+
18
+ Use invoke when the step needs to act. Use think when the step only needs reasoning.
19
+
20
+ ## Setup
21
+
22
+ ```bash
23
+ operator-cli setup
24
+ ```
25
+
26
+ Or configure manually -- see [references/setup.md](references/setup.md).
27
+
28
+ ## Invoke
29
+
30
+ ```js
31
+ import { invoke } from "@operator-labs/operator-cli";
32
+
33
+ await invoke("Analyze example.com, write findings to /tmp/report.md");
34
+ ```
35
+
36
+ **Returns immediately.** The agent runs in the background. Your script must
37
+ poll for results -- a file, a database row, an API response, anything.
38
+
39
+ ```js
40
+ await invoke("Write report to /tmp/report.md");
41
+
42
+ while (!existsSync("/tmp/report.md")) {
43
+ await new Promise(r => setTimeout(r, 5000));
44
+ }
45
+ ```
46
+
47
+ Safety rules are prepended by default. Pass `{ safety: false }` if the user
48
+ explicitly requests unrestricted access.
49
+
50
+ ## Think
51
+
52
+ ```js
53
+ import { think } from "@operator-labs/operator-cli";
54
+
55
+ const result = await think("What is 2+2? Return {answer: number}");
56
+ ```
57
+
58
+ Synchronous (request/response). No agent session. Returns JSON. Accepts a
59
+ plain string or a JSON object with llm-task args (`prompt`, `input`, `schema`).
60
+
61
+ ## Writing Workflows
62
+
63
+ A workflow script is deterministic Node.js that calls `invoke()` and `think()`
64
+ to delegate intelligence. The script handles loops, conditionals, polling,
65
+ and state. Each step gets fresh context.
66
+
67
+ ### When to use invoke vs think
68
+
69
+ - **invoke** -- the step needs tools, browser, filesystem, or writes output.
70
+ Async: returns immediately, poll for results.
71
+ - **think** -- the step only needs reasoning, classification, or structured
72
+ extraction. Sync: blocks until the LLM responds. Cheaper and faster.
73
+
74
+ In a 50-item batch, use think for classification gates and invoke for the
75
+ actual work. This avoids 50 unnecessary full agent sessions.
76
+
77
+ ### Workflow data
78
+
79
+ Workflow state lives under `workflows/<name>/` relative to cwd. On OpenClaw
80
+ this resolves to `~/.openclaw/workspace/workflows/<name>/`, alongside
81
+ other workspace conventions like `skills/` and `memory/`.
82
+
83
+ ```js
84
+ const WORK_DIR = join(process.cwd(), "workflows", WORKFLOW_NAME);
85
+ ```
86
+
87
+ Before running, ask the user if they want to track state differently
88
+ (database, API, custom path). The default is the workspace directory.
89
+
90
+ Never hardcode absolute home directory paths.
91
+
92
+ ### Standard directory layout
93
+
94
+ ```
95
+ workflows/<name>/
96
+ runs/<runId>.jsonl # one file per invoke/think call
97
+ state/ # checkpoints, progress, intermediate data
98
+ results/ # final output files
99
+ ```
100
+
101
+ ### Run tracking
102
+
103
+ Each `invoke()` or `think()` call gets its own JSONL file in `runs/`,
104
+ matching how OpenClaw stores sessions in `sessions/<sessionId>.jsonl`:
105
+
106
+ ```js
107
+ function logRun(runId, entry) {
108
+ mkdirSync(join(WORK_DIR, "runs"), { recursive: true });
109
+ appendFileSync(join(WORK_DIR, "runs", runId + ".jsonl"),
110
+ JSON.stringify({ ts: new Date().toISOString(), ...entry }) + "\n");
111
+ }
112
+
113
+ const run = await invoke(message);
114
+ logRun(run.runId, { type: "invoke", message: message.slice(0, 200), ok: run.ok });
115
+ ```
116
+
117
+ The `runId` correlates with OpenClaw's session logs for debugging.
118
+
119
+ ### Checkpointing
120
+
121
+ Write progress to `state/` after each step so the workflow can resume if
122
+ interrupted:
123
+
124
+ ```js
125
+ writeFileSync(join(WORK_DIR, "state/progress.txt"), String(i + 1));
126
+ ```
127
+
128
+ ### Packaging as a skill
129
+
130
+ Package the workflow as a skill using the agentskills.io format so it
131
+ appears in the skill catalog on future sessions:
132
+
133
+ ```
134
+ <workflow-name>/
135
+ ├── SKILL.md # name, description (triggers activation), usage docs
136
+ ├── scripts/
137
+ │ └── workflow.mjs # the operator workflow script
138
+ ├── references/ # optional: detailed docs loaded on demand
139
+ └── assets/ # optional: templates, config files
140
+ ```
141
+
142
+ SKILL.md frontmatter needs `name` (lowercase, hyphens, max 64 chars, matches
143
+ directory name) and `description` (what it does + when to use it -- this is
144
+ how the agent decides to activate).
145
+
146
+ Install to `~/.openclaw/workspace/skills/<workflow-name>/`.
147
+
148
+ ### Constraints
149
+
150
+ - Each invoke spawns a full agent session. No backpressure. Cap concurrency.
151
+ - invoke returns a `runId` -- log it for tracing.
152
+ - Each invoke and think consumes tokens. Prefer think when possible.
153
+
154
+ ### References and examples
155
+
156
+ See [references/workflow-guide.md](references/workflow-guide.md) for the
157
+ `invokeAndWait` helper, completion checks, and detailed state management.
158
+
159
+ Before building a workflow, read the examples:
160
+
161
+ - [examples/batch-analyze.mjs](examples/batch-analyze.mjs) -- checkpointing, resume, progress tracking
162
+ - [examples/research-pipeline.mjs](examples/research-pipeline.mjs) -- sequential steps with validation
163
+ - [examples/monitor-and-act.mjs](examples/monitor-and-act.mjs) -- conditional branching, CSV state
164
+
165
+ ## When to Use
166
+
167
+ Use when control flow is deterministic but each step needs judgment. The
168
+ script manages context between steps -- read outputs, compose prompts, pass
169
+ forward what matters. This gives you explicit control over what context each
170
+ step sees, without session window limits.
171
+
172
+ Do not use for single tasks small enough for one session.
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from "fs";
4
+ import { fileURLToPath } from "url";
5
+ import { dirname, join } from "path";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
9
+
10
+ const [command, ...rest] = process.argv.slice(2);
11
+
12
+ function hasFlag(name) {
13
+ return rest.includes("--" + name);
14
+ }
15
+
16
+ function flagValue(name) {
17
+ const idx = rest.indexOf("--" + name);
18
+ if (idx === -1 || idx + 1 >= rest.length) return undefined;
19
+ return rest[idx + 1];
20
+ }
21
+
22
+ function positional() {
23
+ return rest.filter((a) => !a.startsWith("--"));
24
+ }
25
+
26
+ switch (command) {
27
+ case "setup": {
28
+ if (hasFlag("help") || hasFlag("h")) {
29
+ console.log(`Configures OpenClaw for operator workflows. Enables hooks, generates
30
+ tokens, enables llm-task plugin, and symlinks the operator skill.
31
+
32
+ Safe to run multiple times.
33
+
34
+ Options:
35
+ --dry-run Show what would change without writing
36
+ --config Path to openclaw.json (default: auto-detect)
37
+
38
+ Examples:
39
+ operator-cli setup
40
+ operator-cli setup --dry-run
41
+ operator-cli setup --config ~/.openclaw/openclaw.json`);
42
+ break;
43
+ }
44
+ const { setup } = await import("../lib/setup.mjs");
45
+ const result = await setup({
46
+ dryRun: hasFlag("dry-run"),
47
+ configPath: flagValue("config"),
48
+ });
49
+ process.exit(result.ok ? 0 : 1);
50
+ break;
51
+ }
52
+
53
+ case "examples": {
54
+ if (hasFlag("help") || hasFlag("h")) {
55
+ console.log(`List or show example workflow scripts.
56
+
57
+ Usage:
58
+ operator-cli examples List available examples
59
+ operator-cli examples batch-analyze Show a specific example`);
60
+ break;
61
+ }
62
+ const name = positional()[0];
63
+ const examplesDir = join(__dirname, "..", "examples");
64
+ if (!name) {
65
+ const { readdirSync } = await import("fs");
66
+ const files = readdirSync(examplesDir).filter((f) => f.endsWith(".mjs"));
67
+ console.log("Available examples:\n");
68
+ for (const f of files) {
69
+ const stem = f.replace(".mjs", "");
70
+ const content = readFileSync(join(examplesDir, f), "utf-8");
71
+ const desc = content.match(/^\/\/\s*(.+)/m);
72
+ console.log(" " + stem.padEnd(24) + (desc ? desc[1] : ""));
73
+ }
74
+ console.log("\nRun 'operator-cli examples <name>' to view.");
75
+ } else {
76
+ const file = join(examplesDir, name + ".mjs");
77
+ try {
78
+ console.log(readFileSync(file, "utf-8"));
79
+ } catch {
80
+ console.error("Error: Unknown example '" + name + "'.");
81
+ console.error(" operator-cli examples");
82
+ process.exit(1);
83
+ }
84
+ }
85
+ break;
86
+ }
87
+
88
+ case "workflows": {
89
+ if (hasFlag("help") || hasFlag("h")) {
90
+ console.log(`Browse pre-built workflows from the monorepo.
91
+
92
+ Usage:
93
+ operator-cli workflows List available workflows
94
+ operator-cli workflows search "email" Search by keyword
95
+
96
+ Environment:
97
+ OPERATOR_REPO GitHub repo (default: openclaw/operator-mono)
98
+ OPERATOR_BRANCH Branch (default: main)
99
+ GITHUB_TOKEN Auth token for private repos / rate limits`);
100
+ break;
101
+ }
102
+ const { listWorkflows, searchWorkflows } = await import("../lib/workflows.mjs");
103
+ const sub = positional()[0];
104
+ try {
105
+ if (sub === "search") {
106
+ const query = positional()[1];
107
+ if (!query) {
108
+ console.error("Error: search requires a keyword.");
109
+ console.error(" operator-cli workflows search \"email\"");
110
+ process.exit(1);
111
+ }
112
+ const results = await searchWorkflows(query);
113
+ if (results.length === 0) {
114
+ console.log("No workflows matching '" + query + "'.");
115
+ } else {
116
+ console.log("Matching workflows:\n");
117
+ for (const w of results) {
118
+ console.log(" " + w.name.padEnd(28) + w.description);
119
+ }
120
+ console.log("\nRun 'operator-cli install <name>' to install.");
121
+ }
122
+ } else {
123
+ const workflows = await listWorkflows();
124
+ if (workflows.length === 0) {
125
+ console.log("No workflows found in the repository.");
126
+ } else {
127
+ console.log("Available workflows:\n");
128
+ for (const w of workflows) {
129
+ console.log(" " + w.name.padEnd(28) + w.description);
130
+ }
131
+ console.log("\nRun 'operator-cli install <name>' to install.");
132
+ console.log("Run 'operator-cli workflows search \"keyword\"' to filter.");
133
+ }
134
+ }
135
+ } catch (err) {
136
+ console.error("Error: " + err.message);
137
+ if (err.message.includes("403")) {
138
+ console.error(" Set GITHUB_TOKEN to avoid rate limits.");
139
+ }
140
+ process.exit(1);
141
+ }
142
+ break;
143
+ }
144
+
145
+ case "install": {
146
+ if (hasFlag("help") || hasFlag("h")) {
147
+ console.log(`Install a workflow to detected agent skill directories.
148
+
149
+ Usage:
150
+ operator-cli install <name> Install a workflow by name
151
+
152
+ The workflow becomes available to the agent on the next session.
153
+
154
+ Environment:
155
+ OPERATOR_REPO GitHub repo (default: openclaw/operator-mono)
156
+ OPERATOR_BRANCH Branch (default: main)
157
+ GITHUB_TOKEN Auth token for private repos / rate limits`);
158
+ break;
159
+ }
160
+ const workflowName = positional()[0];
161
+ if (!workflowName) {
162
+ console.error("Error: install requires a workflow name.");
163
+ console.error(" operator-cli workflows # list available");
164
+ console.error(" operator-cli install <name> # install one");
165
+ process.exit(1);
166
+ }
167
+ const { installWorkflow } = await import("../lib/workflows.mjs");
168
+ try {
169
+ const result = await installWorkflow(workflowName);
170
+ if (!result.ok) {
171
+ console.error("Error: " + result.error);
172
+ process.exit(1);
173
+ }
174
+ } catch (err) {
175
+ console.error("Error: " + err.message);
176
+ process.exit(1);
177
+ }
178
+ break;
179
+ }
180
+
181
+ case "--version":
182
+ case "-v":
183
+ console.log(pkg.version);
184
+ break;
185
+
186
+ case "--help":
187
+ case "-h":
188
+ case undefined:
189
+ console.log(`operator-cli ${pkg.version} - self-calling workflow primitives for OpenClaw
190
+
191
+ Usage: operator-cli <command> [options]
192
+
193
+ Commands:
194
+ setup Configure OpenClaw for operator workflows
195
+ examples List or show example workflow scripts
196
+ workflows Browse pre-built workflows from the monorepo
197
+ install Install a workflow to the skill directory
198
+
199
+ Use invoke() and think() in your workflow scripts:
200
+
201
+ import { invoke, think } from "@operator-labs/operator-cli";
202
+
203
+ Requires OpenClaw.
204
+
205
+ Run 'operator-cli <command> --help' for command-specific options.`);
206
+ break;
207
+
208
+ default:
209
+ console.error("Error: Unknown command '" + command + "'.");
210
+ console.error(" operator-cli --help");
211
+ process.exit(1);
212
+ }
@@ -0,0 +1,103 @@
1
+ // Batch analyze: process a list of items, analyze each, synthesize results.
2
+ //
3
+ // Demonstrates: checkpointing, resume, progress tracking, run tracking,
4
+ // think for classification, invoke for analysis, final synthesis step.
5
+ //
6
+ // invoke() returns immediately -- the agent works in the background.
7
+ // The script polls for each output file before moving to the next item.
8
+ //
9
+ // Usage: node examples/batch-analyze.mjs
10
+
11
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "fs";
12
+ import { join } from "path";
13
+ import { invoke, think } from "@operator-labs/operator-cli";
14
+ import { randomUUID } from "crypto";
15
+
16
+ const WORK_DIR = join(process.cwd(), "workflows", "batch-analyze");
17
+ for (const dir of ["runs", "results", "state"]) {
18
+ mkdirSync(join(WORK_DIR, dir), { recursive: true });
19
+ }
20
+
21
+ function logRun(runId, entry) {
22
+ appendFileSync(join(WORK_DIR, "runs", runId + ".jsonl"),
23
+ JSON.stringify({ ts: new Date().toISOString(), ...entry }) + "\n");
24
+ }
25
+
26
+ function status(fields) {
27
+ const current = existsSync(join(WORK_DIR, "state/status.json"))
28
+ ? JSON.parse(readFileSync(join(WORK_DIR, "state/status.json"), "utf-8"))
29
+ : {};
30
+ writeFileSync(join(WORK_DIR, "state/status.json"),
31
+ JSON.stringify({ ...current, ...fields, updatedAt: new Date().toISOString() }, null, 2));
32
+ }
33
+
34
+ async function waitForFile(path, maxWaitMs = 300000) {
35
+ const start = Date.now();
36
+ while (Date.now() - start < maxWaitMs) {
37
+ if (existsSync(path) && readFileSync(path, "utf-8").trim()) return true;
38
+ await new Promise(r => setTimeout(r, 5000));
39
+ }
40
+ return false;
41
+ }
42
+
43
+ const urls = [
44
+ "https://example.com",
45
+ "https://example.org",
46
+ "https://example.net",
47
+ ];
48
+
49
+ const progressFile = join(WORK_DIR, "state/progress.txt");
50
+ let start = 0;
51
+ if (existsSync(progressFile)) {
52
+ start = parseInt(readFileSync(progressFile, "utf-8"), 10);
53
+ }
54
+
55
+ status({ status: "running", pattern: "batch", total: urls.length, completed: start });
56
+
57
+ for (let i = start; i < urls.length; i++) {
58
+ const url = urls[i];
59
+ const outputPath = join(WORK_DIR, "results/url-" + i + ".md");
60
+
61
+ status({ currentStep: "classifying " + url, completed: i });
62
+
63
+ const thinkId = randomUUID();
64
+ const prompt = JSON.stringify({
65
+ prompt: "Classify this URL as blog, product, docs, or other. Return {type: string}.",
66
+ input: url,
67
+ schema: {
68
+ type: "object",
69
+ properties: { type: { type: "string", enum: ["blog", "product", "docs", "other"] } },
70
+ required: ["type"],
71
+ },
72
+ });
73
+ const classification = await think(prompt);
74
+ logRun(thinkId, { type: "think", prompt: prompt.slice(0, 200), ok: classification.ok, output: classification.output });
75
+
76
+ const type = classification.output?.type || "unknown";
77
+ status({ currentStep: "analyzing " + url + " (type: " + type + ")" });
78
+
79
+ const message =
80
+ "Analyze " + url + " (classified as " + type + "). " +
81
+ "Write a detailed analysis to " + outputPath + ". " +
82
+ "Include: purpose, content summary, target audience, and notable features.";
83
+ const run = await invoke(message);
84
+ logRun(run.runId, { type: "invoke", message: message.slice(0, 200), ok: run.ok });
85
+
86
+ const found = await waitForFile(outputPath);
87
+ logRun(run.runId, { type: "poll", path: outputPath, found });
88
+
89
+ writeFileSync(progressFile, String(i + 1));
90
+ }
91
+
92
+ status({ currentStep: "synthesizing results", completed: urls.length });
93
+
94
+ const synthesisMessage =
95
+ "Read all .md files in " + join(WORK_DIR, "results") + " and write a comparative " +
96
+ "synthesis to " + join(WORK_DIR, "results/synthesis.md") + ". " +
97
+ "Include a summary table and key findings.";
98
+ const synthRun = await invoke(synthesisMessage);
99
+ logRun(synthRun.runId, { type: "invoke", message: synthesisMessage.slice(0, 200), ok: synthRun.ok });
100
+ await waitForFile(join(WORK_DIR, "results/synthesis.md"));
101
+ logRun(synthRun.runId, { type: "done" });
102
+
103
+ status({ status: "completed", currentStep: "done" });
@@ -0,0 +1,122 @@
1
+ // Monitor and act: check conditions, branch on results, update state.
2
+ //
3
+ // Demonstrates: think for boolean gates, conditional invoke calls,
4
+ // CSV as shared state, data-driven branching, per-run JSONL tracking.
5
+ //
6
+ // think() returns the decision synchronously; invoke() dispatches the
7
+ // agent and the script polls for the report file before continuing.
8
+ //
9
+ // Usage: node examples/monitor-and-act.mjs
10
+
11
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "fs";
12
+ import { join } from "path";
13
+ import { invoke, think } from "@operator-labs/operator-cli";
14
+ import { randomUUID } from "crypto";
15
+
16
+ const WORK_DIR = join(process.cwd(), "workflows", "monitor-and-act");
17
+ for (const dir of ["runs", "state"]) {
18
+ mkdirSync(join(WORK_DIR, dir), { recursive: true });
19
+ }
20
+
21
+ function logRun(runId, entry) {
22
+ appendFileSync(join(WORK_DIR, "runs", runId + ".jsonl"),
23
+ JSON.stringify({ ts: new Date().toISOString(), ...entry }) + "\n");
24
+ }
25
+
26
+ async function waitForFile(path, maxWaitMs = 300000) {
27
+ const start = Date.now();
28
+ while (Date.now() - start < maxWaitMs) {
29
+ if (existsSync(path) && readFileSync(path, "utf-8").trim()) return true;
30
+ await new Promise(r => setTimeout(r, 5000));
31
+ }
32
+ return false;
33
+ }
34
+
35
+ const csvPath = join(WORK_DIR, "state/status.csv");
36
+ if (!existsSync(csvPath)) {
37
+ writeFileSync(csvPath, "url,status,last_checked,notes\n" +
38
+ "https://example.com/api/health,pending,,\n" +
39
+ "https://example.org/status,pending,,\n" +
40
+ "https://example.net/ping,pending,,\n");
41
+ }
42
+
43
+ function readCsv() {
44
+ const lines = readFileSync(csvPath, "utf-8").trim().split("\n");
45
+ const header = lines[0].split(",");
46
+ return lines.slice(1).map((line) => {
47
+ const values = line.split(",");
48
+ const row = {};
49
+ header.forEach((h, i) => row[h] = values[i] || "");
50
+ return row;
51
+ });
52
+ }
53
+
54
+ function writeCsv(rows) {
55
+ const header = Object.keys(rows[0]).join(",");
56
+ const lines = rows.map((r) => Object.values(r).join(","));
57
+ writeFileSync(csvPath, header + "\n" + lines.join("\n") + "\n");
58
+ }
59
+
60
+ const rows = readCsv();
61
+
62
+ for (const row of rows) {
63
+ const gateId = randomUUID();
64
+ const check = await think(JSON.stringify({
65
+ prompt: "You are monitoring this endpoint. Based on its current status, " +
66
+ "should it be checked now? Consider: status is '" + row.status + "', " +
67
+ "last checked: " + (row.last_checked || "never") + ". " +
68
+ "Return {shouldCheck: boolean, reason: string}",
69
+ input: { url: row.url, status: row.status, lastChecked: row.last_checked },
70
+ schema: {
71
+ type: "object",
72
+ properties: {
73
+ shouldCheck: { type: "boolean" },
74
+ reason: { type: "string" },
75
+ },
76
+ required: ["shouldCheck", "reason"],
77
+ },
78
+ }));
79
+ logRun(gateId, { type: "think", step: "gate", url: row.url, ok: check.ok, output: check.output });
80
+
81
+ if (!check.output?.shouldCheck) {
82
+ continue;
83
+ }
84
+
85
+ const reportPath = join(WORK_DIR, "state/report-" + Buffer.from(row.url).toString("base64url").slice(0, 20) + ".md");
86
+ const checkMsg =
87
+ "Check the endpoint " + row.url + ". Determine if it is healthy. " +
88
+ "Write a brief status report to " + reportPath + ". " +
89
+ "Include: response status, response time estimate, any issues found.";
90
+ const run = await invoke(checkMsg);
91
+ logRun(run.runId, { type: "invoke", step: "check", url: row.url, message: checkMsg.slice(0, 200), ok: run.ok });
92
+ await waitForFile(reportPath, 120000);
93
+ logRun(run.runId, { type: "poll", path: reportPath, found: existsSync(reportPath) });
94
+
95
+ let newStatus = "checked";
96
+ if (existsSync(reportPath)) {
97
+ const report = readFileSync(reportPath, "utf-8");
98
+ const extractId = randomUUID();
99
+ const extract = await think(JSON.stringify({
100
+ prompt: "Extract the health status from this report. Return {healthy: boolean, summary: string}",
101
+ input: report,
102
+ schema: {
103
+ type: "object",
104
+ properties: {
105
+ healthy: { type: "boolean" },
106
+ summary: { type: "string" },
107
+ },
108
+ required: ["healthy", "summary"],
109
+ },
110
+ }));
111
+ logRun(extractId, { type: "think", step: "extract", url: row.url, ok: extract.ok, output: extract.output });
112
+
113
+ newStatus = extract.output?.healthy ? "healthy" : "unhealthy";
114
+ row.notes = (extract.output?.summary || "").replace(/,/g, ";").slice(0, 100);
115
+ }
116
+
117
+ row.status = newStatus;
118
+ row.last_checked = new Date().toISOString();
119
+ }
120
+
121
+ writeCsv(rows);
122
+ console.log("status: " + csvPath);