@zswarm/core 0.1.3 → 0.1.5
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/exec.d.ts +2 -0
- package/dist/exec.js +18 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/ops/broadcast.d.ts +1 -1
- package/dist/ops/broadcast.js +9 -5
- package/dist/ops/bus.d.ts +10 -7
- package/dist/ops/bus.js +119 -86
- package/dist/ops/delivery.d.ts +31 -1
- package/dist/ops/delivery.js +53 -2
- package/dist/ops/dispatch.js +40 -18
- package/dist/ops/panes.d.ts +4 -3
- package/dist/ops/panes.js +14 -4
- package/dist/ops/serve.d.ts +10 -1
- package/dist/ops/serve.js +99 -26
- package/dist/ops/types.d.ts +2 -0
- package/dist/ops/util.d.ts +1 -0
- package/dist/ops/util.js +5 -0
- package/dist/ops/wait.d.ts +8 -1
- package/dist/ops/wait.js +7 -3
- package/dist/schema.js +6 -3
- package/dist/state.d.ts +7 -6
- package/dist/state.js +160 -23
- package/dist/zellij/args.d.ts +2 -0
- package/dist/zellij/args.js +1 -0
- package/dist/zellij/binary.d.ts +3 -1
- package/dist/zellij/binary.js +34 -2
- package/dist/zellij/bus.d.ts +9 -0
- package/dist/zellij/bus.js +20 -0
- package/dist/zellij/client.d.ts +1 -0
- package/dist/zellij/client.js +17 -3
- package/dist/zellij/session.d.ts +2 -0
- package/dist/zellij/session.js +4 -0
- package/package.json +2 -2
package/dist/exec.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type ExecOptions = {
|
|
|
16
16
|
* timeout.
|
|
17
17
|
*/
|
|
18
18
|
until?: (stdout: string) => boolean;
|
|
19
|
+
/** Kill the child when aborted (MCP cancellation). */
|
|
20
|
+
signal?: AbortSignal;
|
|
19
21
|
};
|
|
20
22
|
export type ExecFn = (args: string[], options: ExecOptions) => Promise<ExecResult>;
|
|
21
23
|
export declare const NOT_FOUND_EXIT = 127;
|
package/dist/exec.js
CHANGED
|
@@ -100,14 +100,25 @@ function runUntil(binPath, args, env, options) {
|
|
|
100
100
|
return;
|
|
101
101
|
settled = true;
|
|
102
102
|
clearTimeout(timer);
|
|
103
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
103
104
|
child.kill();
|
|
104
105
|
resolve(result);
|
|
105
106
|
};
|
|
107
|
+
const onAbort = () => finish({
|
|
108
|
+
code: -1,
|
|
109
|
+
stdout,
|
|
110
|
+
stderr: `${binPath} cancelled`,
|
|
111
|
+
});
|
|
106
112
|
const timer = setTimeout(() => finish({
|
|
107
113
|
code: -1,
|
|
108
114
|
stdout,
|
|
109
115
|
stderr: `${binPath} timed out after ${options.timeoutMs}ms`,
|
|
110
116
|
}), options.timeoutMs);
|
|
117
|
+
if (options.signal?.aborted) {
|
|
118
|
+
onAbort();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
111
122
|
child.on("error", (err) => {
|
|
112
123
|
const missing = err.code === "ENOENT" || err.code === "ENOTDIR";
|
|
113
124
|
finish({
|
|
@@ -138,6 +149,7 @@ function runToExit(binPath, args, env, options) {
|
|
|
138
149
|
cwd: options.cwd,
|
|
139
150
|
maxBuffer: 8 * 1024 * 1024,
|
|
140
151
|
windowsHide: true,
|
|
152
|
+
signal: options.signal,
|
|
141
153
|
env: options.env ? { ...env, ...options.env } : env,
|
|
142
154
|
}, (error, stdout, stderr) => {
|
|
143
155
|
const failure = error;
|
|
@@ -151,11 +163,15 @@ function runToExit(binPath, args, env, options) {
|
|
|
151
163
|
return;
|
|
152
164
|
}
|
|
153
165
|
if (failure &&
|
|
154
|
-
(failure.killed === true ||
|
|
166
|
+
(failure.killed === true ||
|
|
167
|
+
typeof failure.signal === "string" ||
|
|
168
|
+
failure.name === "AbortError")) {
|
|
155
169
|
resolve({
|
|
156
170
|
code: -1,
|
|
157
171
|
stdout: String(stdout ?? ""),
|
|
158
|
-
stderr:
|
|
172
|
+
stderr: failure.name === "AbortError"
|
|
173
|
+
? `${binPath} cancelled`
|
|
174
|
+
: `${binPath} timed out after ${options.timeoutMs}ms`,
|
|
159
175
|
});
|
|
160
176
|
return;
|
|
161
177
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,9 +4,9 @@ export { createGitClient, defaultWorktreeRoot, normalizeRepoPath, parseWorktreeL
|
|
|
4
4
|
export { expandHomePath, resolveZellijBinary, sanitizeZellijEnv, type ZellijExecFn, type ZellijExecResult, } from "./zellij/binary.js";
|
|
5
5
|
export { normalizePaneId, parsePaneList, resolvePane, type ZellijPane, } from "./zellij/panes.js";
|
|
6
6
|
export { parseSessionList, resolveSelfPaneId, type ZellijSessionResolve, } from "./zellij/session.js";
|
|
7
|
-
export { buildClosePaneArgs, buildDumpArgs, buildLaunchPluginArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildPipeArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, scrollbackPayload, type LaunchPluginInput, type NewPaneInput, type NewTabInput, type PaneDirection, type PipeInput, type ScrollbackRequest, } from "./zellij/args.js";
|
|
8
|
-
export { busPluginUrl, busToPanes, nextConfigKey, parseBusReply, parseScrollbackReply, resolveBusPlugin, scrollbackToScreen, BUS_PIPE_NAME, DEFAULT_BUS_KEY, DEFAULT_BUS_TIMEOUT_MS, type BusMarker, type BusPane, type BusScrollback, type BusScrollbackPane, type BusSnapshot, } from "./zellij/bus.js";
|
|
9
|
-
export { busSnapshot, planBus, resetBusCache, type BusPlan, } from "./ops/bus.js";
|
|
7
|
+
export { buildClosePaneArgs, buildDumpArgs, buildLaunchPluginArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildPipeArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, scrollbackPayload, waitPayload, type LaunchPluginInput, type NewPaneInput, type NewTabInput, type PaneDirection, type PipeInput, type ScrollbackRequest, type WaitRequest, } from "./zellij/args.js";
|
|
8
|
+
export { busPluginUrl, busToPanes, isBusPluginPane, nextConfigKey, parseBusReply, parseScrollbackReply, resolveBusPlugin, scrollbackToScreen, BUS_PIPE_NAME, DEFAULT_BUS_KEY, DEFAULT_BUS_TIMEOUT_MS, type BusMarker, type BusPane, type BusScrollback, type BusScrollbackPane, type BusSnapshot, } from "./zellij/bus.js";
|
|
9
|
+
export { busSnapshot, closeBusPluginPanes, planBus, resetBusCache, type BusPlan, } from "./ops/bus.js";
|
|
10
10
|
export { createZellijClient, type ZellijClient, type ZellijClientOptions, } from "./zellij/client.js";
|
|
11
11
|
export { createStateStore, defaultStateDir, type LogEntry, type SignalChannel, type StateStore, type StateStoreOptions, } from "./state.js";
|
|
12
12
|
export { dispatchZswarm } from "./ops/dispatch.js";
|
|
@@ -14,7 +14,7 @@ export { assertOpAllowed, assertPaneAllowed, isWriteOp, loadPolicy, type Policy,
|
|
|
14
14
|
export { buildSshRemoteCommand, createSshExec, quoteRemoteArg, shellQuote, type SshTarget, } from "./exec.js";
|
|
15
15
|
export { resolveSshTarget } from "./zellij/binary.js";
|
|
16
16
|
export { applyIpcTmpEnv, cmdQuote, concreteTmp, encodePowerShellCommand, inferRemoteShell, parseZellijServerPaths, pickIpcDirs, pickIpcTmp, socketDirFromServerPath, tmpFromServerPath, unixDiscoverRemote, windowsDiscoverRemote, windowsInteractiveRemote, wrapWithTmpEnv, } from "./zellij/ipc.js";
|
|
17
|
-
export { callServe, DEFAULT_SERVE_LISTEN, installServeLogon, isLoopbackHost, parseListenAddress, SERVE_CALL_TIMEOUT_CAP_MS, SERVE_MAX_REQUEST_BYTES, SERVE_TASK_NAME, serveCallTimeout, serveChildEnv, serveLogonCommand, startServe, uninstallServeLogon, } from "./ops/serve.js";
|
|
17
|
+
export { callServe, DEFAULT_SERVE_LISTEN, installServeLogon, isLoopbackHost, parseListenAddress, redactServeSecret, SERVE_CALL_TIMEOUT_CAP_MS, SERVE_MAX_REQUEST_BYTES, SERVE_TASK_NAME, serveCallTimeout, serveChildEnv, serveLogonCommand, startServe, uninstallServeLogon, } from "./ops/serve.js";
|
|
18
18
|
export { parseTabList, resolveTab, type ZellijTab } from "./zellij/tabs.js";
|
|
19
19
|
export { selectTargets } from "./ops/broadcast.js";
|
|
20
20
|
export { diffScreens, cursorKey } from "./ops/tail.js";
|
package/dist/index.js
CHANGED
|
@@ -4,9 +4,9 @@ export { createGitClient, defaultWorktreeRoot, normalizeRepoPath, parseWorktreeL
|
|
|
4
4
|
export { expandHomePath, resolveZellijBinary, sanitizeZellijEnv, } from "./zellij/binary.js";
|
|
5
5
|
export { normalizePaneId, parsePaneList, resolvePane, } from "./zellij/panes.js";
|
|
6
6
|
export { parseSessionList, resolveSelfPaneId, } from "./zellij/session.js";
|
|
7
|
-
export { buildClosePaneArgs, buildDumpArgs, buildLaunchPluginArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildPipeArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, scrollbackPayload, } from "./zellij/args.js";
|
|
8
|
-
export { busPluginUrl, busToPanes, nextConfigKey, parseBusReply, parseScrollbackReply, resolveBusPlugin, scrollbackToScreen, BUS_PIPE_NAME, DEFAULT_BUS_KEY, DEFAULT_BUS_TIMEOUT_MS, } from "./zellij/bus.js";
|
|
9
|
-
export { busSnapshot, planBus, resetBusCache, } from "./ops/bus.js";
|
|
7
|
+
export { buildClosePaneArgs, buildDumpArgs, buildLaunchPluginArgs, buildListPanesArgs, buildNewPaneArgs, buildNewTabArgs, buildPasteArgs, buildPipeArgs, buildSendEnterArgs, buildSendKeysArgs, buildWriteCharsArgs, scrollbackPayload, waitPayload, } from "./zellij/args.js";
|
|
8
|
+
export { busPluginUrl, busToPanes, isBusPluginPane, nextConfigKey, parseBusReply, parseScrollbackReply, resolveBusPlugin, scrollbackToScreen, BUS_PIPE_NAME, DEFAULT_BUS_KEY, DEFAULT_BUS_TIMEOUT_MS, } from "./zellij/bus.js";
|
|
9
|
+
export { busSnapshot, closeBusPluginPanes, planBus, resetBusCache, } from "./ops/bus.js";
|
|
10
10
|
export { createZellijClient, } from "./zellij/client.js";
|
|
11
11
|
export { createStateStore, defaultStateDir, } from "./state.js";
|
|
12
12
|
export { dispatchZswarm } from "./ops/dispatch.js";
|
|
@@ -14,7 +14,7 @@ export { assertOpAllowed, assertPaneAllowed, isWriteOp, loadPolicy, } from "./po
|
|
|
14
14
|
export { buildSshRemoteCommand, createSshExec, quoteRemoteArg, shellQuote, } from "./exec.js";
|
|
15
15
|
export { resolveSshTarget } from "./zellij/binary.js";
|
|
16
16
|
export { applyIpcTmpEnv, cmdQuote, concreteTmp, encodePowerShellCommand, inferRemoteShell, parseZellijServerPaths, pickIpcDirs, pickIpcTmp, socketDirFromServerPath, tmpFromServerPath, unixDiscoverRemote, windowsDiscoverRemote, windowsInteractiveRemote, wrapWithTmpEnv, } from "./zellij/ipc.js";
|
|
17
|
-
export { callServe, DEFAULT_SERVE_LISTEN, installServeLogon, isLoopbackHost, parseListenAddress, SERVE_CALL_TIMEOUT_CAP_MS, SERVE_MAX_REQUEST_BYTES, SERVE_TASK_NAME, serveCallTimeout, serveChildEnv, serveLogonCommand, startServe, uninstallServeLogon, } from "./ops/serve.js";
|
|
17
|
+
export { callServe, DEFAULT_SERVE_LISTEN, installServeLogon, isLoopbackHost, parseListenAddress, redactServeSecret, SERVE_CALL_TIMEOUT_CAP_MS, SERVE_MAX_REQUEST_BYTES, SERVE_TASK_NAME, serveCallTimeout, serveChildEnv, serveLogonCommand, startServe, uninstallServeLogon, } from "./ops/serve.js";
|
|
18
18
|
export { parseTabList, resolveTab } from "./zellij/tabs.js";
|
|
19
19
|
export { selectTargets } from "./ops/broadcast.js";
|
|
20
20
|
export { diffScreens, cursorKey } from "./ops/tail.js";
|
package/dist/ops/broadcast.d.ts
CHANGED
|
@@ -16,4 +16,4 @@ export declare function selectTargets(client: ZellijClient, panes: ZellijPane[],
|
|
|
16
16
|
skipped: Skipped[];
|
|
17
17
|
};
|
|
18
18
|
/** Send one body to many panes, reporting per-pane outcomes. */
|
|
19
|
-
export declare function broadcast(client: ZellijClient, state: StateStore, args: Record<string, unknown>, clock: Clock, policy?: Policy): Promise<OpsResult>;
|
|
19
|
+
export declare function broadcast(client: ZellijClient, state: StateStore, args: Record<string, unknown>, clock: Clock, policy?: Policy, env?: NodeJS.ProcessEnv): Promise<OpsResult>;
|
package/dist/ops/broadcast.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ZellijError } from "../errors.js";
|
|
2
2
|
import { assertPaneAllowed } from "../policy.js";
|
|
3
|
-
import { deliverTo } from "./delivery.js";
|
|
3
|
+
import { deliverTo, selfPaneTitle, withSenderLabel } from "./delivery.js";
|
|
4
4
|
import { isTrue, optionalString } from "./util.js";
|
|
5
5
|
/**
|
|
6
6
|
* Pick the panes a broadcast should reach: an explicit comma list, a tab, or
|
|
@@ -65,13 +65,17 @@ export function selectTargets(client, panes, args) {
|
|
|
65
65
|
return { targets, skipped };
|
|
66
66
|
}
|
|
67
67
|
/** Send one body to many panes, reporting per-pane outcomes. */
|
|
68
|
-
export async function broadcast(client, state, args, clock, policy) {
|
|
68
|
+
export async function broadcast(client, state, args, clock, policy, env = process.env) {
|
|
69
69
|
const body = String(args.body ?? args.text ?? "");
|
|
70
70
|
if (!body.trim())
|
|
71
71
|
throw new ZellijError("missing_body", "body required");
|
|
72
72
|
const { session } = await client.resolveSession(typeof args.session === "string" ? args.session : undefined);
|
|
73
73
|
const panes = await client.listPanes(session);
|
|
74
|
-
const
|
|
74
|
+
const labeled = withSenderLabel(args, {
|
|
75
|
+
env,
|
|
76
|
+
selfTitle: selfPaneTitle(client, panes),
|
|
77
|
+
});
|
|
78
|
+
const { targets: selected, skipped } = selectTargets(client, panes, labeled);
|
|
75
79
|
// Policy narrows the selection rather than failing the whole broadcast.
|
|
76
80
|
const targets = selected.filter((pane) => {
|
|
77
81
|
if (!policy)
|
|
@@ -90,7 +94,7 @@ export async function broadcast(client, state, args, clock, policy) {
|
|
|
90
94
|
}
|
|
91
95
|
const results = [];
|
|
92
96
|
for (const pane of targets) {
|
|
93
|
-
results.push(await deliverTo(client, state,
|
|
97
|
+
results.push(await deliverTo(client, state, labeled, {
|
|
94
98
|
session,
|
|
95
99
|
pane,
|
|
96
100
|
body,
|
|
@@ -104,7 +108,7 @@ export async function broadcast(client, state, args, clock, policy) {
|
|
|
104
108
|
ok: true,
|
|
105
109
|
data: {
|
|
106
110
|
session,
|
|
107
|
-
from:
|
|
111
|
+
from: labeled.from,
|
|
108
112
|
delivered: delivered.map((r) => r.to),
|
|
109
113
|
failed: results.filter((r) => !r.ok),
|
|
110
114
|
skipped,
|
package/dist/ops/bus.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Policy } from "../policy.js";
|
|
2
2
|
import type { StateStore } from "../state.js";
|
|
3
3
|
import { type BusChanged, type BusSnapshot, type BusWait } from "../zellij/bus.js";
|
|
4
4
|
import type { WaitRequest } from "../zellij/args.js";
|
|
@@ -15,15 +15,16 @@ export type BusPlan = {
|
|
|
15
15
|
};
|
|
16
16
|
/** Tests share a module instance; this drops what a previous case cached. */
|
|
17
17
|
export declare function resetBusCache(): void;
|
|
18
|
-
export declare function planBus(client: ZellijClient, state: StateStore, env?: NodeJS.ProcessEnv): BusPlan;
|
|
18
|
+
export declare function planBus(client: ZellijClient, state: StateStore, env?: NodeJS.ProcessEnv, session?: string): BusPlan;
|
|
19
19
|
/**
|
|
20
20
|
* One pipe, one answer — or null, and the caller polls instead.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* A freshly launched instance replies before Zellij has pushed it a manifest,
|
|
23
|
+
* so a not-ready answer is retried once rather than reported as an empty
|
|
24
|
+
* session. If it is still cold after that, one same-title rename nudges Zellij
|
|
25
|
+
* to push. A silent pipe used to rotate `instance=` keys, and `zellij pipe
|
|
26
|
+
* --plugin` loads a new copy for each key — that is how a dead bus became
|
|
27
|
+
* dozens of WASM panes. One remembered key, then polling.
|
|
27
28
|
*/
|
|
28
29
|
export declare function busSnapshot(client: ZellijClient, state: StateStore, session: string, clock: Clock, env?: NodeJS.ProcessEnv, payload?: string): Promise<{
|
|
29
30
|
snapshot: BusSnapshot;
|
|
@@ -56,5 +57,7 @@ export declare function busWait(client: ZellijClient, state: StateStore, session
|
|
|
56
57
|
* relative to the previous call rather than a fixed window.
|
|
57
58
|
*/
|
|
58
59
|
export declare function busChanged(client: ZellijClient, state: StateStore, session: string, paneIds: string[], env?: NodeJS.ProcessEnv): Promise<BusChanged | null>;
|
|
60
|
+
/** Close every zswarm-bus plugin pane in this session. Best-effort per pane. */
|
|
61
|
+
export declare function closeBusPluginPanes(client: ZellijClient, session: string, pluginPath: string | null): Promise<string[]>;
|
|
59
62
|
/** Report on the bus, install it, or forget it. */
|
|
60
63
|
export declare function busOp(client: ZellijClient, state: StateStore, args: Record<string, unknown>, clock: Clock, env?: NodeJS.ProcessEnv, policy?: Policy): Promise<OpsResult>;
|
package/dist/ops/bus.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { ZellijError } from "../errors.js";
|
|
3
|
-
import {
|
|
3
|
+
import { loadPolicy } from "../policy.js";
|
|
4
|
+
import { DEFAULT_BUS_KEY, busPluginUrl, isBusPluginPane, parseBusReply, parseChangedReply, parseScrollbackReply, parseWaitReply, resolveBusPlugin, scrollbackToScreen, } from "../zellij/bus.js";
|
|
4
5
|
import { isTrue } from "./util.js";
|
|
5
6
|
/**
|
|
6
7
|
* Whether to ask the event-bus plugin, and what to do when it does not answer.
|
|
@@ -9,10 +10,14 @@ import { isTrue } from "./util.js";
|
|
|
9
10
|
* prompt, because a pipe to a plugin that was never approved costs a process
|
|
10
11
|
* and a timeout on every call and then falls back anyway.
|
|
11
12
|
*/
|
|
12
|
-
/** Config keys to try before giving up; each one is a fresh pipe destination. */
|
|
13
|
-
const MAX_KEY_ATTEMPTS = 3;
|
|
14
13
|
/** A just-launched instance answers before Zellij has pushed it anything. */
|
|
15
14
|
const COLD_RETRY_MS = 200;
|
|
15
|
+
/**
|
|
16
|
+
* A marker written this recently with no live pane is an in-flight install,
|
|
17
|
+
* not a closed pane. Relaunching it is how concurrent agents used to stack
|
|
18
|
+
* copies.
|
|
19
|
+
*/
|
|
20
|
+
const INSTALL_IN_FLIGHT_MS = 10_000;
|
|
16
21
|
/** Set once a pipe has failed, so one dead plugin costs one timeout, not many. */
|
|
17
22
|
let processDisabled = null;
|
|
18
23
|
/** The key that actually answered, per session, for the life of this process. */
|
|
@@ -30,8 +35,8 @@ function negative(raw) {
|
|
|
30
35
|
const v = (raw ?? "").trim().toLowerCase();
|
|
31
36
|
return v === "0" || v === "false" || v === "no" || v === "off";
|
|
32
37
|
}
|
|
33
|
-
export function planBus(client, state, env = process.env) {
|
|
34
|
-
const marker = state.readBus();
|
|
38
|
+
export function planBus(client, state, env = process.env, session = "") {
|
|
39
|
+
const marker = session ? state.readBus(session) : null;
|
|
35
40
|
// A marker can outlive the wasm it names — an upgrade renames the artifact,
|
|
36
41
|
// so a remembered path that no longer exists must not shadow the current one.
|
|
37
42
|
const remembered = marker && existsSync(marker.plugin) ? marker : null;
|
|
@@ -75,7 +80,9 @@ async function askOnce(client, session, url, configKey, payload) {
|
|
|
75
80
|
* blind (ready:false, paneUpdates:0) until something moves. Renaming a pane
|
|
76
81
|
* to the title it already has is an invisible change that forces the push.
|
|
77
82
|
*/
|
|
78
|
-
async function nudgeManifest(client, session) {
|
|
83
|
+
async function nudgeManifest(client, session, env) {
|
|
84
|
+
if (loadPolicy(env).readOnly)
|
|
85
|
+
return;
|
|
79
86
|
const panes = await client.listPanes(session);
|
|
80
87
|
const pane = panes.find((p) => !p.isPlugin && !p.exited && p.title.trim());
|
|
81
88
|
if (!pane)
|
|
@@ -85,52 +92,40 @@ async function nudgeManifest(client, session) {
|
|
|
85
92
|
/**
|
|
86
93
|
* One pipe, one answer — or null, and the caller polls instead.
|
|
87
94
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
95
|
+
* A freshly launched instance replies before Zellij has pushed it a manifest,
|
|
96
|
+
* so a not-ready answer is retried once rather than reported as an empty
|
|
97
|
+
* session. If it is still cold after that, one same-title rename nudges Zellij
|
|
98
|
+
* to push. A silent pipe used to rotate `instance=` keys, and `zellij pipe
|
|
99
|
+
* --plugin` loads a new copy for each key — that is how a dead bus became
|
|
100
|
+
* dozens of WASM panes. One remembered key, then polling.
|
|
93
101
|
*/
|
|
94
102
|
export async function busSnapshot(client, state, session, clock, env = process.env, payload = "status") {
|
|
95
|
-
const plan = planBus(client, state, env);
|
|
103
|
+
const plan = planBus(client, state, env, session);
|
|
96
104
|
const pluginPath = plan.plugin;
|
|
97
105
|
if (!plan.enabled || !plan.url || !pluginPath)
|
|
98
106
|
return null;
|
|
99
|
-
|
|
100
|
-
let
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
await
|
|
107
|
+
const configKey = answeredWith.get(session) ?? plan.configKey;
|
|
108
|
+
let snapshot = await askOnce(client, session, plan.url, configKey, payload);
|
|
109
|
+
if (snapshot && !snapshot.ready) {
|
|
110
|
+
await clock.sleep(COLD_RETRY_MS);
|
|
111
|
+
snapshot =
|
|
112
|
+
(await askOnce(client, session, plan.url, configKey, payload)) ??
|
|
113
|
+
snapshot;
|
|
114
|
+
}
|
|
115
|
+
if (snapshot && !snapshot.ready) {
|
|
116
|
+
try {
|
|
117
|
+
await nudgeManifest(client, session, env);
|
|
105
118
|
snapshot =
|
|
106
119
|
(await askOnce(client, session, plan.url, configKey, payload)) ??
|
|
107
120
|
snapshot;
|
|
108
121
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
await nudgeManifest(client, session);
|
|
113
|
-
snapshot =
|
|
114
|
-
(await askOnce(client, session, plan.url, configKey, payload)) ??
|
|
115
|
-
snapshot;
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
// A failed nudge must not fail the op — the caller polls instead.
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (snapshot) {
|
|
122
|
-
answeredWith.set(session, configKey);
|
|
123
|
-
if (plan.installed && configKey !== plan.configKey) {
|
|
124
|
-
// Remember the rotation, so the next process starts on a live key.
|
|
125
|
-
state.writeBus({
|
|
126
|
-
plugin: pluginPath,
|
|
127
|
-
configKey,
|
|
128
|
-
installedAt: clock.now(),
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
return { snapshot, configKey };
|
|
122
|
+
catch {
|
|
123
|
+
// A failed nudge must not fail the op — the caller polls instead.
|
|
132
124
|
}
|
|
133
|
-
|
|
125
|
+
}
|
|
126
|
+
if (snapshot) {
|
|
127
|
+
answeredWith.set(session, configKey);
|
|
128
|
+
return { snapshot, configKey };
|
|
134
129
|
}
|
|
135
130
|
processDisabled = "plugin did not answer; using zellij polling for this run";
|
|
136
131
|
return null;
|
|
@@ -149,7 +144,7 @@ export async function busSnapshot(client, state, session, clock, env = process.e
|
|
|
149
144
|
export async function busScreens(client, state, session, paneIds, clock, env = process.env) {
|
|
150
145
|
if (paneIds.length < 2)
|
|
151
146
|
return null;
|
|
152
|
-
const plan = planBus(client, state, env);
|
|
147
|
+
const plan = planBus(client, state, env, session);
|
|
153
148
|
if (!plan.enabled || !plan.url)
|
|
154
149
|
return null;
|
|
155
150
|
const configKey = answeredWith.get(session) ?? plan.configKey;
|
|
@@ -179,7 +174,7 @@ export async function busScreens(client, state, session, paneIds, clock, env = p
|
|
|
179
174
|
* instance, no reply — and the caller runs its own loop.
|
|
180
175
|
*/
|
|
181
176
|
export async function busWait(client, state, session, request, env = process.env) {
|
|
182
|
-
const plan = planBus(client, state, env);
|
|
177
|
+
const plan = planBus(client, state, env, session);
|
|
183
178
|
if (!plan.enabled || !plan.url)
|
|
184
179
|
return null;
|
|
185
180
|
const configKey = answeredWith.get(session) ?? plan.configKey;
|
|
@@ -202,7 +197,7 @@ export async function busWait(client, state, session, request, env = process.env
|
|
|
202
197
|
export async function busChanged(client, state, session, paneIds, env = process.env) {
|
|
203
198
|
if (paneIds.length === 0)
|
|
204
199
|
return null;
|
|
205
|
-
const plan = planBus(client, state, env);
|
|
200
|
+
const plan = planBus(client, state, env, session);
|
|
206
201
|
if (!plan.enabled || !plan.url)
|
|
207
202
|
return null;
|
|
208
203
|
const configKey = answeredWith.get(session) ?? plan.configKey;
|
|
@@ -217,14 +212,63 @@ export async function busChanged(client, state, session, paneIds, env = process.
|
|
|
217
212
|
return null;
|
|
218
213
|
return parsed.panes.length === paneIds.length ? parsed : null;
|
|
219
214
|
}
|
|
215
|
+
function installedReply(session, plugin, configKey, ready, panes, extra = {}) {
|
|
216
|
+
return {
|
|
217
|
+
ok: true,
|
|
218
|
+
data: {
|
|
219
|
+
session,
|
|
220
|
+
installed: true,
|
|
221
|
+
plugin,
|
|
222
|
+
url: busPluginUrl(plugin),
|
|
223
|
+
configKey,
|
|
224
|
+
ready,
|
|
225
|
+
panes,
|
|
226
|
+
...extra,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
async function listBusPluginPanes(client, session, pluginPath) {
|
|
231
|
+
const panes = await client.listPanes(session);
|
|
232
|
+
return panes.filter((pane) => isBusPluginPane(pane, pluginPath));
|
|
233
|
+
}
|
|
234
|
+
/** Close every zswarm-bus plugin pane in this session. Best-effort per pane. */
|
|
235
|
+
export async function closeBusPluginPanes(client, session, pluginPath) {
|
|
236
|
+
const closed = [];
|
|
237
|
+
const panes = await listBusPluginPanes(client, session, pluginPath);
|
|
238
|
+
for (const pane of panes) {
|
|
239
|
+
try {
|
|
240
|
+
await client.closePane({ session, paneId: pane.id });
|
|
241
|
+
closed.push(pane.id);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// A pane that vanished between list and close is the desired end state.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return closed;
|
|
248
|
+
}
|
|
220
249
|
/** Report on the bus, install it, or forget it. */
|
|
221
250
|
export async function busOp(client, state, args, clock, env = process.env, policy) {
|
|
251
|
+
const sessionArg = typeof args.session === "string" ? args.session : undefined;
|
|
222
252
|
if (isTrue(args.clear)) {
|
|
223
|
-
|
|
253
|
+
let session = null;
|
|
254
|
+
let closed = [];
|
|
255
|
+
try {
|
|
256
|
+
session = (await client.resolveSession(sessionArg)).session;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
session = sessionArg?.trim() || null;
|
|
260
|
+
}
|
|
261
|
+
if (session) {
|
|
262
|
+
closed = await closeBusPluginPanes(client, session, resolveBusPlugin(env) ?? state.readBus(session)?.plugin ?? null);
|
|
263
|
+
state.clearBus(session);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
state.clearBus();
|
|
267
|
+
}
|
|
224
268
|
resetBusCache();
|
|
225
|
-
return { ok: true, data: { installed: false, cleared: true } };
|
|
269
|
+
return { ok: true, data: { installed: false, cleared: true, closed } };
|
|
226
270
|
}
|
|
227
|
-
const { session } = await client.resolveSession(
|
|
271
|
+
const { session } = await client.resolveSession(sessionArg);
|
|
228
272
|
if (isTrue(args.install)) {
|
|
229
273
|
// Installing opens a pane, which a read-only crew is not allowed to do.
|
|
230
274
|
if (policy?.readOnly) {
|
|
@@ -234,32 +278,31 @@ export async function busOp(client, state, args, clock, env = process.env, polic
|
|
|
234
278
|
if (!plugin) {
|
|
235
279
|
throw new ZellijError("bus_missing", "no plugin wasm found; run pnpm build:plugin or set ZSWARM_BUS_PLUGIN");
|
|
236
280
|
}
|
|
237
|
-
const previous = state.readBus();
|
|
281
|
+
const previous = state.readBus(session);
|
|
238
282
|
const force = isTrue(args.force);
|
|
239
|
-
// Launching again would add a second instance, so an install that is
|
|
240
|
-
// already answering is a no-op unless the caller asked for a fresh one.
|
|
241
283
|
if (previous && !force) {
|
|
242
284
|
const live = await busSnapshot(client, state, session, clock, env);
|
|
243
285
|
if (live?.snapshot.ready) {
|
|
244
|
-
return {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
};
|
|
286
|
+
return installedReply(session, previous.plugin, live.configKey, true, live.snapshot.panes.length, { note: "already installed" });
|
|
287
|
+
}
|
|
288
|
+
const existing = await listBusPluginPanes(client, session, plugin);
|
|
289
|
+
if (existing.length > 0) {
|
|
290
|
+
return installedReply(session, previous.plugin, previous.configKey, false, null, {
|
|
291
|
+
pane: existing[0]?.id ?? null,
|
|
292
|
+
note: "already installed but not answering; approve the permission prompt in the existing pane — do not --force unless you intend to replace it",
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (clock.now() - previous.installedAt < INSTALL_IN_FLIGHT_MS) {
|
|
296
|
+
return installedReply(session, previous.plugin, previous.configKey, false, null, {
|
|
297
|
+
note: "install already in flight; wait for the permission prompt rather than launching another pane",
|
|
298
|
+
});
|
|
257
299
|
}
|
|
258
300
|
}
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
301
|
+
// Marker first so a concurrent install sees an in-flight write and
|
|
302
|
+
// stops. Then reap orphans so a reload cannot stack on the old copies.
|
|
303
|
+
const configKey = previous?.configKey ?? DEFAULT_BUS_KEY;
|
|
304
|
+
state.writeBus(session, { plugin, configKey, installedAt: clock.now() });
|
|
305
|
+
const closed = await closeBusPluginPanes(client, session, plugin);
|
|
263
306
|
const url = busPluginUrl(plugin);
|
|
264
307
|
const launched = await client.launchPlugin({
|
|
265
308
|
session,
|
|
@@ -268,27 +311,17 @@ export async function busOp(client, state, args, clock, env = process.env, polic
|
|
|
268
311
|
floating: true,
|
|
269
312
|
skipCache: force,
|
|
270
313
|
});
|
|
271
|
-
state.writeBus({ plugin, configKey, installedAt: clock.now() });
|
|
272
314
|
resetBusCache();
|
|
273
315
|
const probe = await busSnapshot(client, state, session, clock, env);
|
|
274
|
-
return {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
plugin,
|
|
280
|
-
|
|
281
|
-
configKey,
|
|
282
|
-
pane: launched.paneId,
|
|
283
|
-
ready: probe?.snapshot.ready ?? false,
|
|
284
|
-
panes: probe?.snapshot.panes.length ?? null,
|
|
285
|
-
note: probe
|
|
286
|
-
? "bus answering; list and status use it now, and this pane can be closed"
|
|
287
|
-
: "approve the plugin's permission prompt in the new pane, then re-run",
|
|
288
|
-
},
|
|
289
|
-
};
|
|
316
|
+
return installedReply(session, plugin, configKey, probe?.snapshot.ready ?? false, probe?.snapshot.panes.length ?? null, {
|
|
317
|
+
pane: launched.paneId,
|
|
318
|
+
closed,
|
|
319
|
+
note: probe
|
|
320
|
+
? "bus answering; keep this floating pane open — closing it unloads the bus"
|
|
321
|
+
: "approve the plugin's permission prompt in the new pane, then re-run without --install",
|
|
322
|
+
});
|
|
290
323
|
}
|
|
291
|
-
const plan = planBus(client, state, env);
|
|
324
|
+
const plan = planBus(client, state, env, session);
|
|
292
325
|
const probe = plan.enabled
|
|
293
326
|
? await busSnapshot(client, state, session, clock, env)
|
|
294
327
|
: null;
|
package/dist/ops/delivery.d.ts
CHANGED
|
@@ -14,7 +14,37 @@ export type DeliveryResult = {
|
|
|
14
14
|
};
|
|
15
15
|
};
|
|
16
16
|
export type SubmitMode = "auto" | "double-enter" | "none";
|
|
17
|
-
export
|
|
17
|
+
export type SenderSource = "arg" | "env" | "title" | "default";
|
|
18
|
+
export type SenderLabel = {
|
|
19
|
+
label: string;
|
|
20
|
+
source: SenderSource;
|
|
21
|
+
};
|
|
22
|
+
/** Drop characters that would break `[zswarm from=…]` or wrap the prefix. */
|
|
23
|
+
export declare function sanitizeSenderLabel(raw: string): string;
|
|
24
|
+
export declare function selfPaneTitle(client: Pick<ZellijClient, "selfPaneId">, panes: ZellijPane[]): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Label for `[zswarm from=…]`. Explicit `from` wins, then `ZSWARM_FROM`,
|
|
27
|
+
* then the sending pane's title when we know that pane, else `swarm`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveSender(args: Record<string, unknown>, opts?: {
|
|
30
|
+
env?: NodeJS.ProcessEnv;
|
|
31
|
+
selfTitle?: string | null;
|
|
32
|
+
}): SenderLabel;
|
|
33
|
+
export declare function senderLabel(args: Record<string, unknown>, opts?: {
|
|
34
|
+
env?: NodeJS.ProcessEnv;
|
|
35
|
+
selfTitle?: string | null;
|
|
36
|
+
}): string;
|
|
37
|
+
/** Stamp `from` so deliverTo and the op result use the same resolved label. */
|
|
38
|
+
export declare function withSenderLabel(args: Record<string, unknown>, opts?: {
|
|
39
|
+
env?: NodeJS.ProcessEnv;
|
|
40
|
+
selfTitle?: string | null;
|
|
41
|
+
}): Record<string, unknown>;
|
|
42
|
+
/**
|
|
43
|
+
* Before forwarding over `ZSWARM_SERVE`, stamp a caller-chosen label. Leaving
|
|
44
|
+
* `from` unset used to let the Zellij-side process fill in its pane title —
|
|
45
|
+
* which is the serve daemon when it runs in a pane, not the remote sender.
|
|
46
|
+
*/
|
|
47
|
+
export declare function attachKnownSender(args: Record<string, unknown>, env: NodeJS.ProcessEnv): Record<string, unknown>;
|
|
18
48
|
export declare function bodyText(client: ZellijClient, args: Record<string, unknown>, body: string): string;
|
|
19
49
|
/**
|
|
20
50
|
* Caller `submit=` always wins. With none given, the pane's harness picks
|
package/dist/ops/delivery.js
CHANGED
|
@@ -2,8 +2,59 @@ import { resolveHarness } from "../harness.js";
|
|
|
2
2
|
import { isTrue, numberArg } from "./util.js";
|
|
3
3
|
/** Cursor / Claude leave this in scrollback after a successful submit. */
|
|
4
4
|
const PASTE_MARKER = "[Pasted text";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const SENDER_LABEL_MAX = 48;
|
|
6
|
+
/** Drop characters that would break `[zswarm from=…]` or wrap the prefix. */
|
|
7
|
+
export function sanitizeSenderLabel(raw) {
|
|
8
|
+
const cleaned = raw.replace(/[\r\n[\]]+/g, " ").replace(/\s+/g, " ").trim();
|
|
9
|
+
if (!cleaned)
|
|
10
|
+
return "";
|
|
11
|
+
return cleaned.length <= SENDER_LABEL_MAX
|
|
12
|
+
? cleaned
|
|
13
|
+
: cleaned.slice(0, SENDER_LABEL_MAX).trimEnd();
|
|
14
|
+
}
|
|
15
|
+
export function selfPaneTitle(client, panes) {
|
|
16
|
+
const id = client.selfPaneId;
|
|
17
|
+
if (!id)
|
|
18
|
+
return null;
|
|
19
|
+
const title = panes.find((p) => p.id === id)?.title;
|
|
20
|
+
return title?.trim() ? title : null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Label for `[zswarm from=…]`. Explicit `from` wins, then `ZSWARM_FROM`,
|
|
24
|
+
* then the sending pane's title when we know that pane, else `swarm`.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveSender(args, opts = {}) {
|
|
27
|
+
if (typeof args.from === "string") {
|
|
28
|
+
const explicit = sanitizeSenderLabel(args.from);
|
|
29
|
+
if (explicit)
|
|
30
|
+
return { label: explicit, source: "arg" };
|
|
31
|
+
}
|
|
32
|
+
const envFrom = sanitizeSenderLabel(opts.env?.ZSWARM_FROM ?? "");
|
|
33
|
+
if (envFrom)
|
|
34
|
+
return { label: envFrom, source: "env" };
|
|
35
|
+
const title = sanitizeSenderLabel(opts.selfTitle ?? "");
|
|
36
|
+
if (title)
|
|
37
|
+
return { label: title, source: "title" };
|
|
38
|
+
return { label: "swarm", source: "default" };
|
|
39
|
+
}
|
|
40
|
+
export function senderLabel(args, opts = {}) {
|
|
41
|
+
return resolveSender(args, opts).label;
|
|
42
|
+
}
|
|
43
|
+
/** Stamp `from` so deliverTo and the op result use the same resolved label. */
|
|
44
|
+
export function withSenderLabel(args, opts = {}) {
|
|
45
|
+
return { ...args, from: senderLabel(args, opts) };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Before forwarding over `ZSWARM_SERVE`, stamp a caller-chosen label. Leaving
|
|
49
|
+
* `from` unset used to let the Zellij-side process fill in its pane title —
|
|
50
|
+
* which is the serve daemon when it runs in a pane, not the remote sender.
|
|
51
|
+
*/
|
|
52
|
+
export function attachKnownSender(args, env) {
|
|
53
|
+
const op = String(args.op ?? "");
|
|
54
|
+
if (op !== "send" && op !== "broadcast")
|
|
55
|
+
return args;
|
|
56
|
+
const sender = resolveSender(args, { env });
|
|
57
|
+
return { ...args, from: sender.label };
|
|
7
58
|
}
|
|
8
59
|
export function bodyText(client, args, body) {
|
|
9
60
|
return isTrue(args.raw) ? body : client.formatPeerMessage(senderLabel(args), body);
|