@openclaw/lobster 2026.5.2 → 2026.5.3-beta.2
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.js +654 -0
- package/dist/runtime-api.js +3 -0
- package/package.json +20 -3
- package/index.ts +0 -24
- package/runtime-api.ts +0 -12
- package/src/lobster-ajv-cache.ts +0 -142
- package/src/lobster-core.d.ts +0 -60
- package/src/lobster-runner.test.ts +0 -572
- package/src/lobster-runner.ts +0 -395
- package/src/lobster-taskflow.test.ts +0 -227
- package/src/lobster-taskflow.ts +0 -279
- package/src/lobster-tool.test.ts +0 -353
- package/src/lobster-tool.ts +0 -320
- package/src/taskflow-test-helpers.ts +0 -48
- package/tsconfig.json +0 -16
package/dist/index.js
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { stat } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { Readable, Writable } from "node:stream";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import AjvPkg from "ajv";
|
|
11
|
+
//#region extensions/lobster/src/lobster-ajv-cache.ts
|
|
12
|
+
const installedSymbol = Symbol.for("openclaw.lobster.ajv-compile-cache.installed");
|
|
13
|
+
const cacheSymbol = Symbol.for("openclaw.lobster.ajv-compile-cache.entries");
|
|
14
|
+
const maxEntries = 512;
|
|
15
|
+
const AjvCtor = AjvPkg;
|
|
16
|
+
function stableJsonStringify(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
17
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
18
|
+
if (seen.has(value)) throw new TypeError("Cannot cache cyclic JSON schema");
|
|
19
|
+
seen.add(value);
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
const items = value.map((entry) => stableJsonStringify(entry, seen));
|
|
22
|
+
seen.delete(value);
|
|
23
|
+
return `[${items.join(",")}]`;
|
|
24
|
+
}
|
|
25
|
+
const record = value;
|
|
26
|
+
const properties = Object.keys(record).toSorted().filter((key) => record[key] !== void 0).map((key) => `${JSON.stringify(key)}:${stableJsonStringify(record[key], seen)}`);
|
|
27
|
+
seen.delete(value);
|
|
28
|
+
return `{${properties.join(",")}}`;
|
|
29
|
+
}
|
|
30
|
+
function compileCacheKey(schema) {
|
|
31
|
+
try {
|
|
32
|
+
return createHash("sha256").update(stableJsonStringify(schema)).digest("hex");
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function readCompileCache(instance) {
|
|
38
|
+
let cache = instance[cacheSymbol];
|
|
39
|
+
if (!cache) {
|
|
40
|
+
cache = /* @__PURE__ */ new Map();
|
|
41
|
+
Object.defineProperty(instance, cacheSymbol, {
|
|
42
|
+
value: cache,
|
|
43
|
+
configurable: true
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return cache;
|
|
47
|
+
}
|
|
48
|
+
function rememberCompiledValidator(params) {
|
|
49
|
+
const { cache, instance, key, removeSchema, schema, validate } = params;
|
|
50
|
+
if (!cache.has(key) && cache.size >= maxEntries) {
|
|
51
|
+
const oldest = cache.keys().next().value;
|
|
52
|
+
if (oldest !== void 0) {
|
|
53
|
+
const evicted = cache.get(oldest);
|
|
54
|
+
cache.delete(oldest);
|
|
55
|
+
if (evicted) removeSchema.call(instance, evicted.schema);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
cache.set(key, {
|
|
59
|
+
schema,
|
|
60
|
+
validate
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function installLobsterAjvCompileCache() {
|
|
64
|
+
const proto = AjvCtor.prototype;
|
|
65
|
+
if (proto[installedSymbol]) return;
|
|
66
|
+
const originalCompile = proto.compile;
|
|
67
|
+
const originalRemoveSchema = proto.removeSchema;
|
|
68
|
+
Object.defineProperty(proto, installedSymbol, {
|
|
69
|
+
value: true,
|
|
70
|
+
configurable: true
|
|
71
|
+
});
|
|
72
|
+
proto.compile = function compileWithContentCache(schema) {
|
|
73
|
+
const key = compileCacheKey(schema);
|
|
74
|
+
if (!key) return originalCompile.call(this, schema);
|
|
75
|
+
const cache = readCompileCache(this);
|
|
76
|
+
const cached = cache.get(key);
|
|
77
|
+
if (cached) return cached.validate;
|
|
78
|
+
const validate = originalCompile.call(this, schema);
|
|
79
|
+
rememberCompiledValidator({
|
|
80
|
+
cache,
|
|
81
|
+
instance: this,
|
|
82
|
+
key,
|
|
83
|
+
removeSchema: originalRemoveSchema,
|
|
84
|
+
schema,
|
|
85
|
+
validate
|
|
86
|
+
});
|
|
87
|
+
return validate;
|
|
88
|
+
};
|
|
89
|
+
proto.removeSchema = function removeSchemaAndClearContentCache(schemaKeyRef) {
|
|
90
|
+
this[cacheSymbol]?.clear();
|
|
91
|
+
return originalRemoveSchema.call(this, schemaKeyRef);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region extensions/lobster/src/lobster-runner.ts
|
|
96
|
+
const lobsterRequire = createRequire(import.meta.url);
|
|
97
|
+
function toEmbeddedToolRuntime(moduleExports, source) {
|
|
98
|
+
const { runToolRequest, resumeToolRequest } = moduleExports;
|
|
99
|
+
if (typeof runToolRequest === "function" && typeof resumeToolRequest === "function") return {
|
|
100
|
+
runToolRequest,
|
|
101
|
+
resumeToolRequest
|
|
102
|
+
};
|
|
103
|
+
throw new Error(`${source} does not export Lobster embedded runtime functions`);
|
|
104
|
+
}
|
|
105
|
+
function findLobsterPackageRoot(resolvedEntryPath) {
|
|
106
|
+
let dir = path.dirname(resolvedEntryPath);
|
|
107
|
+
while (true) {
|
|
108
|
+
const packageJsonPath = path.join(dir, "package.json");
|
|
109
|
+
try {
|
|
110
|
+
if (JSON.parse(readFileSync(packageJsonPath, "utf8")).name === "@clawdbot/lobster") return dir;
|
|
111
|
+
} catch {}
|
|
112
|
+
const parent = path.dirname(dir);
|
|
113
|
+
if (parent === dir) throw new Error(`Could not locate @clawdbot/lobster package root from ${resolvedEntryPath}`);
|
|
114
|
+
dir = parent;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function normalizeForCwdSandbox(p) {
|
|
118
|
+
const normalized = path.normalize(p);
|
|
119
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
120
|
+
}
|
|
121
|
+
function resolveLobsterCwd(cwdRaw) {
|
|
122
|
+
if (typeof cwdRaw !== "string" || !cwdRaw.trim()) return process.cwd();
|
|
123
|
+
const cwd = cwdRaw.trim();
|
|
124
|
+
if (path.isAbsolute(cwd)) throw new Error("cwd must be a relative path");
|
|
125
|
+
const base = process.cwd();
|
|
126
|
+
const resolved = path.resolve(base, cwd);
|
|
127
|
+
const rel = path.relative(normalizeForCwdSandbox(base), normalizeForCwdSandbox(resolved));
|
|
128
|
+
if (rel === "" || rel === ".") return resolved;
|
|
129
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error("cwd must stay within the gateway working directory");
|
|
130
|
+
return resolved;
|
|
131
|
+
}
|
|
132
|
+
function createLimitedSink(maxBytes, label) {
|
|
133
|
+
let bytes = 0;
|
|
134
|
+
return new Writable({ write(chunk, _encoding, callback) {
|
|
135
|
+
bytes += Buffer.byteLength(String(chunk), "utf8");
|
|
136
|
+
if (bytes > maxBytes) {
|
|
137
|
+
callback(/* @__PURE__ */ new Error(`lobster ${label} exceeded maxStdoutBytes`));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
callback();
|
|
141
|
+
} });
|
|
142
|
+
}
|
|
143
|
+
function normalizeEnvelope(envelope) {
|
|
144
|
+
if (envelope.ok) {
|
|
145
|
+
if (envelope.status === "needs_input") return {
|
|
146
|
+
ok: false,
|
|
147
|
+
error: {
|
|
148
|
+
type: "unsupported_status",
|
|
149
|
+
message: "Lobster input requests are not supported by the OpenClaw Lobster tool yet"
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
ok: true,
|
|
154
|
+
status: envelope.status ?? "ok",
|
|
155
|
+
output: Array.isArray(envelope.output) ? envelope.output : [],
|
|
156
|
+
requiresApproval: envelope.requiresApproval ? {
|
|
157
|
+
type: "approval_request",
|
|
158
|
+
prompt: envelope.requiresApproval.prompt,
|
|
159
|
+
items: envelope.requiresApproval.items,
|
|
160
|
+
...envelope.requiresApproval.resumeToken ? { resumeToken: envelope.requiresApproval.resumeToken } : {},
|
|
161
|
+
...envelope.requiresApproval.approvalId ? { approvalId: envelope.requiresApproval.approvalId } : {}
|
|
162
|
+
} : null
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
error: {
|
|
168
|
+
type: envelope.error?.type,
|
|
169
|
+
message: envelope.error?.message ?? "lobster runtime failed"
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function throwOnErrorEnvelope(envelope) {
|
|
174
|
+
if (envelope.ok) return envelope;
|
|
175
|
+
throw new Error(envelope.error.message);
|
|
176
|
+
}
|
|
177
|
+
async function resolveWorkflowFile(candidate, cwd) {
|
|
178
|
+
const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(cwd, candidate);
|
|
179
|
+
if (!(await stat(resolved)).isFile()) throw new Error("Workflow path is not a file");
|
|
180
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
181
|
+
if (![
|
|
182
|
+
".lobster",
|
|
183
|
+
".yaml",
|
|
184
|
+
".yml",
|
|
185
|
+
".json"
|
|
186
|
+
].includes(ext)) throw new Error("Workflow file must end in .lobster, .yaml, .yml, or .json");
|
|
187
|
+
return resolved;
|
|
188
|
+
}
|
|
189
|
+
async function detectWorkflowFile(candidate, cwd) {
|
|
190
|
+
const trimmed = candidate.trim();
|
|
191
|
+
if (!trimmed || trimmed.includes("|")) return null;
|
|
192
|
+
try {
|
|
193
|
+
return await resolveWorkflowFile(trimmed, cwd);
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function parseWorkflowArgs(argsJson) {
|
|
199
|
+
return JSON.parse(argsJson);
|
|
200
|
+
}
|
|
201
|
+
function createEmbeddedToolContext(params, signal) {
|
|
202
|
+
const env = { ...process.env };
|
|
203
|
+
return {
|
|
204
|
+
cwd: params.cwd,
|
|
205
|
+
env,
|
|
206
|
+
mode: "tool",
|
|
207
|
+
stdin: Readable.from([]),
|
|
208
|
+
stdout: createLimitedSink(Math.max(1024, params.maxStdoutBytes), "stdout"),
|
|
209
|
+
stderr: createLimitedSink(Math.max(1024, params.maxStdoutBytes), "stderr"),
|
|
210
|
+
signal
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async function withTimeout(timeoutMs, fn) {
|
|
214
|
+
const timeout = Math.max(200, timeoutMs);
|
|
215
|
+
const controller = new AbortController();
|
|
216
|
+
return await new Promise((resolve, reject) => {
|
|
217
|
+
const onTimeout = () => {
|
|
218
|
+
const error = /* @__PURE__ */ new Error("lobster runtime timed out");
|
|
219
|
+
controller.abort(error);
|
|
220
|
+
reject(error);
|
|
221
|
+
};
|
|
222
|
+
const timer = setTimeout(onTimeout, timeout);
|
|
223
|
+
fn(controller.signal).then((value) => {
|
|
224
|
+
clearTimeout(timer);
|
|
225
|
+
resolve(value);
|
|
226
|
+
}, (error) => {
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
reject(error);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async function loadEmbeddedToolRuntimeFromPackage(options = {}) {
|
|
233
|
+
installLobsterAjvCompileCache();
|
|
234
|
+
const importModule = options.importModule ?? (async (specifier) => await import(specifier));
|
|
235
|
+
const resolvePackageEntry = options.resolvePackageEntry ?? ((specifier) => lobsterRequire.resolve(specifier));
|
|
236
|
+
let coreLoadError;
|
|
237
|
+
try {
|
|
238
|
+
return toEmbeddedToolRuntime(await importModule([
|
|
239
|
+
"@clawdbot",
|
|
240
|
+
"lobster",
|
|
241
|
+
"core"
|
|
242
|
+
].join("/")), "@clawdbot/lobster/core");
|
|
243
|
+
} catch (error) {
|
|
244
|
+
coreLoadError = error;
|
|
245
|
+
}
|
|
246
|
+
let fallbackLoadError;
|
|
247
|
+
try {
|
|
248
|
+
const packageRoot = findLobsterPackageRoot(resolvePackageEntry("@clawdbot/lobster"));
|
|
249
|
+
const coreRuntimeUrl = pathToFileURL(path.join(packageRoot, "dist/src/core/index.js")).href;
|
|
250
|
+
return toEmbeddedToolRuntime(await importModule(coreRuntimeUrl), coreRuntimeUrl);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
fallbackLoadError = error;
|
|
253
|
+
}
|
|
254
|
+
throw new Error("Failed to load the Lobster embedded runtime", { cause: new AggregateError([coreLoadError, fallbackLoadError], "Both Lobster embedded runtime load paths failed") });
|
|
255
|
+
}
|
|
256
|
+
function createEmbeddedLobsterRunner(options) {
|
|
257
|
+
const loadRuntime = options?.loadRuntime ?? loadEmbeddedToolRuntimeFromPackage;
|
|
258
|
+
let runtimePromise;
|
|
259
|
+
return { async run(params) {
|
|
260
|
+
runtimePromise ??= loadRuntime();
|
|
261
|
+
const runtime = await runtimePromise;
|
|
262
|
+
return await withTimeout(params.timeoutMs, async (signal) => {
|
|
263
|
+
const ctx = createEmbeddedToolContext(params, signal);
|
|
264
|
+
if (params.action === "run") {
|
|
265
|
+
const pipeline = params.pipeline?.trim() ?? "";
|
|
266
|
+
if (!pipeline) throw new Error("pipeline required");
|
|
267
|
+
const filePath = await detectWorkflowFile(pipeline, params.cwd);
|
|
268
|
+
if (filePath) {
|
|
269
|
+
const parsedArgsJson = params.argsJson?.trim() ?? "";
|
|
270
|
+
let args;
|
|
271
|
+
if (parsedArgsJson) try {
|
|
272
|
+
args = parseWorkflowArgs(parsedArgsJson);
|
|
273
|
+
} catch {
|
|
274
|
+
throw new Error("run --args-json must be valid JSON");
|
|
275
|
+
}
|
|
276
|
+
return throwOnErrorEnvelope(normalizeEnvelope(await runtime.runToolRequest({
|
|
277
|
+
filePath,
|
|
278
|
+
args,
|
|
279
|
+
ctx
|
|
280
|
+
})));
|
|
281
|
+
}
|
|
282
|
+
return throwOnErrorEnvelope(normalizeEnvelope(await runtime.runToolRequest({
|
|
283
|
+
pipeline,
|
|
284
|
+
ctx
|
|
285
|
+
})));
|
|
286
|
+
}
|
|
287
|
+
const token = params.token?.trim() ?? "";
|
|
288
|
+
const approvalId = params.approvalId?.trim() ?? "";
|
|
289
|
+
if (!token && !approvalId) throw new Error("token or approvalId required");
|
|
290
|
+
if (typeof params.approve !== "boolean") throw new Error("approve required");
|
|
291
|
+
return throwOnErrorEnvelope(normalizeEnvelope(await runtime.resumeToolRequest({
|
|
292
|
+
...token ? { token } : {},
|
|
293
|
+
...approvalId ? { approvalId } : {},
|
|
294
|
+
approved: params.approve,
|
|
295
|
+
ctx
|
|
296
|
+
})));
|
|
297
|
+
});
|
|
298
|
+
} };
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region extensions/lobster/src/lobster-taskflow.ts
|
|
302
|
+
function toJsonLike(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
303
|
+
if (value === null) return null;
|
|
304
|
+
switch (typeof value) {
|
|
305
|
+
case "boolean":
|
|
306
|
+
case "string": return value;
|
|
307
|
+
case "number": return Number.isFinite(value) ? value : String(value);
|
|
308
|
+
case "bigint": return value.toString();
|
|
309
|
+
case "undefined":
|
|
310
|
+
case "function":
|
|
311
|
+
case "symbol": return null;
|
|
312
|
+
case "object": {
|
|
313
|
+
if (value instanceof Date) return value.toISOString();
|
|
314
|
+
if (Array.isArray(value)) return value.map((item) => toJsonLike(item, seen));
|
|
315
|
+
if (seen.has(value)) return "[Circular]";
|
|
316
|
+
seen.add(value);
|
|
317
|
+
const jsonObject = {};
|
|
318
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
319
|
+
if (entry === void 0 || typeof entry === "function" || typeof entry === "symbol") continue;
|
|
320
|
+
jsonObject[key] = toJsonLike(entry, seen);
|
|
321
|
+
}
|
|
322
|
+
seen.delete(value);
|
|
323
|
+
return jsonObject;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
function buildApprovalWaitState(envelope) {
|
|
329
|
+
if (!envelope.requiresApproval) return {
|
|
330
|
+
kind: "lobster_approval",
|
|
331
|
+
prompt: "",
|
|
332
|
+
items: []
|
|
333
|
+
};
|
|
334
|
+
return {
|
|
335
|
+
kind: "lobster_approval",
|
|
336
|
+
prompt: envelope.requiresApproval.prompt,
|
|
337
|
+
items: envelope.requiresApproval.items.map((item) => toJsonLike(item)),
|
|
338
|
+
...envelope.requiresApproval.resumeToken ? { resumeToken: envelope.requiresApproval.resumeToken } : {},
|
|
339
|
+
...envelope.requiresApproval.approvalId ? { approvalId: envelope.requiresApproval.approvalId } : {}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function applyEnvelopeToFlow(params) {
|
|
343
|
+
const { taskFlow, flow, envelope, waitingStep } = params;
|
|
344
|
+
if (!envelope.ok) return taskFlow.fail({
|
|
345
|
+
flowId: flow.flowId,
|
|
346
|
+
expectedRevision: flow.revision
|
|
347
|
+
});
|
|
348
|
+
if (envelope.status === "needs_approval") return taskFlow.setWaiting({
|
|
349
|
+
flowId: flow.flowId,
|
|
350
|
+
expectedRevision: flow.revision,
|
|
351
|
+
currentStep: waitingStep,
|
|
352
|
+
waitJson: buildApprovalWaitState(envelope)
|
|
353
|
+
});
|
|
354
|
+
return taskFlow.finish({
|
|
355
|
+
flowId: flow.flowId,
|
|
356
|
+
expectedRevision: flow.revision
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
function buildEnvelopeError(envelope) {
|
|
360
|
+
return new Error(envelope.error.message);
|
|
361
|
+
}
|
|
362
|
+
async function runManagedLobsterFlow(params) {
|
|
363
|
+
const flow = params.taskFlow.createManaged({
|
|
364
|
+
controllerId: params.controllerId,
|
|
365
|
+
goal: params.goal,
|
|
366
|
+
currentStep: params.currentStep ?? "run_lobster",
|
|
367
|
+
...params.stateJson !== void 0 ? { stateJson: params.stateJson } : {}
|
|
368
|
+
});
|
|
369
|
+
try {
|
|
370
|
+
const envelope = await params.runner.run(params.runnerParams);
|
|
371
|
+
const mutation = applyEnvelopeToFlow({
|
|
372
|
+
taskFlow: params.taskFlow,
|
|
373
|
+
flow,
|
|
374
|
+
envelope,
|
|
375
|
+
waitingStep: params.waitingStep ?? "await_lobster_approval"
|
|
376
|
+
});
|
|
377
|
+
if (!envelope.ok) return {
|
|
378
|
+
ok: false,
|
|
379
|
+
flow,
|
|
380
|
+
mutation,
|
|
381
|
+
error: buildEnvelopeError(envelope)
|
|
382
|
+
};
|
|
383
|
+
return {
|
|
384
|
+
ok: true,
|
|
385
|
+
envelope,
|
|
386
|
+
flow,
|
|
387
|
+
mutation
|
|
388
|
+
};
|
|
389
|
+
} catch (error) {
|
|
390
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
391
|
+
try {
|
|
392
|
+
return {
|
|
393
|
+
ok: false,
|
|
394
|
+
flow,
|
|
395
|
+
mutation: params.taskFlow.fail({
|
|
396
|
+
flowId: flow.flowId,
|
|
397
|
+
expectedRevision: flow.revision
|
|
398
|
+
}),
|
|
399
|
+
error: err
|
|
400
|
+
};
|
|
401
|
+
} catch {
|
|
402
|
+
return {
|
|
403
|
+
ok: false,
|
|
404
|
+
flow,
|
|
405
|
+
error: err
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function resumeManagedLobsterFlow(params) {
|
|
411
|
+
const resumed = params.taskFlow.resume({
|
|
412
|
+
flowId: params.flowId,
|
|
413
|
+
expectedRevision: params.expectedRevision,
|
|
414
|
+
status: "running",
|
|
415
|
+
currentStep: params.currentStep ?? "resume_lobster"
|
|
416
|
+
});
|
|
417
|
+
if (!resumed.applied) return {
|
|
418
|
+
ok: false,
|
|
419
|
+
mutation: resumed,
|
|
420
|
+
error: /* @__PURE__ */ new Error(`TaskFlow resume failed: ${resumed.code}`)
|
|
421
|
+
};
|
|
422
|
+
try {
|
|
423
|
+
const envelope = await params.runner.run(params.runnerParams);
|
|
424
|
+
const mutation = applyEnvelopeToFlow({
|
|
425
|
+
taskFlow: params.taskFlow,
|
|
426
|
+
flow: resumed.flow,
|
|
427
|
+
envelope,
|
|
428
|
+
waitingStep: params.waitingStep ?? "await_lobster_approval"
|
|
429
|
+
});
|
|
430
|
+
if (!envelope.ok) return {
|
|
431
|
+
ok: false,
|
|
432
|
+
flow: resumed.flow,
|
|
433
|
+
mutation,
|
|
434
|
+
error: buildEnvelopeError(envelope)
|
|
435
|
+
};
|
|
436
|
+
return {
|
|
437
|
+
ok: true,
|
|
438
|
+
envelope,
|
|
439
|
+
flow: resumed.flow,
|
|
440
|
+
mutation
|
|
441
|
+
};
|
|
442
|
+
} catch (error) {
|
|
443
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
444
|
+
try {
|
|
445
|
+
const mutation = params.taskFlow.fail({
|
|
446
|
+
flowId: params.flowId,
|
|
447
|
+
expectedRevision: resumed.flow.revision
|
|
448
|
+
});
|
|
449
|
+
return {
|
|
450
|
+
ok: false,
|
|
451
|
+
flow: resumed.flow,
|
|
452
|
+
mutation,
|
|
453
|
+
error: err
|
|
454
|
+
};
|
|
455
|
+
} catch {
|
|
456
|
+
return {
|
|
457
|
+
ok: false,
|
|
458
|
+
flow: resumed.flow,
|
|
459
|
+
error: err
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region extensions/lobster/src/lobster-tool.ts
|
|
466
|
+
function readOptionalTrimmedString(value, fieldName) {
|
|
467
|
+
if (value === void 0) return;
|
|
468
|
+
if (typeof value !== "string") throw new Error(`${fieldName} must be a string`);
|
|
469
|
+
const trimmed = value.trim();
|
|
470
|
+
return trimmed ? trimmed : void 0;
|
|
471
|
+
}
|
|
472
|
+
function readOptionalNumber(value, fieldName) {
|
|
473
|
+
if (value === void 0) return;
|
|
474
|
+
if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${fieldName} must be an integer`);
|
|
475
|
+
return value;
|
|
476
|
+
}
|
|
477
|
+
function readOptionalBoolean(value, fieldName) {
|
|
478
|
+
if (value === void 0) return;
|
|
479
|
+
if (typeof value !== "boolean") throw new Error(`${fieldName} must be a boolean`);
|
|
480
|
+
return value;
|
|
481
|
+
}
|
|
482
|
+
function parseOptionalFlowStateJson(value) {
|
|
483
|
+
if (value === void 0) return;
|
|
484
|
+
if (typeof value !== "string") throw new Error("flowStateJson must be a JSON string");
|
|
485
|
+
try {
|
|
486
|
+
return JSON.parse(value);
|
|
487
|
+
} catch {
|
|
488
|
+
throw new Error("flowStateJson must be valid JSON");
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function parseRunFlowParams(params) {
|
|
492
|
+
const controllerId = readOptionalTrimmedString(params.flowControllerId, "flowControllerId");
|
|
493
|
+
const goal = readOptionalTrimmedString(params.flowGoal, "flowGoal");
|
|
494
|
+
const currentStep = readOptionalTrimmedString(params.flowCurrentStep, "flowCurrentStep");
|
|
495
|
+
const waitingStep = readOptionalTrimmedString(params.flowWaitingStep, "flowWaitingStep");
|
|
496
|
+
const stateJson = parseOptionalFlowStateJson(params.flowStateJson);
|
|
497
|
+
const resumeFlowId = readOptionalTrimmedString(params.flowId, "flowId");
|
|
498
|
+
const resumeRevision = readOptionalNumber(params.flowExpectedRevision, "flowExpectedRevision");
|
|
499
|
+
if (!(controllerId !== void 0 || goal !== void 0 || currentStep !== void 0 || waitingStep !== void 0 || stateJson !== void 0)) return null;
|
|
500
|
+
if (resumeFlowId !== void 0 || resumeRevision !== void 0) throw new Error("run action does not accept flowId or flowExpectedRevision");
|
|
501
|
+
if (!controllerId) throw new Error("flowControllerId required when using managed TaskFlow run mode");
|
|
502
|
+
if (!goal) throw new Error("flowGoal required when using managed TaskFlow run mode");
|
|
503
|
+
return {
|
|
504
|
+
controllerId,
|
|
505
|
+
goal,
|
|
506
|
+
...currentStep ? { currentStep } : {},
|
|
507
|
+
...waitingStep ? { waitingStep } : {},
|
|
508
|
+
...stateJson !== void 0 ? { stateJson } : {}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function parseResumeFlowParams(params) {
|
|
512
|
+
const flowId = readOptionalTrimmedString(params.flowId, "flowId");
|
|
513
|
+
const expectedRevision = readOptionalNumber(params.flowExpectedRevision, "flowExpectedRevision");
|
|
514
|
+
const currentStep = readOptionalTrimmedString(params.flowCurrentStep, "flowCurrentStep");
|
|
515
|
+
const waitingStep = readOptionalTrimmedString(params.flowWaitingStep, "flowWaitingStep");
|
|
516
|
+
const token = readOptionalTrimmedString(params.token, "token");
|
|
517
|
+
const approvalId = readOptionalTrimmedString(params.approvalId, "approvalId");
|
|
518
|
+
const approve = readOptionalBoolean(params.approve, "approve");
|
|
519
|
+
const runControllerId = readOptionalTrimmedString(params.flowControllerId, "flowControllerId");
|
|
520
|
+
const runGoal = readOptionalTrimmedString(params.flowGoal, "flowGoal");
|
|
521
|
+
const stateJson = params.flowStateJson;
|
|
522
|
+
if (!(flowId !== void 0 || expectedRevision !== void 0 || currentStep !== void 0 || waitingStep !== void 0)) return null;
|
|
523
|
+
if (runControllerId !== void 0 || runGoal !== void 0 || stateJson !== void 0) throw new Error("resume action does not accept flowControllerId, flowGoal, or flowStateJson");
|
|
524
|
+
if (!flowId) throw new Error("flowId required when using managed TaskFlow resume mode");
|
|
525
|
+
if (expectedRevision === void 0) throw new Error("flowExpectedRevision required when using managed TaskFlow resume mode");
|
|
526
|
+
if (!token && !approvalId) throw new Error("token or approvalId required when using managed TaskFlow resume mode");
|
|
527
|
+
if (approve === void 0) throw new Error("approve required when using managed TaskFlow resume mode");
|
|
528
|
+
return {
|
|
529
|
+
flowId,
|
|
530
|
+
expectedRevision,
|
|
531
|
+
...currentStep ? { currentStep } : {},
|
|
532
|
+
...waitingStep ? { waitingStep } : {}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function formatManagedFlowResult(result) {
|
|
536
|
+
const details = {
|
|
537
|
+
...result.envelope && typeof result.envelope === "object" && !Array.isArray(result.envelope) ? result.envelope : { envelope: result.envelope },
|
|
538
|
+
flow: result.flow,
|
|
539
|
+
mutation: result.mutation
|
|
540
|
+
};
|
|
541
|
+
return {
|
|
542
|
+
content: [{
|
|
543
|
+
type: "text",
|
|
544
|
+
text: JSON.stringify(details, null, 2)
|
|
545
|
+
}],
|
|
546
|
+
details
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function requireTaskFlowRuntime(taskFlow, action) {
|
|
550
|
+
if (!taskFlow) throw new Error(`Managed TaskFlow ${action} mode requires a bound taskFlow runtime`);
|
|
551
|
+
return taskFlow;
|
|
552
|
+
}
|
|
553
|
+
function resolveManagedFlowToolResult(result) {
|
|
554
|
+
if (!result.ok) throw result.error;
|
|
555
|
+
return formatManagedFlowResult(result);
|
|
556
|
+
}
|
|
557
|
+
function createLobsterTool(api, options) {
|
|
558
|
+
const runner = options?.runner ?? createEmbeddedLobsterRunner();
|
|
559
|
+
return {
|
|
560
|
+
name: "lobster",
|
|
561
|
+
label: "Lobster Workflow",
|
|
562
|
+
description: "Run Lobster pipelines as a local-first workflow runtime (typed JSON envelope + resumable approvals).",
|
|
563
|
+
parameters: Type.Object({
|
|
564
|
+
action: Type.Unsafe({
|
|
565
|
+
type: "string",
|
|
566
|
+
enum: ["run", "resume"]
|
|
567
|
+
}),
|
|
568
|
+
pipeline: Type.Optional(Type.String()),
|
|
569
|
+
argsJson: Type.Optional(Type.String()),
|
|
570
|
+
token: Type.Optional(Type.String()),
|
|
571
|
+
approvalId: Type.Optional(Type.String()),
|
|
572
|
+
approve: Type.Optional(Type.Boolean()),
|
|
573
|
+
cwd: Type.Optional(Type.String({ description: "Relative working directory (optional). Must stay within the gateway working directory." })),
|
|
574
|
+
timeoutMs: Type.Optional(Type.Number()),
|
|
575
|
+
maxStdoutBytes: Type.Optional(Type.Number()),
|
|
576
|
+
flowControllerId: Type.Optional(Type.String()),
|
|
577
|
+
flowGoal: Type.Optional(Type.String()),
|
|
578
|
+
flowStateJson: Type.Optional(Type.String()),
|
|
579
|
+
flowId: Type.Optional(Type.String()),
|
|
580
|
+
flowExpectedRevision: Type.Optional(Type.Number()),
|
|
581
|
+
flowCurrentStep: Type.Optional(Type.String()),
|
|
582
|
+
flowWaitingStep: Type.Optional(Type.String())
|
|
583
|
+
}),
|
|
584
|
+
async execute(_id, params) {
|
|
585
|
+
const action = typeof params.action === "string" ? params.action.trim() : "";
|
|
586
|
+
if (!action) throw new Error("action required");
|
|
587
|
+
if (action !== "run" && action !== "resume") throw new Error(`Unknown action: ${action}`);
|
|
588
|
+
const cwd = resolveLobsterCwd(params.cwd);
|
|
589
|
+
const timeoutMs = typeof params.timeoutMs === "number" ? params.timeoutMs : 2e4;
|
|
590
|
+
const maxStdoutBytes = typeof params.maxStdoutBytes === "number" ? params.maxStdoutBytes : 512e3;
|
|
591
|
+
if (api.runtime?.version && api.logger?.debug) api.logger.debug(`lobster plugin runtime=${api.runtime.version}`);
|
|
592
|
+
const runnerParams = {
|
|
593
|
+
action,
|
|
594
|
+
...typeof params.pipeline === "string" ? { pipeline: params.pipeline } : {},
|
|
595
|
+
...typeof params.argsJson === "string" ? { argsJson: params.argsJson } : {},
|
|
596
|
+
...typeof params.token === "string" ? { token: params.token } : {},
|
|
597
|
+
...typeof params.approvalId === "string" ? { approvalId: params.approvalId } : {},
|
|
598
|
+
...typeof params.approve === "boolean" ? { approve: params.approve } : {},
|
|
599
|
+
cwd,
|
|
600
|
+
timeoutMs,
|
|
601
|
+
maxStdoutBytes
|
|
602
|
+
};
|
|
603
|
+
const taskFlow = options?.taskFlow;
|
|
604
|
+
if (action === "run") {
|
|
605
|
+
const flowParams = parseRunFlowParams(params);
|
|
606
|
+
if (flowParams) return resolveManagedFlowToolResult(await runManagedLobsterFlow({
|
|
607
|
+
taskFlow: requireTaskFlowRuntime(taskFlow, "run"),
|
|
608
|
+
runner,
|
|
609
|
+
runnerParams,
|
|
610
|
+
controllerId: flowParams.controllerId,
|
|
611
|
+
goal: flowParams.goal,
|
|
612
|
+
...flowParams.stateJson !== void 0 ? { stateJson: flowParams.stateJson } : {},
|
|
613
|
+
...flowParams.currentStep ? { currentStep: flowParams.currentStep } : {},
|
|
614
|
+
...flowParams.waitingStep ? { waitingStep: flowParams.waitingStep } : {}
|
|
615
|
+
}));
|
|
616
|
+
} else {
|
|
617
|
+
const flowParams = parseResumeFlowParams(params);
|
|
618
|
+
if (flowParams) return resolveManagedFlowToolResult(await resumeManagedLobsterFlow({
|
|
619
|
+
taskFlow: requireTaskFlowRuntime(taskFlow, "resume"),
|
|
620
|
+
runner,
|
|
621
|
+
runnerParams,
|
|
622
|
+
flowId: flowParams.flowId,
|
|
623
|
+
expectedRevision: flowParams.expectedRevision,
|
|
624
|
+
...flowParams.currentStep ? { currentStep: flowParams.currentStep } : {},
|
|
625
|
+
...flowParams.waitingStep ? { waitingStep: flowParams.waitingStep } : {}
|
|
626
|
+
}));
|
|
627
|
+
}
|
|
628
|
+
const envelope = await runner.run(runnerParams);
|
|
629
|
+
if (!envelope.ok) throw new Error(envelope.error.message);
|
|
630
|
+
return {
|
|
631
|
+
content: [{
|
|
632
|
+
type: "text",
|
|
633
|
+
text: JSON.stringify(envelope, null, 2)
|
|
634
|
+
}],
|
|
635
|
+
details: envelope
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region extensions/lobster/index.ts
|
|
642
|
+
var lobster_default = definePluginEntry({
|
|
643
|
+
id: "lobster",
|
|
644
|
+
name: "Lobster",
|
|
645
|
+
description: "Optional local shell helper tools",
|
|
646
|
+
register(api) {
|
|
647
|
+
api.registerTool(((ctx) => {
|
|
648
|
+
if (ctx.sandboxed) return null;
|
|
649
|
+
return createLobsterTool(api, { taskFlow: api.runtime?.tasks.managedFlows && ctx.sessionKey ? api.runtime.tasks.managedFlows.fromToolContext(ctx) : void 0 });
|
|
650
|
+
}), { optional: true });
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
//#endregion
|
|
654
|
+
export { lobster_default as default };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import { applyWindowsSpawnProgramPolicy, materializeWindowsSpawnProgram, resolveWindowsSpawnProgramCandidate } from "openclaw/plugin-sdk/windows-spawn";
|
|
3
|
+
export { applyWindowsSpawnProgramPolicy, definePluginEntry, materializeWindowsSpawnProgram, resolveWindowsSpawnProgramCandidate };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/lobster",
|
|
3
|
-
"version": "2026.5.2",
|
|
3
|
+
"version": "2026.5.3-beta.2",
|
|
4
4
|
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,14 +25,31 @@
|
|
|
25
25
|
"minHostVersion": ">=2026.4.25"
|
|
26
26
|
},
|
|
27
27
|
"compat": {
|
|
28
|
-
"pluginApi": ">=2026.5.2"
|
|
28
|
+
"pluginApi": ">=2026.5.3-beta.2"
|
|
29
29
|
},
|
|
30
30
|
"build": {
|
|
31
|
-
"openclawVersion": "2026.5.2"
|
|
31
|
+
"openclawVersion": "2026.5.3-beta.2"
|
|
32
32
|
},
|
|
33
33
|
"release": {
|
|
34
34
|
"publishToClawHub": true,
|
|
35
35
|
"publishToNpm": true
|
|
36
|
+
},
|
|
37
|
+
"runtimeExtensions": [
|
|
38
|
+
"./dist/index.js"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist/**",
|
|
43
|
+
"openclaw.plugin.json",
|
|
44
|
+
"README.md",
|
|
45
|
+
"SKILL.md"
|
|
46
|
+
],
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"openclaw": ">=2026.5.3-beta.2"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"openclaw": {
|
|
52
|
+
"optional": true
|
|
36
53
|
}
|
|
37
54
|
}
|
|
38
55
|
}
|