@zergai/ztc 0.9.0-beta.1 → 0.10.0-beta.1

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.
@@ -0,0 +1,403 @@
1
+ /**
2
+ * `ztc workspace-mcp` — the MCP server attached to Claude Code and Codex panes.
3
+ *
4
+ * The CLI starts it from its session-scoped MCP config (see panes/agent_cli.ts).
5
+ * It reaches this workspace through the pane relay ($ZTC_PANE_RELAY) as pane
6
+ * $ZTC_PANE_ID, and gives the agent the workspace: list panes; open a ZTC
7
+ * agent, Claude Code, Codex or a shell; read a pane; type into one; message
8
+ * another agent; close a pane. Messages travel over ZTC's bus when the mirror
9
+ * is reachable, so ZTC panes receive them as messages from this pane;
10
+ * otherwise they are typed in with the sender's name.
11
+ */
12
+ import { request } from 'node:http';
13
+ import { randomUUID } from 'node:crypto';
14
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
17
+ import { childZtcPaneEnv, closeTerminalPaneTool, listWorkspacePanes, readTerminalPaneTool, setPaneToolsOwnId, setPaneToolsTuiProcess, waitForPaneCreatedOrError, writeTerminalPaneTool, } from '../agent/tools/pane.js';
18
+ import { AGENT_CLIS, findAgentCli, nextPaneTitle, workspaceMcpProgram } from '../panes/agent_cli.js';
19
+ import { TYPED_ENTER_DELAY_MS, formatAgentPaneMessage, openAgentPane } from '../panes/agent_panes.js';
20
+ import { sessionNamePaneTitle } from '../panes/labels.js';
21
+ import { buildZtcPaneLaunchCommand, createZtcLaunchSpec } from '../panes/ztc_launch.js';
22
+ import { PaneRelayClient } from '../ui/ratatui/pane_relay.js';
23
+ import { buildUserSubprocessEnvOverrides } from '../utils/subprocess_env.js';
24
+ import { getVersion } from '../utils/version.js';
25
+ const PANE_REF = {
26
+ description: 'The pane: its id, or its name as list_panes shows it.',
27
+ anyOf: [{ type: 'integer', minimum: 0 }, { type: 'string', minLength: 1 }],
28
+ };
29
+ const TOOLS = [
30
+ {
31
+ name: 'list_panes',
32
+ description: 'List the panes in this ZTC workspace: each pane\'s id, name and kind (ZTC agent, terminal, document), and which one is yours. Pane ids and names work in every other ztc tool.',
33
+ inputSchema: { type: 'object', properties: {}, additionalProperties: false },
34
+ },
35
+ {
36
+ name: 'open_pane',
37
+ description: 'Open a new pane in this ZTC workspace: a ZTC agent ("ztc"), Claude Code ("claude"), Codex ("codex") or a shell ("shell"). Claude Code and Codex run the user\'s own installed CLI on the user\'s own account, with these same ztc tools. Give a task to start an agent working on it at once. Returns the new pane\'s id.',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ kind: { type: 'string', enum: ['ztc', 'claude', 'codex', 'shell'], description: 'What runs in the pane.' },
42
+ task: { type: 'string', description: 'For an agent pane: its first message. Ignored for a shell.' },
43
+ name: { type: 'string', description: 'Name for the pane. Defaults to "ZTC", "Claude Code", "Codex" or "Shell", numbered when taken.' },
44
+ cwd: { type: 'string', description: 'Working directory. Defaults to yours.' },
45
+ placement: { type: 'string', enum: ['right', 'below', 'tab'], description: 'Next to your pane (right or below) or in a new tab. Defaults to the workspace\'s layout.' },
46
+ },
47
+ required: ['kind'],
48
+ additionalProperties: false,
49
+ },
50
+ },
51
+ {
52
+ name: 'read_pane',
53
+ description: 'Read a pane\'s recent output: its scrollback, or just what is on screen with viewport=true.',
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ pane: PANE_REF,
58
+ lines: { type: 'integer', minimum: 1, maximum: 2000, description: 'Lines of scrollback (default 200).' },
59
+ viewport: { type: 'boolean', description: 'Read exactly what is visible instead.' },
60
+ },
61
+ required: ['pane'],
62
+ additionalProperties: false,
63
+ },
64
+ },
65
+ {
66
+ name: 'type_into_pane',
67
+ description: 'Type text into a pane as keystrokes, then press Enter unless submit is false. Use it to run a command in a shell pane or answer a prompt on screen. To talk to another agent prefer send_message, which says who the message is from.',
68
+ inputSchema: {
69
+ type: 'object',
70
+ properties: {
71
+ pane: PANE_REF,
72
+ text: { type: 'string', minLength: 1 },
73
+ submit: { type: 'boolean', description: 'Press Enter after the text (default true).' },
74
+ },
75
+ required: ['pane', 'text'],
76
+ additionalProperties: false,
77
+ },
78
+ },
79
+ {
80
+ name: 'send_message',
81
+ description: 'Send a message to another pane\'s agent, or "all" for every ZTC agent. ZTC agents receive it as a message from you; Claude Code and Codex panes see it typed in with your pane\'s name. Replies come back to you the same way: typed into your input, starting "[Message from …]".',
82
+ inputSchema: {
83
+ type: 'object',
84
+ properties: {
85
+ to: { description: 'A pane id or name, or "all".', anyOf: [{ type: 'integer', minimum: 0 }, { type: 'string', minLength: 1 }] },
86
+ text: { type: 'string', minLength: 1 },
87
+ },
88
+ required: ['to', 'text'],
89
+ additionalProperties: false,
90
+ },
91
+ },
92
+ {
93
+ name: 'close_pane',
94
+ description: 'Close a pane and stop its program. Your own pane cannot be closed this way.',
95
+ inputSchema: { type: 'object', properties: { pane: PANE_REF }, required: ['pane'], additionalProperties: false },
96
+ },
97
+ ];
98
+ class ToolError extends Error {
99
+ }
100
+ const KIND_LABELS = { ztc: 'ZTC', process: 'terminal', command: 'terminal', doc: 'document', feed: 'feed' };
101
+ function paneName(pane) {
102
+ return pane.labels?.manual_title || pane.title;
103
+ }
104
+ function normalizedName(name) {
105
+ return name.trim().replace(/^\[[^\]]+\]\s+/, '').toLowerCase();
106
+ }
107
+ function placementOf(value, ownPaneId) {
108
+ if (value === 'tab')
109
+ return { newWindow: true };
110
+ const direction = value === 'right' ? 'horizontal' : value === 'below' ? 'vertical' : undefined;
111
+ if (!direction)
112
+ return {};
113
+ return ownPaneId === null ? { direction } : { direction, target: ownPaneId };
114
+ }
115
+ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
116
+ /** The MCP server, connected to a workspace through `transport`. */
117
+ export function createWorkspaceMcpServer(options) {
118
+ const { transport, ownPaneId } = options;
119
+ const env = options.env ?? process.env;
120
+ if (transport) {
121
+ setPaneToolsTuiProcess(transport);
122
+ if (ownPaneId !== null)
123
+ setPaneToolsOwnId(ownPaneId);
124
+ }
125
+ const workspace = () => {
126
+ if (!transport || !transport.isMuxMode) {
127
+ throw new ToolError('This session is not running in a ZTC workspace pane, so there are no panes to work with. Open it from ZTC (/claude or /codex, or the new-pane menu).');
128
+ }
129
+ return transport;
130
+ };
131
+ const panes = async () => {
132
+ const listed = await listWorkspacePanes(workspace());
133
+ if (listed instanceof Error)
134
+ throw new ToolError(listed.message.replace(/^Error: /, ''));
135
+ return listed;
136
+ };
137
+ const findPane = async (ref) => {
138
+ const all = await panes();
139
+ if (typeof ref === 'number' || (typeof ref === 'string' && /^\d+$/.test(ref.trim()))) {
140
+ const id = Number(ref);
141
+ const pane = all.find(candidate => candidate.id === id);
142
+ if (!pane)
143
+ throw new ToolError(`There is no pane ${id}. Panes: ${all.map(p => `[${p.id}] ${paneName(p)}`).join(', ')}.`);
144
+ return pane;
145
+ }
146
+ const wanted = normalizedName(String(ref ?? ''));
147
+ const others = all.filter(pane => pane.id !== ownPaneId);
148
+ const matches = others.filter(pane => normalizedName(paneName(pane)) === wanted);
149
+ if (matches.length === 1)
150
+ return matches[0];
151
+ if (matches.length > 1) {
152
+ throw new ToolError(`${matches.length} panes are named "${String(ref)}" (${matches.map(pane => pane.id).join(', ')}); use a pane id.`);
153
+ }
154
+ throw new ToolError(`No pane is named "${String(ref)}". Panes: ${others.map(p => `[${p.id}] ${paneName(p)}`).join(', ') || 'none besides yours'}.`);
155
+ };
156
+ const ownName = async () => {
157
+ if (ownPaneId === null)
158
+ return 'an agent';
159
+ const own = (await panes()).find(pane => pane.id === ownPaneId);
160
+ return own ? paneName(own) : `pane ${ownPaneId}`;
161
+ };
162
+ /** Type into a pane the way its program reads input. */
163
+ const typeInto = async (pane, text, submit) => {
164
+ if (pane.pane_type === 'ztc') {
165
+ return String(await writeTerminalPaneTool.execute({ pane_id: pane.id, input: text, submit }));
166
+ }
167
+ const typed = String(await writeTerminalPaneTool.execute({ pane_id: pane.id, input: text, submit: false }));
168
+ if (!submit)
169
+ return typed;
170
+ await delay(TYPED_ENTER_DELAY_MS);
171
+ await writeTerminalPaneTool.execute({ pane_id: pane.id, input: '\r', submit: false });
172
+ return `Typed into ${paneName(pane)} (pane ${pane.id}) and pressed Enter.`;
173
+ };
174
+ const openZtcPane = async (args, cwd) => {
175
+ const tui = workspace();
176
+ const titles = (await panes()).map(paneName);
177
+ const title = typeof args.name === 'string' && args.name.trim() ? args.name.trim() : nextPaneTitle('ZTC', titles);
178
+ const spec = createZtcLaunchSpec({ cwd, title });
179
+ // This process was started as `workspace-mcp`; the child is a ZTC session.
180
+ const built = buildZtcPaneLaunchCommand(spec, process.argv.slice(0, 2));
181
+ if (built instanceof Error)
182
+ throw new ToolError(built.message.replace(/^Error: /, ''));
183
+ const task = typeof args.task === 'string' ? args.task.trim() : '';
184
+ const requestId = `ztc-pane-${randomUUID()}`;
185
+ const created = waitForPaneCreatedOrError(tui, { requestId }, 15000);
186
+ const placement = placementOf(args.placement, ownPaneId);
187
+ const sent = tui.send({
188
+ type: 'create_pane',
189
+ request_id: requestId,
190
+ command: built.command,
191
+ args: task ? [...built.args, '--', task] : built.args,
192
+ cwd: built.cwd || cwd,
193
+ title,
194
+ pane_type: 'ztc',
195
+ ...(placement.direction ? { direction: placement.direction, ratio: 0.5 } : {}),
196
+ ...(placement.target !== undefined ? { target: placement.target } : {}),
197
+ ...(placement.newWindow ? { new_window: true } : {}),
198
+ env: childZtcPaneEnv(),
199
+ });
200
+ if (!sent) {
201
+ created.catch(() => { });
202
+ throw new ToolError('The ZTC pane could not be requested: the workspace is not connected.');
203
+ }
204
+ const event = await created;
205
+ if (event.type === 'error')
206
+ throw new ToolError(`The workspace refused the pane: ${event.message}`);
207
+ if (event.type !== 'pane_created')
208
+ throw new ToolError('Unexpected response from the workspace.');
209
+ tui.send(sessionNamePaneTitle(event.pane_id, title));
210
+ return `Opened ZTC agent "${title}" in pane ${event.pane_id} (${cwd})${task ? ' with its task' : ''}.`;
211
+ };
212
+ const openShellPane = async (args, cwd) => {
213
+ const tui = workspace();
214
+ const titles = (await panes()).map(paneName);
215
+ const title = typeof args.name === 'string' && args.name.trim() ? args.name.trim() : nextPaneTitle('Shell', titles);
216
+ const requestId = `create-shell-${randomUUID()}`;
217
+ const created = waitForPaneCreatedOrError(tui, { requestId }, 15000);
218
+ const placement = placementOf(args.placement, ownPaneId);
219
+ const sent = tui.send({
220
+ type: 'create_pane',
221
+ request_id: requestId,
222
+ command: env.SHELL || '/bin/sh',
223
+ args: [],
224
+ cwd,
225
+ title,
226
+ pane_type: 'process',
227
+ ...(placement.direction ? { direction: placement.direction, ratio: 0.5 } : {}),
228
+ ...(placement.target !== undefined ? { target: placement.target } : {}),
229
+ ...(placement.newWindow ? { new_window: true } : {}),
230
+ env: buildUserSubprocessEnvOverrides(),
231
+ });
232
+ if (!sent) {
233
+ created.catch(() => { });
234
+ throw new ToolError('The shell pane could not be requested: the workspace is not connected.');
235
+ }
236
+ const event = await created;
237
+ if (event.type === 'error')
238
+ throw new ToolError(`The workspace refused the pane: ${event.message}`);
239
+ if (event.type !== 'pane_created')
240
+ throw new ToolError('Unexpected response from the workspace.');
241
+ tui.send(sessionNamePaneTitle(event.pane_id, title));
242
+ return `Opened a shell "${title}" in pane ${event.pane_id} (${cwd}).`;
243
+ };
244
+ const handlers = {
245
+ list_panes: async () => {
246
+ const all = await panes();
247
+ if (all.length === 0)
248
+ return 'The workspace has no panes.';
249
+ return all
250
+ .slice()
251
+ .sort((a, b) => a.id - b.id)
252
+ .map(pane => {
253
+ const notes = [
254
+ pane.id === ownPaneId ? 'you' : '',
255
+ pane.is_focused ? 'focused' : '',
256
+ pane.is_alive ? '' : 'exited',
257
+ ].filter(Boolean);
258
+ return `[${pane.id}] ${paneName(pane)} (${KIND_LABELS[pane.pane_type] ?? pane.pane_type})${notes.length ? ` — ${notes.join(', ')}` : ''}`;
259
+ })
260
+ .join('\n');
261
+ },
262
+ open_pane: async (args) => {
263
+ const kind = String(args.kind ?? '');
264
+ const cwd = typeof args.cwd === 'string' && args.cwd.trim() ? args.cwd.trim() : (options.cwd ?? process.cwd());
265
+ if (kind === 'ztc')
266
+ return openZtcPane(args, cwd);
267
+ if (kind === 'shell')
268
+ return openShellPane(args, cwd);
269
+ if (kind !== 'claude' && kind !== 'codex')
270
+ throw new ToolError('kind must be "ztc", "claude", "codex" or "shell".');
271
+ const task = typeof args.task === 'string' ? args.task.trim() : '';
272
+ const result = await openAgentPane(workspace(), {
273
+ cli: kind,
274
+ request: task ? { words: [], prompt: task } : { words: [] },
275
+ cwd,
276
+ ...(typeof args.name === 'string' && args.name.trim() ? { title: args.name.trim() } : {}),
277
+ ...placementOf(args.placement, ownPaneId),
278
+ findBinary: options.findBinary ?? findAgentCli,
279
+ server: options.server === undefined ? workspaceMcpProgram(process.argv.slice(0, 2)) : options.server,
280
+ env,
281
+ });
282
+ if (result.error)
283
+ throw new ToolError(result.error);
284
+ const opened = `Opened ${result.title ?? AGENT_CLIS[kind].label} in pane ${result.paneId} (${cwd})${task ? ' with its task' : ''}.`;
285
+ return result.notice ? `${opened} ${result.notice}` : opened;
286
+ },
287
+ read_pane: async (args) => {
288
+ const pane = await findPane(args.pane);
289
+ return String(await readTerminalPaneTool.execute({
290
+ pane_id: pane.id,
291
+ ...(args.lines !== undefined ? { lines: args.lines } : {}),
292
+ ...(args.viewport === true ? { viewport: true } : {}),
293
+ }));
294
+ },
295
+ type_into_pane: async (args) => {
296
+ const pane = await findPane(args.pane);
297
+ const text = String(args.text ?? '');
298
+ if (!text)
299
+ throw new ToolError('text is required.');
300
+ return typeInto(pane, text, args.submit !== false);
301
+ },
302
+ send_message: async (args) => {
303
+ const text = String(args.text ?? '').trim();
304
+ if (!text)
305
+ throw new ToolError('text is required.');
306
+ const from = { id: ownPaneId ?? -1, name: await ownName() };
307
+ const broadcast = typeof args.to === 'string' && args.to.trim().toLowerCase() === 'all';
308
+ const target = broadcast ? null : await findPane(args.to);
309
+ if (options.bus) {
310
+ await options.bus.send({ from, to: target ? target.id : 'all', type: 'ztc_message', text });
311
+ return target ? `Message sent to ${paneName(target)} (pane ${target.id}).` : 'Message sent to every ZTC agent.';
312
+ }
313
+ if (!target)
314
+ throw new ToolError('Sending to "all" needs ZTC\'s message bus, which this pane cannot reach; send to one pane at a time.');
315
+ await typeInto(target, formatAgentPaneMessage({ from, text }), true);
316
+ return `Message typed into ${paneName(target)} (pane ${target.id}).`;
317
+ },
318
+ close_pane: async (args) => {
319
+ const pane = await findPane(args.pane);
320
+ if (pane.id === ownPaneId)
321
+ throw new ToolError('That is your own pane; the user can close it, or you can exit.');
322
+ return String(await closeTerminalPaneTool.execute({ pane_id: pane.id }));
323
+ },
324
+ };
325
+ const server = new Server({ name: 'ztc', version: getVersion() }, { capabilities: { tools: {} } });
326
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS.map(tool => ({ ...tool })) }));
327
+ server.setRequestHandler(CallToolRequestSchema, async (call) => {
328
+ const handler = handlers[call.params.name];
329
+ if (!handler)
330
+ return { content: [{ type: 'text', text: `Unknown tool ${call.params.name}.` }], isError: true };
331
+ try {
332
+ const text = await handler((call.params.arguments ?? {}));
333
+ return { content: [{ type: 'text', text }], isError: /^Error\b/.test(text) };
334
+ }
335
+ catch (error) {
336
+ const message = error instanceof Error ? error.message : String(error);
337
+ return { content: [{ type: 'text', text: message.replace(/^Error: /, '') }], isError: true };
338
+ }
339
+ });
340
+ return server;
341
+ }
342
+ /** Posts bus messages to the overseer's mirror, as MirrorClient does, without joining the bus. */
343
+ export function mirrorBusSender(env = process.env) {
344
+ const port = Number(env.ZTC_MIRROR_PORT);
345
+ const token = env.ZTC_MIRROR_INTERNAL_TOKEN;
346
+ if (!Number.isInteger(port) || port <= 0 || !token)
347
+ return null;
348
+ const group = env.ZTC_MIRROR_GROUP?.trim();
349
+ return {
350
+ send: message => new Promise((resolve, reject) => {
351
+ const body = JSON.stringify(message);
352
+ const req = request({
353
+ hostname: 'localhost',
354
+ port,
355
+ path: group ? `/bus/send/${group}` : '/bus/send',
356
+ method: 'POST',
357
+ headers: {
358
+ 'Content-Type': 'application/json',
359
+ 'Content-Length': Buffer.byteLength(body),
360
+ 'x-ztc-internal-token': token,
361
+ },
362
+ }, res => {
363
+ let text = '';
364
+ res.on('data', (chunk) => { text += chunk.toString(); });
365
+ res.on('end', () => {
366
+ if (res.statusCode === 200)
367
+ resolve();
368
+ else
369
+ reject(new ToolError(`ZTC's message bus refused the message (HTTP ${res.statusCode}): ${text.slice(0, 200)}`));
370
+ });
371
+ });
372
+ req.on('error', error => reject(new ToolError(`ZTC's message bus is unreachable: ${error.message}`)));
373
+ req.end(body);
374
+ }),
375
+ };
376
+ }
377
+ /** Entry point for `ztc workspace-mcp`: serve on stdio until the CLI closes it. */
378
+ export async function runWorkspaceMcpServer(env = process.env) {
379
+ let relay = null;
380
+ if (env.ZTC_PANE_RELAY) {
381
+ relay = new PaneRelayClient(env.ZTC_PANE_RELAY);
382
+ try {
383
+ await relay.connect();
384
+ }
385
+ catch (error) {
386
+ process.stderr.write(`ztc workspace-mcp: cannot reach this workspace (${error.message}); its tools will say so.\n`);
387
+ relay = null;
388
+ }
389
+ }
390
+ const paneId = Number(env.ZTC_PANE_ID);
391
+ const server = createWorkspaceMcpServer({
392
+ transport: relay,
393
+ ownPaneId: Number.isSafeInteger(paneId) && paneId >= 0 ? paneId : null,
394
+ bus: mirrorBusSender(env),
395
+ env,
396
+ });
397
+ const stdio = new StdioServerTransport();
398
+ const closed = new Promise(resolve => { stdio.onclose = () => resolve(); });
399
+ await server.connect(stdio);
400
+ process.stdin.on('end', () => { void server.close(); });
401
+ await closed;
402
+ relay?.disconnect();
403
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Agent CLIs that run in ZTC panes under the user's own accounts.
3
+ *
4
+ * ZTC hosts Claude Code and Codex the way a terminal does: it runs the user's
5
+ * own, unmodified `claude` or `codex` binary in a pane, and the user signs in
6
+ * through that CLI's own flow. ZTC never reads, stores or forwards their
7
+ * credentials. (Anthropic's terms for Claude subscription sign-in:
8
+ * https://code.claude.com/docs/en/legal-and-compliance.)
9
+ *
10
+ * What ZTC adds is a session-scoped MCP server, `ztc workspace-mcp`, passed on
11
+ * the command line (Claude Code `--mcp-config`, Codex `-c mcp_servers.ztc.*`),
12
+ * so the agent can list, open, read, type into, message and close the other
13
+ * panes of the workspace. The user's own CLI configuration is never edited.
14
+ */
15
+ export type AgentCliId = 'claude' | 'codex';
16
+ export interface AgentCliSpec {
17
+ id: AgentCliId;
18
+ /** What the pane is called and what the chooser shows. */
19
+ label: string;
20
+ /** The executable's name on PATH. */
21
+ binary: string;
22
+ /** The vendor's install page. */
23
+ setupUrl: string;
24
+ /** One install command from that page. */
25
+ install: string;
26
+ }
27
+ export declare const AGENT_CLIS: Readonly<Record<AgentCliId, AgentCliSpec>>;
28
+ export declare const AGENT_CLI_IDS: readonly AgentCliId[];
29
+ export declare function isAgentCliId(value: string): value is AgentCliId;
30
+ export interface BinaryLookup {
31
+ env?: NodeJS.ProcessEnv;
32
+ home?: string;
33
+ platform?: NodeJS.Platform;
34
+ /** Shared bin directories checked after PATH and the user's own. */
35
+ systemDirs?: readonly string[];
36
+ }
37
+ /** The absolute path of the user's `claude` or `codex`, or null when it is not installed. */
38
+ export declare function findAgentCli(id: AgentCliId, lookup?: BinaryLookup): string | null;
39
+ export declare class CliArgumentsError extends Error {
40
+ constructor(message: string);
41
+ }
42
+ /**
43
+ * Split words the way a POSIX shell does, without expanding anything: quotes
44
+ * group, a backslash escapes, and `$`, backticks, globs and `;` stay literal.
45
+ */
46
+ export declare function splitCliArguments(raw: string): string[];
47
+ /** Words as a shell would accept them back: quoted only where needed. */
48
+ export declare function formatCliArguments(words: readonly string[]): string;
49
+ /** What to start the CLI with: its own flags, an opening prompt, or both. */
50
+ export interface AgentLaunchRequest {
51
+ words: string[];
52
+ prompt?: string;
53
+ }
54
+ /**
55
+ * Read the text after `/claude` or `/codex`. Text that starts with a flag is
56
+ * the CLI's own arguments, split like a shell command line (`/claude
57
+ * --continue`, `/codex -m gpt-5 "review this"`). Anything else is the opening
58
+ * prompt, kept exactly as typed, apostrophes and all (`/claude don't touch the
59
+ * migrations`).
60
+ */
61
+ export declare function parseAgentCommandBody(body: string): AgentLaunchRequest;
62
+ export interface PaneProgram {
63
+ command: string;
64
+ args: string[];
65
+ }
66
+ export declare const WORKSPACE_MCP_SERVER_NAME = "ztc";
67
+ /**
68
+ * Variables the workspace server reads. Codex starts MCP servers with a short
69
+ * allowlist of variables, so these are named for it to forward; Claude Code
70
+ * passes its whole environment. They reach the pane through its environment,
71
+ * never its command line.
72
+ */
73
+ export declare const WORKSPACE_MCP_ENV: readonly string[];
74
+ /** The CLI's own arguments that attach the workspace server, or nothing without one. */
75
+ export declare function workspaceMcpArgs(id: AgentCliId, server: PaneProgram | null): string[];
76
+ /**
77
+ * The CLI's full argument list. Codex takes its overrides first, before any
78
+ * subcommand. Claude Code's `--mcp-config` takes several values, so it goes
79
+ * after the user's flags, ahead of a `--` they typed, and a prompt follows its
80
+ * own `--` so a prompt starting with "-" is never read as a flag.
81
+ */
82
+ export declare function agentCliArgs(id: AgentCliId, request: AgentLaunchRequest, server: PaneProgram | null): string[];
83
+ /**
84
+ * Run the binary from a login shell, as custom panes are, so PATH and the rest
85
+ * of the login environment apply. The binary and its arguments travel as
86
+ * separate argv entries (`exec "$0" "$@"`): no shell ever re-parses them, so
87
+ * quotes, `$` and `;` in a prompt arrive exactly as written. A shell outside
88
+ * the POSIX family (fish, nushell), or no shell at all, is replaced by /bin/sh
89
+ * for this one step.
90
+ */
91
+ export declare function loginShellExec(binary: string, args: readonly string[], shell: string | undefined, platform?: NodeJS.Platform): PaneProgram;
92
+ /** "Claude Code", then "Claude Code 2", "Claude Code 3"… so every pane name stays unique. */
93
+ export declare function nextPaneTitle(label: string, existing: readonly string[]): string;
94
+ /**
95
+ * The pane's environment overrides: ZTC's control variables are cleared as for
96
+ * any user program, except what the workspace server needs to reach this
97
+ * workspace. Everything else is the user's own environment, untouched, so the
98
+ * CLI offers every sign-in method it normally would.
99
+ */
100
+ export declare function agentPaneEnv(source?: NodeJS.ProcessEnv): Record<string, string>;
101
+ /**
102
+ * How to start `ztc workspace-mcp` from the running installation: the desktop
103
+ * app's own launcher, or this Node and entrypoint.
104
+ */
105
+ export declare function workspaceMcpProgram(argv?: readonly string[]): PaneProgram | Error;