@zswarm/core 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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/dist/errors.d.ts +5 -0
  4. package/dist/errors.js +9 -0
  5. package/dist/exec.d.ts +32 -0
  6. package/dist/exec.js +118 -0
  7. package/dist/git.d.ts +51 -0
  8. package/dist/git.js +190 -0
  9. package/dist/harness.d.ts +21 -0
  10. package/dist/harness.js +98 -0
  11. package/dist/index.d.ts +23 -0
  12. package/dist/index.js +22 -0
  13. package/dist/keys.d.ts +15 -0
  14. package/dist/keys.js +168 -0
  15. package/dist/ops/broadcast.d.ts +19 -0
  16. package/dist/ops/broadcast.js +113 -0
  17. package/dist/ops/bus.d.ts +59 -0
  18. package/dist/ops/bus.js +284 -0
  19. package/dist/ops/delivery.d.ts +43 -0
  20. package/dist/ops/delivery.js +191 -0
  21. package/dist/ops/dispatch.d.ts +4 -0
  22. package/dist/ops/dispatch.js +331 -0
  23. package/dist/ops/guards.d.ts +15 -0
  24. package/dist/ops/guards.js +36 -0
  25. package/dist/ops/log.d.ts +4 -0
  26. package/dist/ops/log.js +26 -0
  27. package/dist/ops/panes.d.ts +13 -0
  28. package/dist/ops/panes.js +111 -0
  29. package/dist/ops/review.d.ts +9 -0
  30. package/dist/ops/review.js +108 -0
  31. package/dist/ops/signals.d.ts +10 -0
  32. package/dist/ops/signals.js +86 -0
  33. package/dist/ops/spawn.d.ts +5 -0
  34. package/dist/ops/spawn.js +98 -0
  35. package/dist/ops/status.d.ts +36 -0
  36. package/dist/ops/status.js +192 -0
  37. package/dist/ops/tail.d.ts +19 -0
  38. package/dist/ops/tail.js +59 -0
  39. package/dist/ops/types.d.ts +26 -0
  40. package/dist/ops/types.js +1 -0
  41. package/dist/ops/util.d.ts +48 -0
  42. package/dist/ops/util.js +85 -0
  43. package/dist/ops/wait.d.ts +19 -0
  44. package/dist/ops/wait.js +138 -0
  45. package/dist/ops/worktree.d.ts +21 -0
  46. package/dist/ops/worktree.js +144 -0
  47. package/dist/policy.d.ts +15 -0
  48. package/dist/policy.js +75 -0
  49. package/dist/schema.d.ts +26 -0
  50. package/dist/schema.js +489 -0
  51. package/dist/state.d.ts +52 -0
  52. package/dist/state.js +150 -0
  53. package/dist/zellij/args.d.ts +93 -0
  54. package/dist/zellij/args.js +261 -0
  55. package/dist/zellij/binary.d.ts +17 -0
  56. package/dist/zellij/binary.js +96 -0
  57. package/dist/zellij/bus.d.ts +91 -0
  58. package/dist/zellij/bus.js +279 -0
  59. package/dist/zellij/client.d.ts +162 -0
  60. package/dist/zellij/client.js +246 -0
  61. package/dist/zellij/panes.d.ts +21 -0
  62. package/dist/zellij/panes.js +110 -0
  63. package/dist/zellij/session.d.ts +14 -0
  64. package/dist/zellij/session.js +44 -0
  65. package/dist/zellij/tabs.d.ts +14 -0
  66. package/dist/zellij/tabs.js +56 -0
  67. package/package.json +34 -0
@@ -0,0 +1,191 @@
1
+ import { resolveHarness } from "../harness.js";
2
+ import { isTrue, numberArg } from "./util.js";
3
+ /** Cursor / Claude leave this in scrollback after a successful submit. */
4
+ const PASTE_MARKER = "[Pasted text";
5
+ export function senderLabel(args) {
6
+ return (typeof args.from === "string" && args.from.trim()) || "swarm";
7
+ }
8
+ export function bodyText(client, args, body) {
9
+ return isTrue(args.raw) ? body : client.formatPeerMessage(senderLabel(args), body);
10
+ }
11
+ /**
12
+ * Caller `submit=` always wins. With none given, the pane's harness picks
13
+ * auto vs double-enter (codex needs two Enters; the rest submit on one).
14
+ */
15
+ export function resolveSubmitMode(args, pane) {
16
+ const raw = args.submit;
17
+ if (raw === "none" || raw === "double-enter" || raw === "auto")
18
+ return raw;
19
+ if (raw !== undefined && raw !== null && raw !== "")
20
+ return "auto";
21
+ return resolveHarness(pane).submit;
22
+ }
23
+ function nonEmptyLines(screen) {
24
+ return screen
25
+ .replace(/\r/g, "")
26
+ .split("\n")
27
+ .map((line) => line.replace(/\s+$/, ""))
28
+ .filter((line) => line.length > 0);
29
+ }
30
+ function isPasteLine(line, needle) {
31
+ if (line.includes(PASTE_MARKER))
32
+ return true;
33
+ return needle.length > 0 && line.includes(needle);
34
+ }
35
+ /** True when the paste still sits in the composer (bottom of the screen). */
36
+ export function composerHolds(screen, body) {
37
+ const needle = body.slice(0, 40);
38
+ const lines = nonEmptyLines(screen);
39
+ const last3 = lines.slice(-3);
40
+ const last = lines[lines.length - 1] ?? "";
41
+ if (last.includes(PASTE_MARKER))
42
+ return true;
43
+ return needle.length > 0 && last3.some((line) => line.includes(needle));
44
+ }
45
+ /**
46
+ * Compare the pane before vs after the paste.
47
+ * Composer clearing (pasted text left the bottom input region) is submission.
48
+ * `[Pasted text` in scrollback is not evidence.
49
+ */
50
+ export function classifySubmit(before, after, body) {
51
+ const needle = body.slice(0, 40);
52
+ const beforeLines = nonEmptyLines(before);
53
+ const afterLines = nonEmptyLines(after);
54
+ const beforeNorm = beforeLines.join("\n");
55
+ const afterNorm = afterLines.join("\n");
56
+ const last3 = afterLines.slice(-3);
57
+ const last = afterLines[afterLines.length - 1] ?? "";
58
+ const markerInLastLine = last.includes(PASTE_MARKER);
59
+ const bodyInLastRegion = needle.length > 0 && last3.some((line) => line.includes(needle));
60
+ const holds = bodyInLastRegion || markerInLastLine;
61
+ if (afterNorm === beforeNorm) {
62
+ // Marker may only count when it sits in the last-line region *and*
63
+ // nothing else moved — leftover scrollback must not flip the answer.
64
+ if (markerInLastLine)
65
+ return false;
66
+ return "unverified";
67
+ }
68
+ if (holds) {
69
+ const withoutPaste = afterLines.filter((line) => !isPasteLine(line, needle));
70
+ if (withoutPaste.join("\n") === beforeNorm)
71
+ return false;
72
+ const lastPasteIdx = afterLines.reduce((found, line, i) => (isPasteLine(line, needle) ? i : found), -1);
73
+ const belowNovel = afterLines
74
+ .slice(lastPasteIdx + 1)
75
+ .filter((line) => !isPasteLine(line, needle))
76
+ .filter((line) => !beforeLines.includes(line));
77
+ return belowNovel.length > 0 ? true : false;
78
+ }
79
+ // Paste left the input region: still on screen, just not at the bottom.
80
+ if (needle.length > 0 && afterLines.some((line) => line.includes(needle))) {
81
+ return true;
82
+ }
83
+ return "unverified";
84
+ }
85
+ async function dumpOrNull(client, session, paneId) {
86
+ try {
87
+ return (await client.dumpPane({ session, paneId })).text;
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ async function verifySubmit(client, input) {
94
+ const { session, paneId, body, mode, settleMs, clock, before } = input;
95
+ if (mode === "none")
96
+ return "unverified";
97
+ if (mode === "double-enter") {
98
+ await clock.sleep(settleMs);
99
+ await client.sendKeys({ session, paneId, keys: ["Enter"] });
100
+ return "unverified";
101
+ }
102
+ if (before === null)
103
+ return "unverified";
104
+ await clock.sleep(settleMs);
105
+ const after = await dumpOrNull(client, session, paneId);
106
+ if (after === null)
107
+ return "unverified";
108
+ let verdict = classifySubmit(before, after, body);
109
+ if (verdict === true)
110
+ return true;
111
+ if (verdict === false) {
112
+ await client.sendKeys({ session, paneId, keys: ["Enter"] });
113
+ await clock.sleep(settleMs);
114
+ const rescued = await dumpOrNull(client, session, paneId);
115
+ if (rescued === null)
116
+ return "unverified";
117
+ verdict = classifySubmit(before, rescued, body);
118
+ if (verdict === true)
119
+ return true;
120
+ if (verdict === false)
121
+ return false;
122
+ }
123
+ // First look was inconclusive; TUIs often redraw after the first settle.
124
+ await clock.sleep(Math.min(Math.max(settleMs * 2, 800), 5000));
125
+ const later = await dumpOrNull(client, session, paneId);
126
+ if (later === null)
127
+ return "unverified";
128
+ if (composerHolds(after, body) && !composerHolds(later, body))
129
+ return true;
130
+ return classifySubmit(before, later, body);
131
+ }
132
+ /** Paste a body into one pane and record the attempt. */
133
+ export async function deliverTo(client, state, args, input) {
134
+ const { session, pane, body, op, at, clock } = input;
135
+ const from = senderLabel(args);
136
+ const mode = resolveSubmitMode(args, pane);
137
+ const settleMs = numberArg(args, "settleMs", 300, { min: 50, max: 5000 });
138
+ try {
139
+ let before = null;
140
+ if (mode === "auto") {
141
+ before = await dumpOrNull(client, session, pane.id);
142
+ }
143
+ const delivered = await client.injectPane({
144
+ session,
145
+ paneId: pane.id,
146
+ text: bodyText(client, args, body),
147
+ });
148
+ const submitted = await verifySubmit(client, {
149
+ session,
150
+ paneId: delivered.paneId,
151
+ body,
152
+ mode,
153
+ settleMs,
154
+ clock,
155
+ before,
156
+ });
157
+ state.appendLog({
158
+ at,
159
+ op,
160
+ session,
161
+ to: delivered.paneId,
162
+ from,
163
+ bytes: body.length,
164
+ ok: true,
165
+ detail: `submitted=${submitted}`,
166
+ });
167
+ return {
168
+ to: delivered.paneId,
169
+ ok: true,
170
+ delivery: "zellij_paste",
171
+ submitted,
172
+ };
173
+ }
174
+ catch (err) {
175
+ const code = err && typeof err === "object" && "code" in err
176
+ ? String(err.code)
177
+ : "failed";
178
+ const message = err instanceof Error ? err.message : String(err);
179
+ state.appendLog({
180
+ at,
181
+ op,
182
+ session,
183
+ to: pane.id,
184
+ from,
185
+ bytes: body.length,
186
+ ok: false,
187
+ detail: `${code}: ${message}`,
188
+ });
189
+ return { to: pane.id, ok: false, error: { code, message } };
190
+ }
191
+ }
@@ -0,0 +1,4 @@
1
+ import { type ZellijClient } from "../zellij/client.js";
2
+ import type { DispatchDeps, OpsResult } from "./types.js";
3
+ /** Shared MCP/CLI dispatch for zswarm ops. */
4
+ export declare function dispatchZswarm(args: Record<string, unknown>, client?: ZellijClient, deps?: DispatchDeps): Promise<OpsResult>;
@@ -0,0 +1,331 @@
1
+ import { ZellijError } from "../errors.js";
2
+ import { createGitClient } from "../git.js";
3
+ import { normalizeKeys } from "../keys.js";
4
+ import { createZellijClient } from "../zellij/client.js";
5
+ import { assertOpAllowed, assertPaneAllowed, loadPolicy, } from "../policy.js";
6
+ import { createStateStore } from "../state.js";
7
+ import { busToPanes } from "../zellij/bus.js";
8
+ import { broadcast } from "./broadcast.js";
9
+ import { busChanged, busOp, busScreens, busSnapshot, busWait } from "./bus.js";
10
+ import { dumpLayoutOp, focusTarget, listTabsOp, renameTarget, stackTargets, } from "./panes.js";
11
+ import { peerCheckpoint, peerDiff } from "./review.js";
12
+ import { deliverTo } from "./delivery.js";
13
+ import { assertNotPlugin, assertNotSelf, assertPaneExpects, assertWritable, } from "./guards.js";
14
+ import { readDeliveryLog } from "./log.js";
15
+ import { awaitSignal, listSignals, postSignal } from "./signals.js";
16
+ import { spawnPane } from "./spawn.js";
17
+ import { peerStatus } from "./status.js";
18
+ import { tailPane } from "./tail.js";
19
+ import { OP_NAMES } from "../schema.js";
20
+ import { dumpMaxChars, fail, isTrue, isVerbose, paneViewBus, paneViewFull, paneViewSlim, truncateDumpText, } from "./util.js";
21
+ import { waitForPane } from "./wait.js";
22
+ import { listPeerWorktrees, removePeerWorktree } from "./worktree.js";
23
+ /** Same default `wait` applies: match when a needle is given, else idle. */
24
+ function waitMode(args) {
25
+ const requested = typeof args.for === "string" ? args.for.trim() : "";
26
+ if (requested === "match" || requested === "either" || requested === "idle") {
27
+ return requested;
28
+ }
29
+ return typeof args.match === "string" && args.match ? "match" : "idle";
30
+ }
31
+ async function resolveTarget(client, args, state, clock, env, policy, op) {
32
+ const to = String(args.to ?? "").trim();
33
+ if (!to)
34
+ throw new ZellijError("missing_peer", "to required");
35
+ const { session } = await client.resolveSession(typeof args.session === "string" ? args.session : undefined);
36
+ // The plugin manifest carries no cwd/command/floating, so verbose
37
+ // responses have to go the polling route.
38
+ const bus = isVerbose(args)
39
+ ? null
40
+ : await busSnapshot(client, state, session, clock, env);
41
+ let panes = null;
42
+ if (bus) {
43
+ const busPanes = busToPanes(bus.snapshot);
44
+ try {
45
+ client.resolvePane(busPanes, to);
46
+ panes = busPanes;
47
+ }
48
+ catch {
49
+ // A command-shaped `to` only matches the polled list.
50
+ }
51
+ }
52
+ if (!panes)
53
+ panes = await client.listPanes(session);
54
+ const pane = client.resolvePane(panes, to);
55
+ if (policy && op)
56
+ assertPaneAllowed(policy, pane, op);
57
+ return { session, panes, pane };
58
+ }
59
+ /** Shared MCP/CLI dispatch for zswarm ops. */
60
+ export async function dispatchZswarm(args, client = createZellijClient(), deps = {}) {
61
+ const op = String(args.op ?? "");
62
+ const verbose = isVerbose(args);
63
+ const clock = {
64
+ now: deps.now ?? (() => Date.now()),
65
+ sleep: deps.sleep ??
66
+ ((ms) => new Promise((r) => setTimeout(r, ms))),
67
+ };
68
+ // Only the worktree ops need git, so the client is built on demand.
69
+ let gitClient = deps.git ?? null;
70
+ const git = () => (gitClient ??= createGitClient());
71
+ let stateStore = deps.state ?? null;
72
+ const state = () => (stateStore ??= createStateStore());
73
+ const policy = deps.policy ?? loadPolicy(deps.env);
74
+ try {
75
+ // Policy gates the op before anything touches the session.
76
+ assertOpAllowed(policy, op);
77
+ switch (op) {
78
+ case "sessions": {
79
+ const sessions = await client.listSessions();
80
+ return { ok: true, data: { sessions, zellij: client.zellijPath } };
81
+ }
82
+ case "list": {
83
+ const { session } = await client.resolveSession(typeof args.session === "string" ? args.session : undefined);
84
+ // The plugin's manifest carries no cwd or floating flag, so anything
85
+ // verbose has to go the polling route.
86
+ const bus = verbose
87
+ ? null
88
+ : await busSnapshot(client, state(), session, clock, deps.env);
89
+ const view = bus ? paneViewBus : verbose ? paneViewFull : paneViewSlim;
90
+ const panes = (bus ? busToPanes(bus.snapshot) : await client.listPanes(session))
91
+ .filter((p) => !p.isPlugin)
92
+ .map(view)
93
+ .sort((a, b) => a.id.localeCompare(b.id));
94
+ const data = {
95
+ session,
96
+ source: bus ? "plugin" : "zellij",
97
+ panes,
98
+ };
99
+ if (verbose && client.selfPaneId)
100
+ data.self = client.selfPaneId;
101
+ return { ok: true, data };
102
+ }
103
+ case "send": {
104
+ const body = String(args.body ?? args.text ?? "");
105
+ if (!body.trim())
106
+ throw new ZellijError("missing_body", "body required");
107
+ const { session, pane } = await resolveTarget(client, args, state(), clock, deps.env, policy, op);
108
+ assertWritable(client, pane, args, "send");
109
+ // A pane that dropped back to a shell will *run* the message. There is
110
+ // no reliable way to tell an agent from a prompt, so the caller names
111
+ // something the screen must show first.
112
+ const expect = typeof args.expect === "string" ? args.expect.trim() : "";
113
+ if (expect) {
114
+ const screen = await client.dumpPane({ session, paneId: pane.id });
115
+ assertPaneExpects(screen.text, expect, pane.id);
116
+ }
117
+ const result = await deliverTo(client, state(), args, {
118
+ session,
119
+ pane,
120
+ body,
121
+ op: "send",
122
+ at: clock.now(),
123
+ clock,
124
+ });
125
+ if (!result.ok && result.error) {
126
+ throw new ZellijError(result.error.code, result.error.message);
127
+ }
128
+ const data = {
129
+ delivery: result.delivery,
130
+ session,
131
+ to: result.to,
132
+ from: (typeof args.from === "string" && args.from.trim()) || "swarm",
133
+ submitted: result.submitted,
134
+ };
135
+ if (verbose)
136
+ data.pane = paneViewFull(pane);
137
+ return { ok: true, data };
138
+ }
139
+ case "broadcast":
140
+ return await broadcast(client, state(), args, clock, policy);
141
+ case "keys":
142
+ case "interrupt": {
143
+ const { session, pane } = await resolveTarget(client, args, state(), clock, deps.env, policy, op);
144
+ assertWritable(client, pane, args, op);
145
+ const chars = typeof args.chars === "string" ? args.chars : "";
146
+ if (op === "keys" && chars) {
147
+ await client.writeChars({ session, paneId: pane.id, chars });
148
+ if (isTrue(args.enter)) {
149
+ await client.sendKeys({ session, paneId: pane.id, keys: ["Enter"] });
150
+ }
151
+ state().appendLog({
152
+ at: clock.now(),
153
+ op,
154
+ session,
155
+ to: pane.id,
156
+ bytes: chars.length,
157
+ ok: true,
158
+ detail: "write-chars",
159
+ });
160
+ return {
161
+ ok: true,
162
+ data: {
163
+ session,
164
+ to: pane.id,
165
+ delivery: "zellij_write_chars",
166
+ chars: chars.length,
167
+ enter: isTrue(args.enter),
168
+ },
169
+ };
170
+ }
171
+ const keys = op === "interrupt"
172
+ ? normalizeKeys(args.keys ?? (isTrue(args.hard) ? "Ctrl c" : "Esc"))
173
+ : normalizeKeys(args.keys);
174
+ const sent = await client.sendKeys({ session, paneId: pane.id, keys });
175
+ if (op === "keys" && isTrue(args.enter)) {
176
+ await client.sendKeys({ session, paneId: pane.id, keys: ["Enter"] });
177
+ }
178
+ state().appendLog({
179
+ at: clock.now(),
180
+ op,
181
+ session,
182
+ to: sent.paneId,
183
+ ok: true,
184
+ detail: sent.keys.join(" "),
185
+ });
186
+ return {
187
+ ok: true,
188
+ data: {
189
+ session,
190
+ to: sent.paneId,
191
+ delivery: "zellij_send_keys",
192
+ keys: sent.keys,
193
+ },
194
+ };
195
+ }
196
+ case "dump": {
197
+ const { session, pane } = await resolveTarget(client, args, state(), clock, deps.env);
198
+ const dumped = await client.dumpPane({
199
+ session,
200
+ paneId: pane.id,
201
+ full: isTrue(args.full),
202
+ });
203
+ const max = dumpMaxChars(args);
204
+ const keep = isTrue(args.head) ? "head" : "tail";
205
+ const clipped = truncateDumpText(dumped.text, max, keep);
206
+ return {
207
+ ok: true,
208
+ data: {
209
+ session: dumped.session,
210
+ to: dumped.paneId,
211
+ text: clipped.text,
212
+ truncated: clipped.truncated,
213
+ chars: clipped.chars,
214
+ max,
215
+ },
216
+ };
217
+ }
218
+ case "tail": {
219
+ const target = await resolveTarget(client, args, state(), clock, deps.env);
220
+ return await tailPane(client, state(), args, target);
221
+ }
222
+ case "wait": {
223
+ const target = await resolveTarget(client, args, state(), clock, deps.env);
224
+ // The plugin holds one pipe for the whole wait and polls far tighter
225
+ // than spawning a process allows. It declines a regex needle — it has
226
+ // no engine — and then this is the loop that runs.
227
+ const viaBus = isTrue(args.regex)
228
+ ? undefined
229
+ : () => busWait(client, state(), target.session, {
230
+ pane: target.pane.id,
231
+ for: waitMode(args),
232
+ match: typeof args.match === "string" ? args.match : null,
233
+ ignoreCase: isTrue(args.ignoreCase),
234
+ idleMs: Number(args.idleMs ?? 2000),
235
+ timeoutMs: Number(args.timeoutMs ?? 60_000),
236
+ }, deps.env);
237
+ return await waitForPane(client, target, args, clock, viaBus);
238
+ }
239
+ case "status": {
240
+ const { session } = await client.resolveSession(typeof args.session === "string" ? args.session : undefined);
241
+ // Verbose reports cwd and command, which the plugin manifest lacks.
242
+ const bus = verbose
243
+ ? null
244
+ : await busSnapshot(client, state(), session, clock, deps.env);
245
+ let supplied = bus
246
+ ? {
247
+ session,
248
+ panes: busToPanes(bus.snapshot),
249
+ source: "plugin",
250
+ // Sampling is 2N dumps; the plugin reads them all in one pipe.
251
+ readScreens: (paneIds) => busScreens(client, state(), session, paneIds, clock, deps.env),
252
+ readChanged: async (paneIds) => {
253
+ const reply = await busChanged(client, state(), session, paneIds, deps.env);
254
+ if (!reply)
255
+ return null;
256
+ return new Map(reply.panes.map((p) => [
257
+ p.id,
258
+ { changed: p.changed, first: p.first, screen: p.screen },
259
+ ]));
260
+ },
261
+ }
262
+ : null;
263
+ const only = typeof args.to === "string" ? args.to.trim() : "";
264
+ if (supplied && only) {
265
+ try {
266
+ client.resolvePane(supplied.panes, only);
267
+ }
268
+ catch {
269
+ // The manifest has no commands, so a command-shaped `to` only
270
+ // resolves against the polled list.
271
+ supplied = null;
272
+ }
273
+ }
274
+ return await peerStatus(client, args, clock, supplied);
275
+ }
276
+ case "bus":
277
+ return await busOp(client, state(), args, clock, deps.env, policy);
278
+ case "signal":
279
+ return postSignal(state(), args, clock);
280
+ case "signals":
281
+ return listSignals(state());
282
+ case "await":
283
+ return await awaitSignal(state(), args, clock);
284
+ case "log":
285
+ return readDeliveryLog(state(), args);
286
+ case "rename":
287
+ return await renameTarget(client, args);
288
+ case "focus":
289
+ return await focusTarget(client, args);
290
+ case "tabs":
291
+ return await listTabsOp(client, args);
292
+ case "layout":
293
+ return await dumpLayoutOp(client, args);
294
+ case "stack":
295
+ return await stackTargets(client, args);
296
+ case "diff":
297
+ return await peerDiff(git(), args);
298
+ case "checkpoint":
299
+ return await peerCheckpoint(git(), args, clock);
300
+ case "spawn":
301
+ return await spawnPane(client, args, deps.git);
302
+ case "worktrees":
303
+ return await listPeerWorktrees(git(), client, args);
304
+ case "unworktree":
305
+ return await removePeerWorktree(git(), client, args);
306
+ case "close": {
307
+ const { session, pane } = await resolveTarget(client, args, state(), clock, deps.env, policy, op);
308
+ // Not assertWritable: closing an exited pane is the point of close.
309
+ assertNotPlugin(pane, "close");
310
+ assertNotSelf(client, pane, args, "close");
311
+ const closed = await client.closePane({ session, paneId: pane.id });
312
+ state().appendLog({
313
+ at: clock.now(),
314
+ op: "close",
315
+ session,
316
+ to: closed.paneId,
317
+ ok: true,
318
+ });
319
+ return {
320
+ ok: true,
321
+ data: { session: closed.session, closed: closed.paneId },
322
+ };
323
+ }
324
+ default:
325
+ throw new ZellijError("usage", `zswarm requires op=${OP_NAMES.join("|")}`);
326
+ }
327
+ }
328
+ catch (err) {
329
+ return fail(err);
330
+ }
331
+ }
@@ -0,0 +1,15 @@
1
+ import type { ZellijClient } from "../zellij/client.js";
2
+ import type { ZellijPane } from "../zellij/panes.js";
3
+ export declare function assertNotPlugin(pane: ZellijPane, action: string): void;
4
+ export declare function assertNotSelf(client: ZellijClient, pane: ZellijPane, args: Record<string, unknown>, action: string): void;
5
+ /**
6
+ * Refuse writes that would loop back into the caller's own pane, land in a
7
+ * dead pane, or hit a plugin pane. `allowSelf` / `force` opt out.
8
+ */
9
+ export declare function assertWritable(client: ZellijClient, pane: ZellijPane, args: Record<string, unknown>, action: string): void;
10
+ /**
11
+ * Opt-in check that the pane still looks like the intended agent, not a
12
+ * shell that would execute the body as a command. Substring only — a regex
13
+ * in a safety check is a foot-gun.
14
+ */
15
+ export declare function assertPaneExpects(screen: string, expect: string, paneId: string): void;
@@ -0,0 +1,36 @@
1
+ import { ZellijError } from "../errors.js";
2
+ import { isTrue } from "./util.js";
3
+ export function assertNotPlugin(pane, action) {
4
+ if (pane.isPlugin) {
5
+ throw new ZellijError("pane_is_plugin", `${pane.id} is a plugin pane; ${action} targets terminal panes`);
6
+ }
7
+ }
8
+ export function assertNotSelf(client, pane, args, action) {
9
+ const self = client.selfPaneId;
10
+ if (self && pane.id === self && !isTrue(args.allowSelf)) {
11
+ throw new ZellijError("self_target", `refusing to ${action} into zswarm's own pane (${self}); pass allowSelf=true to override`);
12
+ }
13
+ }
14
+ /**
15
+ * Refuse writes that would loop back into the caller's own pane, land in a
16
+ * dead pane, or hit a plugin pane. `allowSelf` / `force` opt out.
17
+ */
18
+ export function assertWritable(client, pane, args, action) {
19
+ assertNotPlugin(pane, action);
20
+ assertNotSelf(client, pane, args, action);
21
+ if (pane.exited && !isTrue(args.force)) {
22
+ throw new ZellijError("pane_exited", `${pane.id} has exited; pass force=true to write to it anyway`);
23
+ }
24
+ }
25
+ /**
26
+ * Opt-in check that the pane still looks like the intended agent, not a
27
+ * shell that would execute the body as a command. Substring only — a regex
28
+ * in a safety check is a foot-gun.
29
+ */
30
+ export function assertPaneExpects(screen, expect, paneId) {
31
+ if (!expect)
32
+ return;
33
+ if (screen.toLowerCase().includes(expect.toLowerCase()))
34
+ return;
35
+ throw new ZellijError("expect_missing", `${paneId} screen does not contain ${JSON.stringify(expect)}`);
36
+ }
@@ -0,0 +1,4 @@
1
+ import type { StateStore } from "../state.js";
2
+ import type { OpsResult } from "./types.js";
3
+ /** Read back the delivery log: what zswarm sent where, and whether it landed. */
4
+ export declare function readDeliveryLog(state: StateStore, args: Record<string, unknown>): OpsResult;
@@ -0,0 +1,26 @@
1
+ import { isTrue, numberArg, optionalString } from "./util.js";
2
+ /** Read back the delivery log: what zswarm sent where, and whether it landed. */
3
+ export function readDeliveryLog(state, args) {
4
+ const limit = numberArg(args, "limit", 20, { min: 1, max: 1_000 });
5
+ const to = optionalString(args.to);
6
+ const since = optionalString(args.since);
7
+ const sinceAt = since ? Number(since) : null;
8
+ const failures = isTrue(args.failed);
9
+ let entries = state.readLog();
10
+ if (to)
11
+ entries = entries.filter((e) => e.to === to);
12
+ if (sinceAt !== null && Number.isFinite(sinceAt)) {
13
+ entries = entries.filter((e) => e.at >= sinceAt);
14
+ }
15
+ if (failures)
16
+ entries = entries.filter((e) => !e.ok);
17
+ return {
18
+ ok: true,
19
+ data: {
20
+ dir: state.dir,
21
+ logging: state.logging,
22
+ total: entries.length,
23
+ entries: entries.slice(-limit),
24
+ },
25
+ };
26
+ }
@@ -0,0 +1,13 @@
1
+ import type { ZellijClient } from "../zellij/client.js";
2
+ import type { OpsResult } from "./types.js";
3
+ /**
4
+ * Give a pane (or its tab) a stable name, so peers can be addressed as
5
+ * `reviewer` instead of whatever id Zellij handed out.
6
+ */
7
+ export declare function renameTarget(client: ZellijClient, args: Record<string, unknown>): Promise<OpsResult>;
8
+ export declare function focusTarget(client: ZellijClient, args: Record<string, unknown>): Promise<OpsResult>;
9
+ export declare function listTabsOp(client: ZellijClient, args: Record<string, unknown>): Promise<OpsResult>;
10
+ /** The session's current layout as KDL — a spawnable description of the crew. */
11
+ export declare function dumpLayoutOp(client: ZellijClient, args: Record<string, unknown>): Promise<OpsResult>;
12
+ /** Collapse several peers into one stack so a big crew stays readable. */
13
+ export declare function stackTargets(client: ZellijClient, args: Record<string, unknown>): Promise<OpsResult>;