@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 +91 -0
- package/SKILL.md +172 -0
- package/bin/operator.mjs +212 -0
- package/examples/batch-analyze.mjs +103 -0
- package/examples/monitor-and-act.mjs +122 -0
- package/examples/research-pipeline.mjs +122 -0
- package/index.mjs +5 -0
- package/lib/backend.mjs +71 -0
- package/lib/backends/openclaw.mjs +151 -0
- package/lib/config.mjs +155 -0
- package/lib/http.mjs +31 -0
- package/lib/invoke.mjs +24 -0
- package/lib/safety.mjs +36 -0
- package/lib/setup.mjs +197 -0
- package/lib/think.mjs +18 -0
- package/lib/workflows.mjs +178 -0
- package/package.json +29 -0
- package/references/error-handling.md +100 -0
- package/references/patterns.md +144 -0
- package/references/safety.md +37 -0
- package/references/setup.md +60 -0
- package/references/workflow-guide.md +150 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Safety Rules
|
|
2
|
+
|
|
3
|
+
`invoke()` prepends safety rules to every message by default. This prevents hook-spawned agent sessions from creating persistent autonomous behavior.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
`invoke()` prepends a short block of text before your message:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
OPERATOR SAFETY RULES:
|
|
11
|
+
You MUST NOT write to the following paths:
|
|
12
|
+
- ~/.openclaw/skills
|
|
13
|
+
- ~/.openclaw/hooks
|
|
14
|
+
- openclaw.json
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
<your actual message>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The agent still has full tool access. The rules tell it not to use write tools on specific paths.
|
|
23
|
+
|
|
24
|
+
## What it prevents
|
|
25
|
+
|
|
26
|
+
An agent that writes a new skill to `~/.openclaw/skills/` containing its own `operator invoke` calls creates a self-replicating loop. The safety rules prevent this by instructing the agent to skip writes to the skills directory, hooks directory, and gateway config.
|
|
27
|
+
|
|
28
|
+
## Limitations
|
|
29
|
+
|
|
30
|
+
This is a prompt-level boundary. The agent is told not to write to protected paths, not mechanically prevented. This is the same trust model OpenClaw uses for external content wrapping. It works for non-adversarial workflows processing structured data. It does not prevent a crafted prompt injection designed to override the rules.
|
|
31
|
+
|
|
32
|
+
## Disabling
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
// Per invocation
|
|
36
|
+
await invoke("your message", { safety: false });
|
|
37
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Manual Setup
|
|
2
|
+
|
|
3
|
+
If you prefer to configure manually instead of running `operator-cli setup`,
|
|
4
|
+
the following must be present in `openclaw.json` (typically at
|
|
5
|
+
`~/.openclaw/openclaw.json` or `$OPENCLAW_CONFIG_PATH`):
|
|
6
|
+
|
|
7
|
+
## 1. Hooks enabled with a token and the operator mapping
|
|
8
|
+
|
|
9
|
+
- `hooks.enabled`: `true`
|
|
10
|
+
- `hooks.token`: any non-empty string (generate with `require("crypto").randomBytes(32).toString("hex")`)
|
|
11
|
+
- `hooks.mappings`: must include:
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"id": "operator",
|
|
16
|
+
"match": { "path": "operator" },
|
|
17
|
+
"action": "agent",
|
|
18
|
+
"messageTemplate": "{{message}}"
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 2. llm-task plugin enabled (required for think())
|
|
23
|
+
|
|
24
|
+
- `plugins.entries.llm-task.enabled`: `true`
|
|
25
|
+
|
|
26
|
+
## 3. llm-task in the tool allowlist
|
|
27
|
+
|
|
28
|
+
- `tools.allow` must include `"llm-task"`
|
|
29
|
+
|
|
30
|
+
## 4. Gateway auth token (required for think(), not for invoke())
|
|
31
|
+
|
|
32
|
+
- `gateway.auth.token` in config, or `OPENCLAW_GATEWAY_TOKEN` env var
|
|
33
|
+
- Must differ from `hooks.token` -- OpenClaw rejects startup if they match
|
|
34
|
+
|
|
35
|
+
## Full example
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"hooks": {
|
|
40
|
+
"enabled": true,
|
|
41
|
+
"token": "<generated 64-char hex>",
|
|
42
|
+
"mappings": [
|
|
43
|
+
{
|
|
44
|
+
"id": "operator",
|
|
45
|
+
"match": { "path": "operator" },
|
|
46
|
+
"action": "agent",
|
|
47
|
+
"messageTemplate": "{{message}}"
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"plugins": {
|
|
52
|
+
"entries": {
|
|
53
|
+
"llm-task": { "enabled": true }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"tools": {
|
|
57
|
+
"allow": ["llm-task"]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Writing Workflow Scripts
|
|
2
|
+
|
|
3
|
+
Workflows are Node.js scripts that import `invoke` and `think` from
|
|
4
|
+
`@operator-labs/operator-cli`. After `operator-cli setup`, imports resolve
|
|
5
|
+
from any script in the agent's skill directory or from a project with the
|
|
6
|
+
package installed.
|
|
7
|
+
|
|
8
|
+
A workflow is a skill. It lives in the agent's skill directory alongside a
|
|
9
|
+
`SKILL.md` so the agent can discover and run it on future sessions.
|
|
10
|
+
|
|
11
|
+
## invokeAndWait
|
|
12
|
+
|
|
13
|
+
`invoke()` returns immediately. Poll for the result:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
async function invokeAndWait(message, done, { interval = 5000, maxWait = 300000 } = {}) {
|
|
17
|
+
await invoke(message);
|
|
18
|
+
const start = Date.now();
|
|
19
|
+
while (Date.now() - start < maxWait) {
|
|
20
|
+
if (await done()) return;
|
|
21
|
+
await new Promise(r => setTimeout(r, interval));
|
|
22
|
+
}
|
|
23
|
+
throw new Error("timed out waiting for task to complete");
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The `done()` check is yours -- file, database row, API call, anything:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
const fileReady = (path) => () => existsSync(path) && readFileSync(path, "utf-8").trim();
|
|
31
|
+
const apiReady = (url) => async () => (await fetch(url)).ok;
|
|
32
|
+
const dbRow = (query) => async () => (await db.query(query)).rows.length > 0;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Full example
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
import { readFileSync, existsSync, mkdirSync } from "fs";
|
|
39
|
+
import { join } from "path";
|
|
40
|
+
import { invoke, think } from "@operator-labs/operator-cli";
|
|
41
|
+
|
|
42
|
+
const WORK_DIR = join(process.cwd(), "workflows", "my-workflow");
|
|
43
|
+
for (const dir of ["results", "state", "logs"]) {
|
|
44
|
+
mkdirSync(join(WORK_DIR, dir), { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Classify with think (synchronous request/response)
|
|
48
|
+
const classification = await think(JSON.stringify({
|
|
49
|
+
prompt: "Classify this URL.",
|
|
50
|
+
input: "https://example.com",
|
|
51
|
+
schema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: { type: { type: "string", enum: ["blog", "product", "docs"] } },
|
|
54
|
+
required: ["type"],
|
|
55
|
+
},
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
// Invoke and wait for output file
|
|
59
|
+
const analysisPath = join(WORK_DIR, "results/analysis.md");
|
|
60
|
+
await invoke(
|
|
61
|
+
"Analyze https://example.com (type: " + classification.output?.type + "). " +
|
|
62
|
+
"Write findings to " + analysisPath,
|
|
63
|
+
);
|
|
64
|
+
while (!existsSync(analysisPath) || !readFileSync(analysisPath, "utf-8").trim()) {
|
|
65
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## State management
|
|
70
|
+
|
|
71
|
+
Workflow data lives under `workflows/<name>/` relative to cwd. On OpenClaw
|
|
72
|
+
this resolves to `~/.openclaw/workspace/workflows/<name>/`.
|
|
73
|
+
|
|
74
|
+
Standard layout:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
workflows/<name>/
|
|
78
|
+
runs/<runId>.jsonl # one file per invoke/think call
|
|
79
|
+
state/ # checkpoints, progress, intermediate data
|
|
80
|
+
results/ # final output files
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Run tracking
|
|
84
|
+
|
|
85
|
+
Each `invoke()` or `think()` call gets its own JSONL file in `runs/`,
|
|
86
|
+
matching how OpenClaw stores sessions in `sessions/<sessionId>.jsonl`:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
function logRun(runId, entry) {
|
|
90
|
+
mkdirSync(join(WORK_DIR, "runs"), { recursive: true });
|
|
91
|
+
appendFileSync(join(WORK_DIR, "runs", runId + ".jsonl"),
|
|
92
|
+
JSON.stringify({ ts: new Date().toISOString(), ...entry }) + "\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const run = await invoke(message);
|
|
96
|
+
logRun(run.runId, { type: "invoke", message: message.slice(0, 200), ok: run.ok });
|
|
97
|
+
// append more events to the same file as the run progresses
|
|
98
|
+
logRun(run.runId, { type: "poll", path: outputPath, found: true });
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The `runId` correlates with OpenClaw's session logs for debugging.
|
|
102
|
+
|
|
103
|
+
### Checkpointing
|
|
104
|
+
|
|
105
|
+
For workflows that benefit from resume (batches, long pipelines), write
|
|
106
|
+
progress to `state/` after each step so the workflow can pick up where it
|
|
107
|
+
left off.
|
|
108
|
+
|
|
109
|
+
## Where workflows live
|
|
110
|
+
|
|
111
|
+
A workflow is a skill. The agent creates it in its own skill directory:
|
|
112
|
+
|
|
113
|
+
- `~/.openclaw/workspace/skills/<workflow-name>/`
|
|
114
|
+
|
|
115
|
+
Each workflow follows the agentskills.io format:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
<workflow-name>/
|
|
119
|
+
├── SKILL.md # frontmatter (name, description) + instructions
|
|
120
|
+
├── scripts/ # executable workflow code
|
|
121
|
+
│ └── workflow.mjs
|
|
122
|
+
├── references/ # docs loaded on demand (optional)
|
|
123
|
+
│ └── *.md
|
|
124
|
+
└── assets/ # templates, config files (optional)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
SKILL.md frontmatter requires `name` (lowercase, hyphens, max 64 chars)
|
|
128
|
+
and `description` (what it does + when to use it). The description is how the
|
|
129
|
+
agent decides to activate the skill, so include specific triggers. The body
|
|
130
|
+
contains usage instructions, loaded only after activation.
|
|
131
|
+
|
|
132
|
+
On future sessions the workflow appears in the skill catalog and can be
|
|
133
|
+
reused or modified.
|
|
134
|
+
|
|
135
|
+
Pre-built workflows from the monorepo can be installed with
|
|
136
|
+
`operator-cli install <name>`, which copies the skill into the skill directory.
|
|
137
|
+
|
|
138
|
+
## Constraints
|
|
139
|
+
|
|
140
|
+
- Each `invoke` spawns a full agent session. Parallel invokes without a concurrency cap can exhaust resources.
|
|
141
|
+
- `invoke()` returns a `runId`. Log it -- it's the only link between your workflow log and the agent session.
|
|
142
|
+
- Each invoke and think call consumes tokens. Prefer `think` over `invoke` when the step only needs reasoning.
|
|
143
|
+
|
|
144
|
+
## Examples
|
|
145
|
+
|
|
146
|
+
Before building a workflow, read the bundled examples:
|
|
147
|
+
|
|
148
|
+
- `examples/batch-analyze.mjs` — checkpointing, resume, progress tracking
|
|
149
|
+
- `examples/research-pipeline.mjs` — sequential steps with validation
|
|
150
|
+
- `examples/monitor-and-act.mjs` — conditional branching, CSV state
|