@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.
- package/README.md +43 -0
- package/bin/zmux-darwin-arm64 +0 -0
- package/bin/ztc-audio-darwin-arm64 +0 -0
- package/bin/ztc-capture-darwin-arm64 +0 -0
- package/bin/ztc-tui-darwin-arm64 +0 -0
- package/bin/zterm-darwin-arm64 +0 -0
- package/dist/agent/commands/agent_cli.d.ts +21 -0
- package/dist/agent/commands/agent_cli.js +66 -0
- package/dist/agent/commands/index.js +2 -1
- package/dist/agent/tools/bus_message.js +27 -4
- package/dist/agent/tools/pane.d.ts +5 -1
- package/dist/agent/tools/pane.js +19 -8
- package/dist/app/command_context.js +5 -3
- package/dist/cli_main.js +13 -0
- package/dist/cli_ratatui.d.ts +10 -0
- package/dist/cli_ratatui.js +62 -17
- package/dist/mcp/workspace_server.d.ts +44 -0
- package/dist/mcp/workspace_server.js +403 -0
- package/dist/panes/agent_cli.d.ts +105 -0
- package/dist/panes/agent_cli.js +295 -0
- package/dist/panes/agent_panes.d.ts +61 -0
- package/dist/panes/agent_panes.js +119 -0
- package/dist/panes/new_pane_choices.d.ts +14 -0
- package/dist/panes/new_pane_choices.js +36 -0
- package/dist/ui/ratatui/protocol.d.ts +3 -0
- package/dist/web/mirror_stream.d.ts +7 -0
- package/dist/web/mirror_stream.js +17 -0
- package/dist/zlm-embed.mjs +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,295 @@
|
|
|
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
|
+
import { accessSync, constants, existsSync, statSync } from 'node:fs';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { basename, dirname, join } from 'node:path';
|
|
18
|
+
import { resolveOwnedCliLaunch } from '../desktop/cli_launch.js';
|
|
19
|
+
import { buildUserSubprocessEnvOverrides } from '../utils/subprocess_env.js';
|
|
20
|
+
export const AGENT_CLIS = Object.freeze({
|
|
21
|
+
claude: {
|
|
22
|
+
id: 'claude',
|
|
23
|
+
label: 'Claude Code',
|
|
24
|
+
binary: 'claude',
|
|
25
|
+
setupUrl: 'https://code.claude.com/docs/en/setup',
|
|
26
|
+
install: 'curl -fsSL https://claude.ai/install.sh | bash',
|
|
27
|
+
},
|
|
28
|
+
codex: {
|
|
29
|
+
id: 'codex',
|
|
30
|
+
label: 'Codex',
|
|
31
|
+
binary: 'codex',
|
|
32
|
+
setupUrl: 'https://developers.openai.com/codex/cli',
|
|
33
|
+
install: 'npm install -g @openai/codex',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
export const AGENT_CLI_IDS = Object.freeze(['claude', 'codex']);
|
|
37
|
+
export function isAgentCliId(value) {
|
|
38
|
+
return AGENT_CLI_IDS.includes(value);
|
|
39
|
+
}
|
|
40
|
+
const SYSTEM_BIN_DIRS = ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin'];
|
|
41
|
+
function isExecutableFile(path) {
|
|
42
|
+
try {
|
|
43
|
+
if (!statSync(path).isFile())
|
|
44
|
+
return false;
|
|
45
|
+
accessSync(path, constants.X_OK);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Where the installers put the launcher, checked when PATH lacks it: a desktop
|
|
54
|
+
* app started from Finder has a bare PATH, and the login shell a pane runs in
|
|
55
|
+
* never reads ~/.zshrc, where installers usually add ~/.local/bin.
|
|
56
|
+
*/
|
|
57
|
+
function installDirs(id, home, systemDirs) {
|
|
58
|
+
const dirs = [join(home, '.local', 'bin')];
|
|
59
|
+
if (id === 'claude')
|
|
60
|
+
dirs.push(join(home, '.claude', 'local'));
|
|
61
|
+
return [...dirs, ...systemDirs];
|
|
62
|
+
}
|
|
63
|
+
/** The absolute path of the user's `claude` or `codex`, or null when it is not installed. */
|
|
64
|
+
export function findAgentCli(id, lookup = {}) {
|
|
65
|
+
const env = lookup.env ?? process.env;
|
|
66
|
+
const platform = lookup.platform ?? process.platform;
|
|
67
|
+
const home = lookup.home ?? homedir();
|
|
68
|
+
const names = platform === 'win32'
|
|
69
|
+
? [`${AGENT_CLIS[id].binary}.exe`, `${AGENT_CLIS[id].binary}.cmd`]
|
|
70
|
+
: [AGENT_CLIS[id].binary];
|
|
71
|
+
const pathDirs = (env.PATH ?? '').split(platform === 'win32' ? ';' : ':').filter(Boolean);
|
|
72
|
+
for (const dir of [...pathDirs, ...installDirs(id, home, lookup.systemDirs ?? SYSTEM_BIN_DIRS)]) {
|
|
73
|
+
for (const name of names) {
|
|
74
|
+
const candidate = join(dir, name);
|
|
75
|
+
if (isExecutableFile(candidate))
|
|
76
|
+
return candidate;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
// ── Arguments ───────────────────────────────────────────────────────────────
|
|
82
|
+
export class CliArgumentsError extends Error {
|
|
83
|
+
constructor(message) {
|
|
84
|
+
super(message);
|
|
85
|
+
this.name = 'CliArgumentsError';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Split words the way a POSIX shell does, without expanding anything: quotes
|
|
90
|
+
* group, a backslash escapes, and `$`, backticks, globs and `;` stay literal.
|
|
91
|
+
*/
|
|
92
|
+
export function splitCliArguments(raw) {
|
|
93
|
+
const words = [];
|
|
94
|
+
let word = '';
|
|
95
|
+
let inWord = false;
|
|
96
|
+
let quote = null;
|
|
97
|
+
for (let i = 0; i < raw.length; i++) {
|
|
98
|
+
const char = raw[i];
|
|
99
|
+
if (quote === "'") {
|
|
100
|
+
if (char === "'")
|
|
101
|
+
quote = null;
|
|
102
|
+
else
|
|
103
|
+
word += char;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (quote === '"') {
|
|
107
|
+
if (char === '"') {
|
|
108
|
+
quote = null;
|
|
109
|
+
}
|
|
110
|
+
else if (char === '\\' && i + 1 < raw.length && '"\\$`\n'.includes(raw[i + 1])) {
|
|
111
|
+
i++;
|
|
112
|
+
if (raw[i] !== '\n')
|
|
113
|
+
word += raw[i];
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
word += char;
|
|
117
|
+
}
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (char === "'" || char === '"') {
|
|
121
|
+
quote = char;
|
|
122
|
+
inWord = true;
|
|
123
|
+
}
|
|
124
|
+
else if (char === '\\') {
|
|
125
|
+
if (i + 1 >= raw.length) {
|
|
126
|
+
word += char;
|
|
127
|
+
inWord = true;
|
|
128
|
+
}
|
|
129
|
+
else if (raw[i + 1] === '\n') {
|
|
130
|
+
i++; // line continuation
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
word += raw[++i];
|
|
134
|
+
inWord = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (/\s/.test(char)) {
|
|
138
|
+
if (inWord) {
|
|
139
|
+
words.push(word);
|
|
140
|
+
word = '';
|
|
141
|
+
inWord = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
word += char;
|
|
146
|
+
inWord = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (quote) {
|
|
150
|
+
throw new CliArgumentsError(`Unclosed ${quote === '"' ? 'double' : 'single'} quote in the arguments.`);
|
|
151
|
+
}
|
|
152
|
+
if (inWord)
|
|
153
|
+
words.push(word);
|
|
154
|
+
return words;
|
|
155
|
+
}
|
|
156
|
+
/** Words as a shell would accept them back: quoted only where needed. */
|
|
157
|
+
export function formatCliArguments(words) {
|
|
158
|
+
return words
|
|
159
|
+
.map(word => (/^[A-Za-z0-9_@%+=:,./-]+$/.test(word) ? word : `'${word.replace(/'/g, `'\\''`)}'`))
|
|
160
|
+
.join(' ');
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Read the text after `/claude` or `/codex`. Text that starts with a flag is
|
|
164
|
+
* the CLI's own arguments, split like a shell command line (`/claude
|
|
165
|
+
* --continue`, `/codex -m gpt-5 "review this"`). Anything else is the opening
|
|
166
|
+
* prompt, kept exactly as typed, apostrophes and all (`/claude don't touch the
|
|
167
|
+
* migrations`).
|
|
168
|
+
*/
|
|
169
|
+
export function parseAgentCommandBody(body) {
|
|
170
|
+
const text = body.trim();
|
|
171
|
+
if (!text)
|
|
172
|
+
return { words: [] };
|
|
173
|
+
if (text.startsWith('-'))
|
|
174
|
+
return { words: splitCliArguments(text) };
|
|
175
|
+
return { words: [], prompt: text };
|
|
176
|
+
}
|
|
177
|
+
export const WORKSPACE_MCP_SERVER_NAME = 'ztc';
|
|
178
|
+
/**
|
|
179
|
+
* Variables the workspace server reads. Codex starts MCP servers with a short
|
|
180
|
+
* allowlist of variables, so these are named for it to forward; Claude Code
|
|
181
|
+
* passes its whole environment. They reach the pane through its environment,
|
|
182
|
+
* never its command line.
|
|
183
|
+
*/
|
|
184
|
+
export const WORKSPACE_MCP_ENV = Object.freeze([
|
|
185
|
+
'ZTC_PANE_ID',
|
|
186
|
+
'ZTC_PANE_RELAY',
|
|
187
|
+
'ZTC_MIRROR_PORT',
|
|
188
|
+
'ZTC_MIRROR_INTERNAL_TOKEN',
|
|
189
|
+
'ZTC_MIRROR_GROUP',
|
|
190
|
+
'ZTC_NAVIGATOR_GROUP',
|
|
191
|
+
'ZTC_PROFILE',
|
|
192
|
+
]);
|
|
193
|
+
function tomlString(value) {
|
|
194
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/[\u0000-\u001f\u007f]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, '0')}`)}"`;
|
|
195
|
+
}
|
|
196
|
+
function tomlArray(values) {
|
|
197
|
+
return `[${values.map(tomlString).join(',')}]`;
|
|
198
|
+
}
|
|
199
|
+
/** The CLI's own arguments that attach the workspace server, or nothing without one. */
|
|
200
|
+
export function workspaceMcpArgs(id, server) {
|
|
201
|
+
if (!server)
|
|
202
|
+
return [];
|
|
203
|
+
if (id === 'claude') {
|
|
204
|
+
return ['--mcp-config', JSON.stringify({
|
|
205
|
+
mcpServers: { [WORKSPACE_MCP_SERVER_NAME]: { type: 'stdio', command: server.command, args: server.args } },
|
|
206
|
+
})];
|
|
207
|
+
}
|
|
208
|
+
const key = `mcp_servers.${WORKSPACE_MCP_SERVER_NAME}`;
|
|
209
|
+
return [
|
|
210
|
+
'-c', `${key}.command=${tomlString(server.command)}`,
|
|
211
|
+
'-c', `${key}.args=${tomlArray(server.args)}`,
|
|
212
|
+
'-c', `${key}.env_vars=${tomlArray(WORKSPACE_MCP_ENV)}`,
|
|
213
|
+
'-c', `${key}.startup_timeout_sec=30`,
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The CLI's full argument list. Codex takes its overrides first, before any
|
|
218
|
+
* subcommand. Claude Code's `--mcp-config` takes several values, so it goes
|
|
219
|
+
* after the user's flags, ahead of a `--` they typed, and a prompt follows its
|
|
220
|
+
* own `--` so a prompt starting with "-" is never read as a flag.
|
|
221
|
+
*/
|
|
222
|
+
export function agentCliArgs(id, request, server) {
|
|
223
|
+
const attach = workspaceMcpArgs(id, server);
|
|
224
|
+
const prompt = request.prompt?.trim() ? ['--', request.prompt] : [];
|
|
225
|
+
if (id === 'codex')
|
|
226
|
+
return [...attach, ...request.words, ...prompt];
|
|
227
|
+
const separator = request.words.indexOf('--');
|
|
228
|
+
if (separator !== -1) {
|
|
229
|
+
return [...request.words.slice(0, separator), ...attach, ...request.words.slice(separator), ...(request.prompt?.trim() ? [request.prompt] : [])];
|
|
230
|
+
}
|
|
231
|
+
return [...request.words, ...attach, ...prompt];
|
|
232
|
+
}
|
|
233
|
+
const POSIX_SHELLS = new Set(['sh', 'bash', 'zsh', 'dash', 'ksh', 'mksh', 'yash', 'ash']);
|
|
234
|
+
/**
|
|
235
|
+
* Run the binary from a login shell, as custom panes are, so PATH and the rest
|
|
236
|
+
* of the login environment apply. The binary and its arguments travel as
|
|
237
|
+
* separate argv entries (`exec "$0" "$@"`): no shell ever re-parses them, so
|
|
238
|
+
* quotes, `$` and `;` in a prompt arrive exactly as written. A shell outside
|
|
239
|
+
* the POSIX family (fish, nushell), or no shell at all, is replaced by /bin/sh
|
|
240
|
+
* for this one step.
|
|
241
|
+
*/
|
|
242
|
+
export function loginShellExec(binary, args, shell, platform = process.platform) {
|
|
243
|
+
if (platform === 'win32')
|
|
244
|
+
return { command: binary, args: [...args] };
|
|
245
|
+
const login = shell && POSIX_SHELLS.has(basename(shell)) ? shell : '/bin/sh';
|
|
246
|
+
return { command: login, args: ['-lc', 'exec "$0" "$@"', binary, ...args] };
|
|
247
|
+
}
|
|
248
|
+
/** "Claude Code", then "Claude Code 2", "Claude Code 3"… so every pane name stays unique. */
|
|
249
|
+
export function nextPaneTitle(label, existing) {
|
|
250
|
+
const taken = new Set(existing.map(title => title.trim().toLowerCase()));
|
|
251
|
+
if (!taken.has(label.toLowerCase()))
|
|
252
|
+
return label;
|
|
253
|
+
for (let n = 2;; n++) {
|
|
254
|
+
const candidate = `${label} ${n}`;
|
|
255
|
+
if (!taken.has(candidate.toLowerCase()))
|
|
256
|
+
return candidate;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The pane's environment overrides: ZTC's control variables are cleared as for
|
|
261
|
+
* any user program, except what the workspace server needs to reach this
|
|
262
|
+
* workspace. Everything else is the user's own environment, untouched, so the
|
|
263
|
+
* CLI offers every sign-in method it normally would.
|
|
264
|
+
*/
|
|
265
|
+
export function agentPaneEnv(source = process.env) {
|
|
266
|
+
const env = buildUserSubprocessEnvOverrides();
|
|
267
|
+
for (const key of WORKSPACE_MCP_ENV) {
|
|
268
|
+
if (key === 'ZTC_PANE_ID')
|
|
269
|
+
continue; // the renderer sets each pane's own
|
|
270
|
+
const value = source[key];
|
|
271
|
+
if (value)
|
|
272
|
+
env[key] = value;
|
|
273
|
+
}
|
|
274
|
+
return env;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* How to start `ztc workspace-mcp` from the running installation: the desktop
|
|
278
|
+
* app's own launcher, or this Node and entrypoint.
|
|
279
|
+
*/
|
|
280
|
+
export function workspaceMcpProgram(argv = process.argv) {
|
|
281
|
+
const owned = resolveOwnedCliLaunch(['workspace-mcp']);
|
|
282
|
+
if (owned)
|
|
283
|
+
return { command: owned.command, args: owned.args };
|
|
284
|
+
const script = argv[1] || '';
|
|
285
|
+
if (!/(^|[\\/])cli_(main|ratatui)\.(js|ts)$/.test(script)) {
|
|
286
|
+
return new Error('the running ZTC entrypoint is unknown');
|
|
287
|
+
}
|
|
288
|
+
if (script.endsWith('.ts')) {
|
|
289
|
+
const tsx = join(dirname(dirname(script)), 'node_modules', '.bin', process.platform === 'win32' ? 'tsx.cmd' : 'tsx');
|
|
290
|
+
if (!existsSync(tsx))
|
|
291
|
+
return new Error('tsx is not installed next to the ZTC sources');
|
|
292
|
+
return { command: tsx, args: [script, 'workspace-mcp'] };
|
|
293
|
+
}
|
|
294
|
+
return { command: process.execPath, args: [script, 'workspace-mcp'] };
|
|
295
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opening agent CLI panes, and typing messages into them.
|
|
3
|
+
*
|
|
4
|
+
* `/claude`, `/codex`, the Ctrl+B c chooser, the desktop app's new-pane menu
|
|
5
|
+
* and the workspace MCP server all open agent panes here, so every entry point
|
|
6
|
+
* launches the same way. The pane is created through whichever pane transport
|
|
7
|
+
* the process has: the overseer's renderer connection, or a child pane's relay.
|
|
8
|
+
*/
|
|
9
|
+
import type { PaneCommandTransport } from '../ui/ratatui/pane_relay.js';
|
|
10
|
+
import type { SplitDirection } from '../ui/ratatui/protocol.js';
|
|
11
|
+
import { type AgentCliId, type AgentLaunchRequest, type PaneProgram } from './agent_cli.js';
|
|
12
|
+
/** Request ids of agent pane launches start with this, so the overseer can tell them apart. */
|
|
13
|
+
export declare const AGENT_PANE_REQUEST_PREFIX = "agent-pane:";
|
|
14
|
+
/** Which agent CLI a `create_pane` request id launched, if any. */
|
|
15
|
+
export declare function agentCliFromRequestId(requestId: string | undefined): AgentCliId | null;
|
|
16
|
+
export interface OpenAgentPaneOptions {
|
|
17
|
+
cli: AgentCliId;
|
|
18
|
+
request?: AgentLaunchRequest;
|
|
19
|
+
cwd?: string;
|
|
20
|
+
/** Pane name; defaults to the next free "Claude Code"/"Codex" name. */
|
|
21
|
+
title?: string;
|
|
22
|
+
direction?: SplitDirection;
|
|
23
|
+
ratio?: number;
|
|
24
|
+
target?: number;
|
|
25
|
+
newWindow?: boolean;
|
|
26
|
+
findBinary?: (id: AgentCliId) => string | null;
|
|
27
|
+
/** The workspace server to attach; an Error opens the pane without it. */
|
|
28
|
+
server?: PaneProgram | Error | null;
|
|
29
|
+
/** Source of SHELL and the workspace variables (default: this process). */
|
|
30
|
+
env?: NodeJS.ProcessEnv;
|
|
31
|
+
}
|
|
32
|
+
export interface OpenAgentPaneResult {
|
|
33
|
+
paneId?: number;
|
|
34
|
+
title?: string;
|
|
35
|
+
cwd?: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
/** Set when the pane opened but something about it should be said. */
|
|
38
|
+
notice?: string;
|
|
39
|
+
}
|
|
40
|
+
/** Open the user's own `claude` or `codex` in a new pane, attached to this workspace. */
|
|
41
|
+
export declare function openAgentPane(transport: PaneCommandTransport | null, options: OpenAgentPaneOptions): Promise<OpenAgentPaneResult>;
|
|
42
|
+
interface PaneMessage {
|
|
43
|
+
from: {
|
|
44
|
+
id: number;
|
|
45
|
+
name: string;
|
|
46
|
+
};
|
|
47
|
+
text: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A bus message as it is typed into an agent pane: one line, saying who sent
|
|
51
|
+
* it and, for a question or request, how to answer.
|
|
52
|
+
*/
|
|
53
|
+
export declare function formatAgentPaneMessage(message: PaneMessage): string;
|
|
54
|
+
/** Delay between typed text and its Enter for programs that read a burst of input as a paste. */
|
|
55
|
+
export declare const TYPED_ENTER_DELAY_MS = 120;
|
|
56
|
+
/**
|
|
57
|
+
* Type a line into a program that is not ZTC (an agent CLI or a shell), then
|
|
58
|
+
* press Enter as a separate keystroke.
|
|
59
|
+
*/
|
|
60
|
+
export declare function typeLineIntoPane(transport: PaneCommandTransport, paneId: number, line: string): boolean;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opening agent CLI panes, and typing messages into them.
|
|
3
|
+
*
|
|
4
|
+
* `/claude`, `/codex`, the Ctrl+B c chooser, the desktop app's new-pane menu
|
|
5
|
+
* and the workspace MCP server all open agent panes here, so every entry point
|
|
6
|
+
* launches the same way. The pane is created through whichever pane transport
|
|
7
|
+
* the process has: the overseer's renderer connection, or a child pane's relay.
|
|
8
|
+
*/
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
10
|
+
import { listWorkspacePanes, waitForPaneCreatedOrError } from '../agent/tools/pane.js';
|
|
11
|
+
import { AGENT_CLIS, agentCliArgs, agentPaneEnv, findAgentCli, isAgentCliId, loginShellExec, nextPaneTitle, workspaceMcpProgram, } from './agent_cli.js';
|
|
12
|
+
import { sessionNamePaneTitle } from './labels.js';
|
|
13
|
+
/** Request ids of agent pane launches start with this, so the overseer can tell them apart. */
|
|
14
|
+
export const AGENT_PANE_REQUEST_PREFIX = 'agent-pane:';
|
|
15
|
+
/** Which agent CLI a `create_pane` request id launched, if any. */
|
|
16
|
+
export function agentCliFromRequestId(requestId) {
|
|
17
|
+
if (!requestId?.startsWith(AGENT_PANE_REQUEST_PREFIX))
|
|
18
|
+
return null;
|
|
19
|
+
const id = requestId.slice(AGENT_PANE_REQUEST_PREFIX.length).split(':')[0] ?? '';
|
|
20
|
+
return isAgentCliId(id) ? id : null;
|
|
21
|
+
}
|
|
22
|
+
const NEEDS_PANES = 'needs ZTC\'s panes: start ZTC with zmux (plain `ztc`) or use the desktop app';
|
|
23
|
+
async function existingTitles(transport) {
|
|
24
|
+
const panes = await listWorkspacePanes(transport, 3000);
|
|
25
|
+
return panes instanceof Error ? [] : panes.map(pane => pane.labels?.manual_title || pane.title);
|
|
26
|
+
}
|
|
27
|
+
/** Open the user's own `claude` or `codex` in a new pane, attached to this workspace. */
|
|
28
|
+
export async function openAgentPane(transport, options) {
|
|
29
|
+
const spec = AGENT_CLIS[options.cli];
|
|
30
|
+
if (!transport || !transport.isMuxMode)
|
|
31
|
+
return { error: `${spec.label} opens in a pane, which ${NEEDS_PANES}.` };
|
|
32
|
+
const binary = (options.findBinary ?? findAgentCli)(options.cli);
|
|
33
|
+
if (!binary) {
|
|
34
|
+
return { error: `${spec.label} is not installed. Install it from ${spec.setupUrl}, for example with \`${spec.install}\`, then try again.` };
|
|
35
|
+
}
|
|
36
|
+
const env = options.env ?? process.env;
|
|
37
|
+
const server = options.server === undefined ? workspaceMcpProgram() : options.server;
|
|
38
|
+
const attached = server && !(server instanceof Error) ? server : null;
|
|
39
|
+
const title = options.title?.trim() || nextPaneTitle(spec.label, await existingTitles(transport));
|
|
40
|
+
const program = loginShellExec(binary, agentCliArgs(options.cli, options.request ?? { words: [] }, attached), env.SHELL);
|
|
41
|
+
const requestId = `${AGENT_PANE_REQUEST_PREFIX}${options.cli}:${randomUUID()}`;
|
|
42
|
+
const created = waitForPaneCreatedOrError(transport, { requestId }, 15000);
|
|
43
|
+
const sent = transport.send({
|
|
44
|
+
type: 'create_pane',
|
|
45
|
+
request_id: requestId,
|
|
46
|
+
command: program.command,
|
|
47
|
+
args: program.args,
|
|
48
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
49
|
+
title,
|
|
50
|
+
pane_type: 'command',
|
|
51
|
+
...(options.direction ? { direction: options.direction } : {}),
|
|
52
|
+
...(options.ratio !== undefined ? { ratio: options.ratio } : {}),
|
|
53
|
+
...(options.target !== undefined ? { target: options.target } : {}),
|
|
54
|
+
...(options.newWindow ? { new_window: true } : {}),
|
|
55
|
+
env: agentPaneEnv(env),
|
|
56
|
+
});
|
|
57
|
+
if (!sent) {
|
|
58
|
+
created.catch(() => { });
|
|
59
|
+
return { error: `Could not open ${spec.label}: the renderer is not connected.` };
|
|
60
|
+
}
|
|
61
|
+
let event;
|
|
62
|
+
try {
|
|
63
|
+
event = await created;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return { error: `Could not open ${spec.label}: no pane appeared (${error.message}).` };
|
|
67
|
+
}
|
|
68
|
+
if (event.type === 'error')
|
|
69
|
+
return { error: `Could not open ${spec.label}: the renderer refused the pane (${event.message}).` };
|
|
70
|
+
if (event.type !== 'pane_created')
|
|
71
|
+
return { error: `Could not open ${spec.label}: unexpected response from the renderer.` };
|
|
72
|
+
// The CLI sets its own terminal title as it works; the pane keeps its name.
|
|
73
|
+
transport.send(sessionNamePaneTitle(event.pane_id, title));
|
|
74
|
+
const result = { paneId: event.pane_id, title };
|
|
75
|
+
if (options.cwd)
|
|
76
|
+
result.cwd = options.cwd;
|
|
77
|
+
if (!attached) {
|
|
78
|
+
const why = server instanceof Error ? server.message : 'none was given';
|
|
79
|
+
result.notice = `${title} opened without ZTC's workspace tools (${why}), so it cannot open or message other panes.`;
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A bus message as it is typed into an agent pane: one line, saying who sent
|
|
85
|
+
* it and, for a question or request, how to answer.
|
|
86
|
+
*/
|
|
87
|
+
export function formatAgentPaneMessage(message) {
|
|
88
|
+
let text = message.text;
|
|
89
|
+
let kind = 'Message';
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(text);
|
|
92
|
+
if (parsed && parsed.__ztc_request === true && typeof parsed.text === 'string') {
|
|
93
|
+
text = parsed.text;
|
|
94
|
+
kind = parsed.kind === 'request' ? 'Request' : 'Question';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Plain text.
|
|
99
|
+
}
|
|
100
|
+
const line = text.replace(/\s*[\r\n]+\s*/g, ' ').trim();
|
|
101
|
+
const who = `${message.from.name} (pane ${message.from.id})`;
|
|
102
|
+
return kind === 'Message'
|
|
103
|
+
? `[Message from ${who}] ${line}`
|
|
104
|
+
: `[${kind} from ${who} — answer with the ztc send_message tool, to: ${message.from.id}] ${line}`;
|
|
105
|
+
}
|
|
106
|
+
/** Delay between typed text and its Enter for programs that read a burst of input as a paste. */
|
|
107
|
+
export const TYPED_ENTER_DELAY_MS = 120;
|
|
108
|
+
/**
|
|
109
|
+
* Type a line into a program that is not ZTC (an agent CLI or a shell), then
|
|
110
|
+
* press Enter as a separate keystroke.
|
|
111
|
+
*/
|
|
112
|
+
export function typeLineIntoPane(transport, paneId, line) {
|
|
113
|
+
if (!transport.send({ type: 'write_pane', pane_id: paneId, input: line, submit: false }))
|
|
114
|
+
return false;
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
transport.send({ type: 'write_pane', pane_id: paneId, input: '\r', submit: false });
|
|
117
|
+
}, TYPED_ENTER_DELAY_MS);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rows of the new-pane chooser (Ctrl+B c): a shell, a ZTC agent, the
|
|
3
|
+
* agent CLIs the user has installed, then their own custom options.
|
|
4
|
+
*/
|
|
5
|
+
import type { CustomPaneOption } from '../config/types.js';
|
|
6
|
+
import { type AgentCliId } from './agent_cli.js';
|
|
7
|
+
export interface NewPaneChoice {
|
|
8
|
+
value: string;
|
|
9
|
+
label: string;
|
|
10
|
+
description: string;
|
|
11
|
+
}
|
|
12
|
+
/** `agent:claude` and `agent:codex` name the agent CLI rows. */
|
|
13
|
+
export declare function agentCliFromChoice(value: string): AgentCliId | null;
|
|
14
|
+
export declare function newPaneChoices(custom: readonly CustomPaneOption[], installed: Partial<Record<AgentCliId, string | null>>): NewPaneChoice[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rows of the new-pane chooser (Ctrl+B c): a shell, a ZTC agent, the
|
|
3
|
+
* agent CLIs the user has installed, then their own custom options.
|
|
4
|
+
*/
|
|
5
|
+
import { basename } from 'node:path';
|
|
6
|
+
import { AGENT_CLIS, AGENT_CLI_IDS } from './agent_cli.js';
|
|
7
|
+
/** `agent:claude` and `agent:codex` name the agent CLI rows. */
|
|
8
|
+
export function agentCliFromChoice(value) {
|
|
9
|
+
const id = value.startsWith('agent:') ? value.slice('agent:'.length) : '';
|
|
10
|
+
return AGENT_CLI_IDS.includes(id) ? id : null;
|
|
11
|
+
}
|
|
12
|
+
function runsBinary(command, binary) {
|
|
13
|
+
const first = command.trim().split(/\s+/)[0] ?? '';
|
|
14
|
+
return basename(first) === binary;
|
|
15
|
+
}
|
|
16
|
+
export function newPaneChoices(custom, installed) {
|
|
17
|
+
const choices = [
|
|
18
|
+
{ value: 'bash', label: 'bash', description: 'A shell pane' },
|
|
19
|
+
{ value: 'ztc', label: 'ZTC', description: 'A ZTC agent pane' },
|
|
20
|
+
];
|
|
21
|
+
for (const id of AGENT_CLI_IDS) {
|
|
22
|
+
const spec = AGENT_CLIS[id];
|
|
23
|
+
// Not installed, or already offered by the user's own custom option.
|
|
24
|
+
if (!installed[id] || custom.some(option => runsBinary(option.command, spec.binary)))
|
|
25
|
+
continue;
|
|
26
|
+
choices.push({
|
|
27
|
+
value: `agent:${id}`,
|
|
28
|
+
label: spec.label,
|
|
29
|
+
description: `${spec.label} on your own account, with ZTC's pane and message tools`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
custom.forEach((option, index) => {
|
|
33
|
+
choices.push({ value: `custom:${index}`, label: option.label, description: option.command });
|
|
34
|
+
});
|
|
35
|
+
return choices;
|
|
36
|
+
}
|
|
@@ -51,6 +51,8 @@ export interface PaneLabels {
|
|
|
51
51
|
group?: string;
|
|
52
52
|
pinned?: boolean;
|
|
53
53
|
}
|
|
54
|
+
/** What a renderer's new-pane menu asks the owner to open; absent means a ZTC pane. */
|
|
55
|
+
export type PaneRequestKind = 'ztc' | 'shell' | 'claude_code' | 'codex';
|
|
54
56
|
export interface PaneInfo {
|
|
55
57
|
id: number;
|
|
56
58
|
title: string;
|
|
@@ -533,6 +535,7 @@ export type RustEvent = DesktopTaskAction | DesktopRuntimeAction | DesktopSessio
|
|
|
533
535
|
target?: number;
|
|
534
536
|
new_window?: boolean;
|
|
535
537
|
cwd?: string;
|
|
538
|
+
kind?: PaneRequestKind;
|
|
536
539
|
} | {
|
|
537
540
|
type: 'pane_created';
|
|
538
541
|
pane_id: number;
|
|
@@ -418,6 +418,7 @@ export declare class MirrorStreamServer implements MirrorPublisher {
|
|
|
418
418
|
private multiPaneMode;
|
|
419
419
|
private paneInputCallback;
|
|
420
420
|
private paneControlCallback;
|
|
421
|
+
private agentPaneMessageCallback;
|
|
421
422
|
private muxCommandCallback;
|
|
422
423
|
private supervisionCommandCallback;
|
|
423
424
|
private paneViewportCallback;
|
|
@@ -641,6 +642,12 @@ export declare class MirrorStreamServer implements MirrorPublisher {
|
|
|
641
642
|
*/
|
|
642
643
|
onPaneInput(cb: (paneId: number, input: MessageInput) => void): void;
|
|
643
644
|
onPaneControl(cb: (paneId: number, controlLine: string) => void): void;
|
|
645
|
+
/**
|
|
646
|
+
* Claim messages for panes that run another agent CLI (Claude Code, Codex).
|
|
647
|
+
* The callback returns true when it delivered the message (the owner types
|
|
648
|
+
* it into the pane with its sender); false leaves the usual fallbacks.
|
|
649
|
+
*/
|
|
650
|
+
onAgentPaneMessage(cb: (paneId: number, msg: BusMessage) => boolean): void;
|
|
644
651
|
onMuxCommand(cb: (command: MirrorMuxCommand) => Promise<MirrorMuxCommandResult>): void;
|
|
645
652
|
onSupervisionCommand(cb: (command: MirrorSupervisionCommand) => Promise<MirrorSupervisionCommandResult>): void;
|
|
646
653
|
onPaneViewport(cb: (paneId: number, ansi?: boolean) => Promise<MirrorPaneViewportResult>): void;
|
|
@@ -167,6 +167,7 @@ export class MirrorStreamServer {
|
|
|
167
167
|
// rather than replaying it to the LLM. Kept separate from paneInputCallback (the
|
|
168
168
|
// user-prompt channel) so the two can never be confused by content.
|
|
169
169
|
paneControlCallback = null;
|
|
170
|
+
agentPaneMessageCallback = null;
|
|
170
171
|
muxCommandCallback = null;
|
|
171
172
|
supervisionCommandCallback = null;
|
|
172
173
|
paneViewportCallback = null;
|
|
@@ -558,6 +559,14 @@ export class MirrorStreamServer {
|
|
|
558
559
|
onPaneControl(cb) {
|
|
559
560
|
this.paneControlCallback = cb;
|
|
560
561
|
}
|
|
562
|
+
/**
|
|
563
|
+
* Claim messages for panes that run another agent CLI (Claude Code, Codex).
|
|
564
|
+
* The callback returns true when it delivered the message (the owner types
|
|
565
|
+
* it into the pane with its sender); false leaves the usual fallbacks.
|
|
566
|
+
*/
|
|
567
|
+
onAgentPaneMessage(cb) {
|
|
568
|
+
this.agentPaneMessageCallback = cb;
|
|
569
|
+
}
|
|
561
570
|
onMuxCommand(cb) {
|
|
562
571
|
this.muxCommandCallback = cb;
|
|
563
572
|
}
|
|
@@ -724,6 +733,14 @@ export class MirrorStreamServer {
|
|
|
724
733
|
return;
|
|
725
734
|
}
|
|
726
735
|
}
|
|
736
|
+
// A pane running Claude Code or Codex has no bus connection: the owner
|
|
737
|
+
// types an agent's message into it, saying who sent it.
|
|
738
|
+
if (typeof targetId === 'number' && targetId !== 0 && msg.type === 'ztc_message'
|
|
739
|
+
&& this.agentPaneMessageCallback?.(targetId, msg)) {
|
|
740
|
+
if (process.env.ZTC_DEBUG)
|
|
741
|
+
process.stderr.write(`${tag} delivered to an agent CLI pane\n`);
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
727
744
|
// Headless ZTC pane children read input from their own stdin — they have no
|
|
728
745
|
// bus callback or SSE connection.
|
|
729
746
|
// A ztc_message is an internal control command ({"__ztc_cmd":...}), NOT user
|
package/dist/zlm-embed.mjs
CHANGED
|
@@ -78502,7 +78502,7 @@ var invokeSkillTool = makeInvokeSkillTool(resolveAmbientSkills);
|
|
|
78502
78502
|
// src/zlm.ts
|
|
78503
78503
|
var ZLM_EMBED_API_VERSION = "zerglang.ztc-embed/1";
|
|
78504
78504
|
var ZLM_STDIO_PROTOCOL = "zerglang.ztc-host/1";
|
|
78505
|
-
var ZLM_ZTC_VERSION = true ? "0.
|
|
78505
|
+
var ZLM_ZTC_VERSION = true ? "0.10.0-beta.1" : "development";
|
|
78506
78506
|
var ZLM_MAX_HOST_TOOLS = 64;
|
|
78507
78507
|
var ZLM_MAX_SYSTEM_PROMPT_BYTES = 128 * 1024;
|
|
78508
78508
|
var ZLM_HOST_APPROVAL_CAPABILITIES = Object.freeze([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zergai/ztc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0-beta.1",
|
|
4
4
|
"description": "Zerg Terminal Client - CLI agent for continual AI systems",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"homepage": "https://zergai.com/docs/ztc",
|
|
@@ -277,7 +277,7 @@
|
|
|
277
277
|
"zergRelease": {
|
|
278
278
|
"branch": "main",
|
|
279
279
|
"channel": "next",
|
|
280
|
-
"sourceCommit": "
|
|
280
|
+
"sourceCommit": "f987bf5a7f5b3b8c9b6ed6e5ace077242bdb2be6",
|
|
281
281
|
"sourceVersion": "0.2.0-canary.2"
|
|
282
282
|
},
|
|
283
283
|
"publishConfig": {
|