lobstah-openclaw-plugin 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 aequitas labs LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # lobstah-openclaw-plugin
2
+
3
+ OpenClaw plugin for [lobstah](https://www.npmjs.com/package/lobstah), the
4
+ supervisor for local coding agents. Fleet agents get four tools —
5
+ `lobstah_dispatch`, `lobstah_status`, `lobstah_send`, `lobstah_cancel` —
6
+ and operators get a `/lobstah` chat command. Calls translate into the same
7
+ file queue the CLI writes.
8
+
9
+ Install into a gateway host that also runs `lobstah daemon`:
10
+
11
+ ```bash
12
+ openclaw plugins install lobstah-openclaw-plugin
13
+ ```
14
+
15
+ Docs: https://github.com/aequitas-labs/lobstah/blob/main/docs/openclaw.md
package/dist/index.js ADDED
@@ -0,0 +1,327 @@
1
+ // apps/node/src/index.ts
2
+ import { definePluginEntry } from "openclaw/plugin-sdk/core";
3
+
4
+ // apps/node/src/tools.ts
5
+ import * as fs6 from "node:fs";
6
+ import * as path4 from "node:path";
7
+ import { randomUUID } from "node:crypto";
8
+
9
+ // packages/core/dist/types.js
10
+ var VERBS = ["working", "needs-decision", "blocked", "paused", "done", "failed"];
11
+ var TERMINAL_VERBS = ["done", "failed"];
12
+
13
+ // packages/core/dist/paths.js
14
+ import * as os from "node:os";
15
+ import * as path from "node:path";
16
+ import * as fs from "node:fs";
17
+ function lobstahHome() {
18
+ return process.env.LOBSTAH_HOME ?? path.join(os.homedir(), ".lobstah");
19
+ }
20
+ function laneRoot(lane) {
21
+ return lane === "work" ? lobstahHome() : path.join(lobstahHome(), "chores");
22
+ }
23
+ function laneDirs(lane) {
24
+ const root = laneRoot(lane);
25
+ return {
26
+ queue: path.join(root, "queue"),
27
+ active: path.join(root, "active"),
28
+ done: path.join(root, "done"),
29
+ state: path.join(root, "state"),
30
+ inbox: path.join(root, "inbox")
31
+ };
32
+ }
33
+ function ensureLayout() {
34
+ for (const lane of ["work", "chore"]) {
35
+ for (const dir of Object.values(laneDirs(lane)))
36
+ fs.mkdirSync(dir, { recursive: true });
37
+ }
38
+ fs.mkdirSync(path.join(lobstahHome(), "worktrees"), { recursive: true });
39
+ }
40
+ function statusPath(id, lane) {
41
+ return path.join(laneDirs(lane).state, `${id}.status`);
42
+ }
43
+ function eventsPath(id, lane) {
44
+ return path.join(laneDirs(lane).state, `${id}.events`);
45
+ }
46
+
47
+ // packages/core/dist/queue.js
48
+ import * as fs2 from "node:fs";
49
+ import * as path2 from "node:path";
50
+ function atomicWrite(file, content) {
51
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
52
+ fs2.writeFileSync(tmp, content);
53
+ fs2.renameSync(tmp, file);
54
+ }
55
+ function enqueue(d, lane = "work") {
56
+ if (!d.id || !d.repo || !d.brief) {
57
+ throw new Error("descriptor requires id, repo, and brief");
58
+ }
59
+ atomicWrite(path2.join(laneDirs(lane).queue, `${d.id}.json`), JSON.stringify(d, null, 2));
60
+ }
61
+ function requestCancel(id, lane) {
62
+ fs2.writeFileSync(path2.join(laneDirs(lane).active, id, "cancel"), (/* @__PURE__ */ new Date()).toISOString());
63
+ }
64
+
65
+ // packages/core/dist/status.js
66
+ import * as fs3 from "node:fs";
67
+ function isVerb(v) {
68
+ return VERBS.includes(v);
69
+ }
70
+ function readStatusLog(id, lane) {
71
+ const file = statusPath(id, lane);
72
+ if (!fs3.existsSync(file))
73
+ return [];
74
+ return fs3.readFileSync(file, "utf8").split("\n").filter(Boolean).flatMap((line) => {
75
+ try {
76
+ const parsed = JSON.parse(line);
77
+ return isVerb(parsed.verb) ? [parsed] : [];
78
+ } catch {
79
+ return [];
80
+ }
81
+ });
82
+ }
83
+ function reconcile({ log, lastEventAt: lastEventAt2, now = Date.now(), busyThresholdMs = 12e4 }) {
84
+ const last = log[log.length - 1];
85
+ if (last && TERMINAL_VERBS.includes(last.verb))
86
+ return last.verb;
87
+ const busy = lastEventAt2 !== void 0 && now - lastEventAt2 < busyThresholdMs;
88
+ if (busy)
89
+ return last && last.verb !== "working" ? last.verb : "working";
90
+ if (last)
91
+ return last.verb;
92
+ return "unknown";
93
+ }
94
+
95
+ // packages/core/dist/events.js
96
+ import * as fs4 from "node:fs";
97
+ function lastEventAt(id, lane) {
98
+ try {
99
+ return fs4.statSync(eventsPath(id, lane)).mtimeMs;
100
+ } catch {
101
+ return void 0;
102
+ }
103
+ }
104
+
105
+ // packages/core/dist/inbox.js
106
+ import * as fs5 from "node:fs";
107
+ import * as path3 from "node:path";
108
+ function inboxDir(id, lane) {
109
+ return path3.join(laneDirs(lane).inbox, id);
110
+ }
111
+ function sendMessage(id, lane, text2) {
112
+ const dir = inboxDir(id, lane);
113
+ fs5.mkdirSync(path3.join(dir, "handled"), { recursive: true });
114
+ const existing = fs5.readdirSync(dir).filter((f) => f.endsWith(".msg")).length;
115
+ const handled = fs5.readdirSync(path3.join(dir, "handled")).length;
116
+ const name = `${String(existing + handled + 1).padStart(3, "0")}.msg`;
117
+ const tmp = path3.join(dir, `.tmp-${process.pid}-${Date.now()}`);
118
+ fs5.writeFileSync(tmp, text2);
119
+ fs5.renameSync(tmp, path3.join(dir, name));
120
+ return name;
121
+ }
122
+
123
+ // packages/core/dist/toon.js
124
+ function cell(v) {
125
+ if (v === void 0 || v === null)
126
+ return "";
127
+ const s = String(v);
128
+ return s.includes(",") || s.includes("\n") ? JSON.stringify(s) : s;
129
+ }
130
+ function toonKV(obj) {
131
+ return Object.entries(obj).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}: ${cell(v)}`).join("\n");
132
+ }
133
+ function toonTable(name, rows, cols) {
134
+ const header = `${name}[${rows.length}]{${cols.join(",")}}:`;
135
+ if (rows.length === 0)
136
+ return header;
137
+ return [header, ...rows.map((r) => ` ${cols.map((c) => cell(r[c])).join(",")}`)].join("\n");
138
+ }
139
+
140
+ // apps/node/src/tools.ts
141
+ function text(payload, details = {}) {
142
+ return { content: [{ type: "text", text: payload }], details };
143
+ }
144
+ function findLane(id) {
145
+ for (const lane of ["work", "chore"]) {
146
+ const d = laneDirs(lane);
147
+ if (fs6.existsSync(path4.join(d.active, id)) || fs6.existsSync(path4.join(d.queue, `${id}.json`)) || fs6.existsSync(path4.join(d.done, id)) || fs6.existsSync(path4.join(d.state, `${id}.status`))) {
148
+ return lane;
149
+ }
150
+ }
151
+ throw new Error(`unknown dispatch ${id}`);
152
+ }
153
+ function stateOf(id, lane) {
154
+ return reconcile({ log: readStatusLog(id, lane), lastEventAt: lastEventAt(id, lane) });
155
+ }
156
+ function param(params, key) {
157
+ const v = params[key];
158
+ return typeof v === "string" && v.length > 0 ? v : void 0;
159
+ }
160
+ function dispatchTool() {
161
+ return {
162
+ name: "lobstah_dispatch",
163
+ label: "Lobstah dispatch",
164
+ description: "Queue a supervised coding-agent dispatch on this host. The lobstah daemon claims it, allocates an isolated git worktree, runs the agent, and reports status to disk. Returns the dispatch id.",
165
+ parameters: {
166
+ type: "object",
167
+ properties: {
168
+ repo: { type: "string", description: "Lobstah repo key from the host config" },
169
+ brief: { type: "string", description: "The full work brief for the dispatched agent" },
170
+ harness: { type: "string", enum: ["claude", "codex"] },
171
+ model: { type: "string" },
172
+ effort: { type: "string" },
173
+ followUp: { type: "string", description: "Earlier dispatch UUID whose session to fork" }
174
+ },
175
+ required: ["repo", "brief"],
176
+ additionalProperties: false
177
+ },
178
+ async execute(_id, params) {
179
+ ensureLayout();
180
+ const repo = param(params, "repo");
181
+ const brief = param(params, "brief");
182
+ if (!repo || !brief) throw new Error("lobstah_dispatch requires repo and brief");
183
+ const d = {
184
+ id: randomUUID(),
185
+ repo,
186
+ brief,
187
+ harness: param(params, "harness"),
188
+ model: param(params, "model"),
189
+ effort: param(params, "effort"),
190
+ followUp: param(params, "followUp")
191
+ };
192
+ enqueue(d, "work");
193
+ return text(toonKV({ id: d.id, repo, queued: (/* @__PURE__ */ new Date()).toISOString() }), { id: d.id });
194
+ }
195
+ };
196
+ }
197
+ function statusTool() {
198
+ return {
199
+ name: "lobstah_status",
200
+ label: "Lobstah status",
201
+ description: "Reconciled state of one dispatch (by id) or every active dispatch on this host. States: working, needs-decision, blocked, paused, done, failed, unknown.",
202
+ parameters: {
203
+ type: "object",
204
+ properties: { id: { type: "string" } },
205
+ additionalProperties: false
206
+ },
207
+ async execute(_id, params) {
208
+ ensureLayout();
209
+ const id = param(params, "id");
210
+ if (id) {
211
+ const lane = findLane(id);
212
+ const log = readStatusLog(id, lane);
213
+ const state = stateOf(id, lane);
214
+ return text(toonKV({ id, lane, state, lastNote: log.at(-1)?.note }), { id, state });
215
+ }
216
+ const rows = ["work", "chore"].flatMap(
217
+ (lane) => fs6.readdirSync(laneDirs(lane).active).filter((f) => !f.startsWith(".")).map((did) => ({ id: did, lane, state: stateOf(did, lane) }))
218
+ );
219
+ return text(toonTable("active", rows, ["id", "lane", "state"]), { count: rows.length });
220
+ }
221
+ };
222
+ }
223
+ function sendTool() {
224
+ return {
225
+ name: "lobstah_send",
226
+ label: "Lobstah send",
227
+ description: "Send an instruction to a running dispatch. Delivered into its inbox between agent turns.",
228
+ parameters: {
229
+ type: "object",
230
+ properties: { id: { type: "string" }, message: { type: "string" } },
231
+ required: ["id", "message"],
232
+ additionalProperties: false
233
+ },
234
+ async execute(_id, params) {
235
+ const id = param(params, "id");
236
+ const message = param(params, "message");
237
+ if (!id || !message) throw new Error("lobstah_send requires id and message");
238
+ const name = sendMessage(id, findLane(id), message);
239
+ return text(toonKV({ id, queued: name }), { id });
240
+ }
241
+ };
242
+ }
243
+ function cancelTool() {
244
+ return {
245
+ name: "lobstah_cancel",
246
+ label: "Lobstah cancel",
247
+ description: "Request cancellation of an active dispatch. The supervisor kills it between polls.",
248
+ parameters: {
249
+ type: "object",
250
+ properties: { id: { type: "string" } },
251
+ required: ["id"],
252
+ additionalProperties: false
253
+ },
254
+ async execute(_id, params) {
255
+ const id = param(params, "id");
256
+ if (!id) throw new Error("lobstah_cancel requires id");
257
+ requestCancel(id, findLane(id));
258
+ return text(toonKV({ id, cancel: "requested" }), { id });
259
+ }
260
+ };
261
+ }
262
+
263
+ // apps/node/src/index.ts
264
+ var index_default = definePluginEntry({
265
+ id: "lobstah",
266
+ name: "Lobstah",
267
+ description: "Dispatch supervised local coding-agent work through the lobstah daemon on this host.",
268
+ register(api) {
269
+ api.registerTool(dispatchTool(), { optional: true });
270
+ api.registerTool(statusTool(), { optional: true });
271
+ api.registerTool(sendTool(), { optional: true });
272
+ api.registerTool(cancelTool(), { optional: true });
273
+ api.registerCommand({
274
+ name: "lobstah",
275
+ description: "Lobstah dispatch status on this host (/lobstah [id])",
276
+ acceptsArgs: true,
277
+ async handler(ctx) {
278
+ const tool = statusTool();
279
+ const arg = ctx.args?.trim();
280
+ const result = await tool.execute("cmd", arg ? { id: arg } : {});
281
+ return { text: result.content[0]?.text ?? "no output" };
282
+ }
283
+ });
284
+ }
285
+ });
286
+ export {
287
+ index_default as default
288
+ };
289
+ /*! Bundled license information:
290
+
291
+ smol-toml/dist/date.js:
292
+ smol-toml/dist/error.js:
293
+ smol-toml/dist/util.js:
294
+ smol-toml/dist/primitive.js:
295
+ smol-toml/dist/extract.js:
296
+ smol-toml/dist/struct.js:
297
+ smol-toml/dist/parse.js:
298
+ smol-toml/dist/stringify.js:
299
+ smol-toml/dist/index.js:
300
+ (*!
301
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
302
+ * SPDX-License-Identifier: BSD-3-Clause
303
+ *
304
+ * Redistribution and use in source and binary forms, with or without
305
+ * modification, are permitted provided that the following conditions are met:
306
+ *
307
+ * 1. Redistributions of source code must retain the above copyright notice, this
308
+ * list of conditions and the following disclaimer.
309
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
310
+ * this list of conditions and the following disclaimer in the
311
+ * documentation and/or other materials provided with the distribution.
312
+ * 3. Neither the name of the copyright holder nor the names of its contributors
313
+ * may be used to endorse or promote products derived from this software without
314
+ * specific prior written permission.
315
+ *
316
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
317
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
318
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
319
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
320
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
321
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
322
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
323
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
324
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
325
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
326
+ *)
327
+ */
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "lobstah",
3
+ "name": "Lobstah",
4
+ "description": "Dispatch supervised local coding-agent work through the lobstah daemon on this host.",
5
+ "activation": { "onStartup": true },
6
+ "contracts": {
7
+ "tools": ["lobstah_dispatch", "lobstah_status", "lobstah_send", "lobstah_cancel"]
8
+ },
9
+ "configSchema": { "type": "object", "properties": {}, "additionalProperties": false }
10
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "lobstah-openclaw-plugin",
3
+ "version": "0.1.0",
4
+ "description": "OpenClaw plugin for lobstah: gives fleet agents dispatch/status/send/cancel tools and operators a /lobstah command.",
5
+ "license": "MIT",
6
+ "author": "aequitas labs LLC",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "files": [
10
+ "dist",
11
+ "openclaw.plugin.json",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "keywords": [
19
+ "openclaw",
20
+ "openclaw-plugin",
21
+ "lobstah",
22
+ "coding-agents"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/aequitas-labs/lobstah.git"
27
+ },
28
+ "peerDependencies": {
29
+ "openclaw": ">=2026.8.2"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "openclaw": {
33
+ "optional": true
34
+ }
35
+ },
36
+ "openclaw": {
37
+ "extensions": [
38
+ "./dist/index.js"
39
+ ]
40
+ }
41
+ }