@zergai/ztc 0.9.1-dev.202611052132 → 0.10.1-dev.202611052216

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 CHANGED
@@ -1357,6 +1357,34 @@ Codex pane is typed into it as `[Message from ben (pane 2)] …` (a question als
1357
1357
  says how to answer), and the answer comes back as a message from that pane.
1358
1358
  Broadcasts to `all` reach ZTC agents only.
1359
1359
 
1360
+ #### Sign-in profiles of ZTC's own
1361
+
1362
+ `/claude @work …` and `/codex @work …` open the pane on a ZTC profile named
1363
+ `work`: a config directory of its own under `~/.ztc/agent-profiles/<cli>/`,
1364
+ handed to the CLI through its documented variable (`CLAUDE_CONFIG_DIR`,
1365
+ `CODEX_HOME`). The first time, the CLI asks you to sign in, with its own login;
1366
+ it keeps that sign-in in the profile, so the CLI's sign-in in iTerm or Ghostty
1367
+ doesn't change. Codex panes on a profile keep their sign-in in the profile's
1368
+ files (`cli_auth_credentials_store="file"`). `/subs` lists your profiles, and
1369
+ `/subs use claude|codex [profile]` sets the one new panes start on (no profile:
1370
+ the CLI's own sign-in). ZTC never reads what a CLI stores in a profile and
1371
+ never moves a pane to another profile on its own. A profile starts without your
1372
+ usual CLI settings, MCP servers or plugins, since those live in the CLI's
1373
+ default config directory.
1374
+
1375
+ #### In Zerg Router's Command Central
1376
+
1377
+ With MotSi installed and its remote view on (`motsi remote view on`), ZTC
1378
+ starts the panes it opens — ZTC agents, Claude Code, Codex, shells and custom
1379
+ panes — through `motsi run --title=<pane name> --`. Zerg Router's Command
1380
+ Central then lists each pane under its name and shows it live as a real
1381
+ terminal. MotSi redacts likely secrets on this computer before anything leaves
1382
+ it, and lets a viewer type only into an agent (Claude Code, Codex or ZTC),
1383
+ never into a shell. Nothing leaves the computer while MotSi's remote view is
1384
+ off. To start panes without MotSi, set `panes.remoteView` to `false` in ZTC's
1385
+ config. The pane ZTC itself starts in is not one ZTC opens; to share it, start
1386
+ ZTC as `motsi run -- ztc`.
1387
+
1360
1388
  ZTC's own agent loop uses an Anthropic API key for Claude models (`/config key`
1361
1389
  or `ANTHROPIC_API_KEY`). Anthropic's [legal and compliance
1362
1390
  terms](https://code.claude.com/docs/en/legal-and-compliance) reserve Claude
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -15,6 +15,8 @@ export interface AgentCliCommandDeps {
15
15
  findBinary?: (id: AgentCliId) => string | null;
16
16
  server?: () => PaneProgram | Error;
17
17
  env?: () => NodeJS.ProcessEnv;
18
+ /** `motsi` to start the pane through; default: MotSi's remote view (see panes/motsi.ts). */
19
+ motsi?: () => string | null;
18
20
  }
19
21
  export declare function createAgentCliCommand(id: AgentCliId, deps?: AgentCliCommandDeps): Command;
20
22
  export declare const claudeCommand: Command;
@@ -48,6 +48,7 @@ export function createAgentCliCommand(id, deps = {}) {
48
48
  findBinary: deps.findBinary ?? findAgentCli,
49
49
  server: (deps.server ?? workspaceMcpProgram)(),
50
50
  env: (deps.env ?? (() => process.env))(),
51
+ ...(deps.motsi ? { motsi: deps.motsi() } : {}),
51
52
  });
52
53
  if (result.error) {
53
54
  ctx.addMessage({ role: 'system', content: result.error });
@@ -31,6 +31,7 @@ import { reloadCommand, restartCommand } from './restart.js';
31
31
  import { initCommand } from './init.js';
32
32
  import { forkCommand, sideCommand } from './fork.js';
33
33
  import { claudeCommand, codexCommand } from './agent_cli.js';
34
+ import { subsCommand } from './subs.js';
34
35
  import { diffCommand } from './diff.js';
35
36
  import { rawCommand } from './raw.js';
36
37
  import { themeCommand } from './theme.js';
@@ -83,7 +84,7 @@ import { effortCommand, thinkingCommand, reasoningCommand, fastCommand } from '.
83
84
  import { runtimePluginRegistry } from '../../plugin/runtime_registry.js';
84
85
  const commandList = [];
85
86
  const helpCommand = makeHelpCommand(() => commandList);
86
- commandList.push(helpCommand, authCommand, loginCommand, logoutCommand, accountsCommand, zergCommand, zergsCommand, configCommand, debugCommand, clearCommand, handoffCommand, statusCommand, runtimeCommand, environmentCommand, jobCommand, shareCommand, hooksCommand, emulationCommand, shellCommand, cdCommand, clipboardCommand, modelsCommand, modelCommand, permissionsCommand, resourcesCommand, initCommand, forkCommand, sideCommand, claudeCommand, codexCommand, diffCommand, rawCommand, themeCommand, doctorCommand, intentCommand, profileCommand, skillsCommand, runbooksCommand, keybindingsCommand, keymapCommand, shortcutsCommand, updateCommand, desktopCommand, layoutCommand, setupCommand, connectCommand, keysCommand, restartCommand, reloadCommand, inputModeCommand, retryCommand, dictationStatusCommand, embeddedCommand, benchCommand, voiceCommand, sfxCommand, compactCommand, automodeCommand, sessionsCommand, resumeCommand, rewindCommand, renameCommand, labelCommand, inboxCommand, attentionCommand, contextCommand, evidenceCommand, boardCommand, usageCommand, shannonRunnerCommand, planCommand, executeCommand, modeCommand, modeListCommand, exitModeCommand, validateCommand, previewCommand, saveCommand, dcaigCommand, manifestCommand, swarmCommand, swarmStopCommand, swarmStatusCommand, superviseCommand, superviseStopCommand, superviseStatusCommand, zlickyCommand, zlickyStopCommand, zlickyStatusCommand, zlickyActCommand, zlickyPresenceCommand, zlickyPttCommand, simCommand, simStopCommand, simStatusCommand, simRunCommand, simCheckpointCommand, simResumeCommand, evolveCommand, evolveStopCommand, evolveStatusCommand, evolveBestCommand, optimizeCommand, batchCommand, batchStatusCommand, batchStopCommand, workflowsCommand, mirrorCommand, settingsCommand, mcpCommand, loopCommand, goalCommand, effortCommand, thinkingCommand, reasoningCommand, fastCommand, exitCommand);
87
+ commandList.push(helpCommand, authCommand, loginCommand, logoutCommand, accountsCommand, zergCommand, zergsCommand, configCommand, debugCommand, clearCommand, handoffCommand, statusCommand, runtimeCommand, environmentCommand, jobCommand, shareCommand, hooksCommand, emulationCommand, shellCommand, cdCommand, clipboardCommand, modelsCommand, modelCommand, permissionsCommand, resourcesCommand, initCommand, forkCommand, sideCommand, claudeCommand, codexCommand, subsCommand, diffCommand, rawCommand, themeCommand, doctorCommand, intentCommand, profileCommand, skillsCommand, runbooksCommand, keybindingsCommand, keymapCommand, shortcutsCommand, updateCommand, desktopCommand, layoutCommand, setupCommand, connectCommand, keysCommand, restartCommand, reloadCommand, inputModeCommand, retryCommand, dictationStatusCommand, embeddedCommand, benchCommand, voiceCommand, sfxCommand, compactCommand, automodeCommand, sessionsCommand, resumeCommand, rewindCommand, renameCommand, labelCommand, inboxCommand, attentionCommand, contextCommand, evidenceCommand, boardCommand, usageCommand, shannonRunnerCommand, planCommand, executeCommand, modeCommand, modeListCommand, exitModeCommand, validateCommand, previewCommand, saveCommand, dcaigCommand, manifestCommand, swarmCommand, swarmStopCommand, swarmStatusCommand, superviseCommand, superviseStopCommand, superviseStatusCommand, zlickyCommand, zlickyStopCommand, zlickyStatusCommand, zlickyActCommand, zlickyPresenceCommand, zlickyPttCommand, simCommand, simStopCommand, simStatusCommand, simRunCommand, simCheckpointCommand, simResumeCommand, evolveCommand, evolveStopCommand, evolveStatusCommand, evolveBestCommand, optimizeCommand, batchCommand, batchStatusCommand, batchStopCommand, workflowsCommand, mirrorCommand, settingsCommand, mcpCommand, loopCommand, goalCommand, effortCommand, thinkingCommand, reasoningCommand, fastCommand, exitCommand);
87
88
  export const commands = commandList;
88
89
  export function refreshPluginCommands() {
89
90
  const existing = new Set(commandList.map(command => command.name));
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `/subs` — ZTC's sign-in profiles for Claude Code and Codex panes, and which
3
+ * one new panes start on.
4
+ *
5
+ * A profile keeps the CLI's own sign-in in a directory of ZTC's (see
6
+ * panes/agent_profiles.ts), so choosing one here never changes the CLI's
7
+ * sign-in in iTerm or Ghostty. Nothing switches on its own: the user picks.
8
+ */
9
+ import type { Command } from './types.js';
10
+ export interface SubsCommandDeps {
11
+ home?: () => string;
12
+ }
13
+ export declare function createSubsCommand(deps?: SubsCommandDeps): Command;
14
+ export declare const subsCommand: Command;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `/subs` — ZTC's sign-in profiles for Claude Code and Codex panes, and which
3
+ * one new panes start on.
4
+ *
5
+ * A profile keeps the CLI's own sign-in in a directory of ZTC's (see
6
+ * panes/agent_profiles.ts), so choosing one here never changes the CLI's
7
+ * sign-in in iTerm or Ghostty. Nothing switches on its own: the user picks.
8
+ */
9
+ import { homedir } from 'node:os';
10
+ import { AGENT_CLIS } from '../../panes/agent_cli.js';
11
+ import { ensureAgentProfile, isProfileName, listAgentProfiles } from '../../panes/agent_profiles.js';
12
+ const USAGE = 'Usage: /subs · /subs use claude|codex [profile] (no profile: the CLI\'s own sign-in)';
13
+ export function createSubsCommand(deps = {}) {
14
+ return {
15
+ name: 'subs',
16
+ description: 'Show and choose the sign-in profiles Claude Code and Codex panes start on',
17
+ usage: '[use <claude|codex> [profile]]',
18
+ handler: async (args, ctx) => {
19
+ const home = (deps.home ?? homedir)();
20
+ const panes = ctx.config.get().panes ?? {};
21
+ const chosen = panes.agentProfiles ?? {};
22
+ if (args.length === 0) {
23
+ const lines = ['claude', 'codex'].map(cli => {
24
+ const label = AGENT_CLIS[cli].label;
25
+ const profiles = listAgentProfiles(cli, home);
26
+ const using = chosen[cli] ? `the "${chosen[cli]}" profile` : `${label}'s own sign-in`;
27
+ const known = profiles.length > 0 ? `Profiles: ${profiles.join(', ')}.` : 'No ZTC profiles yet.';
28
+ return `${label}: new panes use ${using}. ${known}`;
29
+ });
30
+ lines.push('Open one on a profile with /claude @<profile> or /codex @<profile>, and sign in there the first time; '
31
+ + 'the CLI keeps that sign-in in ZTC\'s profile, so iTerm and Ghostty keep theirs. /subs use claude|codex [profile] sets the default.');
32
+ ctx.addMessage({ role: 'system', content: lines.join('\n') });
33
+ return;
34
+ }
35
+ const cli = args[1];
36
+ if (args[0] !== 'use' || (cli !== 'claude' && cli !== 'codex') || args.length > 3) {
37
+ ctx.addMessage({ role: 'system', content: USAGE });
38
+ return;
39
+ }
40
+ const label = AGENT_CLIS[cli].label;
41
+ const name = args[2];
42
+ const next = { ...chosen };
43
+ if (name) {
44
+ if (!isProfileName(name)) {
45
+ ctx.addMessage({ role: 'system', content: `"${name}" is not a profile name: use letters, digits, ".", "_" or "-", up to 32.` });
46
+ return;
47
+ }
48
+ ensureAgentProfile(cli, name, home);
49
+ next[cli] = name;
50
+ }
51
+ else {
52
+ delete next[cli];
53
+ }
54
+ ctx.config.set('panes', { ...panes, agentProfiles: next });
55
+ ctx.addMessage({
56
+ role: 'system',
57
+ content: name
58
+ ? `New ${label} panes use the "${name}" profile. Sign in there the first time one opens; ${label}'s sign-in in other terminals doesn't change.`
59
+ : `New ${label} panes use ${label}'s own sign-in.`,
60
+ });
61
+ },
62
+ };
63
+ }
64
+ export const subsCommand = createSubsCommand();
@@ -1300,6 +1300,12 @@ export declare class RatatuiApp {
1300
1300
  private spawnAgentCliPane;
1301
1301
  /** Where the user's own `claude` and `codex` are installed, if they are. */
1302
1302
  private installedAgentClis;
1303
+ /**
1304
+ * The `motsi` that panes start through so Zerg Router's Command Central can
1305
+ * show them, or null: MotSi installed, its remote view on, and
1306
+ * `panes.remoteView` not false (see panes/motsi.ts).
1307
+ */
1308
+ private paneMotsi;
1303
1309
  /**
1304
1310
  * Create a ZTC agent pane on the user's behalf: no agent turn, no tool call.
1305
1311
  *
@@ -8,6 +8,7 @@ import { waitForPaneCreatedOrError } from './agent/tools/pane.js';
8
8
  import { findAgentCli } from './panes/agent_cli.js';
9
9
  import { agentCliFromRequestId, formatAgentPaneMessage, openAgentPane, typeLineIntoPane } from './panes/agent_panes.js';
10
10
  import { agentCliFromChoice, newPaneChoices } from './panes/new_pane_choices.js';
11
+ import { MOTSI_PANE_ENV, remoteViewMotsi, throughMotsi } from './panes/motsi.js';
11
12
  import { IntakeNotifications } from './intake/notifications.js';
12
13
  import { projectGoal } from './goal_contract.js';
13
14
  import { isToolResultMessage } from './utils/tool_result_message.js';
@@ -13037,6 +13038,7 @@ export class RatatuiApp {
13037
13038
  ...options,
13038
13039
  cwd: options.cwd || this.shellCwd || process.cwd(),
13039
13040
  findBinary: id => this.installedAgentClis()[id] ?? null,
13041
+ motsi: this.paneMotsi(),
13040
13042
  });
13041
13043
  if (result.notice)
13042
13044
  this.addSystemMessage(result.notice);
@@ -13046,6 +13048,14 @@ export class RatatuiApp {
13046
13048
  installedAgentClis() {
13047
13049
  return { claude: findAgentCli('claude'), codex: findAgentCli('codex') };
13048
13050
  }
13051
+ /**
13052
+ * The `motsi` that panes start through so Zerg Router's Command Central can
13053
+ * show them, or null: MotSi installed, its remote view on, and
13054
+ * `panes.remoteView` not false (see panes/motsi.ts).
13055
+ */
13056
+ paneMotsi() {
13057
+ return remoteViewMotsi(configStore.get().panes);
13058
+ }
13049
13059
  /**
13050
13060
  * Create a ZTC agent pane on the user's behalf: no agent turn, no tool call.
13051
13061
  *
@@ -13096,11 +13106,13 @@ export class RatatuiApp {
13096
13106
  // leaving the user waiting out the timeout on a failed launch.
13097
13107
  (event) => event.type === 'error'
13098
13108
  || (event.type === 'pane_created' && event.request_id === requestId), 15000);
13109
+ const motsi = this.paneMotsi();
13110
+ const launched = motsi ? throughMotsi(motsi, title, child) : child;
13099
13111
  const sent = this.tui.send({
13100
13112
  type: 'create_pane',
13101
13113
  request_id: requestId,
13102
- command: child.command,
13103
- args: child.args,
13114
+ command: launched.command,
13115
+ args: launched.args,
13104
13116
  cwd: child.cwd,
13105
13117
  title,
13106
13118
  pane_type: 'ztc',
@@ -13108,7 +13120,11 @@ export class RatatuiApp {
13108
13120
  ratio: options?.ratio ?? 0.5,
13109
13121
  target: options?.target,
13110
13122
  new_window: options?.newWindow,
13111
- env: { ...this.childZtcPaneEnv(), ...(captured ? { ZTC_INITIAL_PANE_INPUT: captured.path } : {}) },
13123
+ env: {
13124
+ ...this.childZtcPaneEnv(),
13125
+ ...(captured ? { ZTC_INITIAL_PANE_INPUT: captured.path } : {}),
13126
+ ...(motsi ? MOTSI_PANE_ENV : {}),
13127
+ },
13112
13128
  });
13113
13129
  if (!sent) {
13114
13130
  created.catch(() => { });
@@ -13204,10 +13220,12 @@ export class RatatuiApp {
13204
13220
  const requestId = `create-shell-${generateSessionId()}`;
13205
13221
  const cwd = options?.cwd || this.shellCwd || process.cwd();
13206
13222
  const title = options?.title || 'Shell';
13223
+ const motsi = this.paneMotsi();
13224
+ const launched = motsi ? throughMotsi(motsi, title, { command: shellCommand, args: [] }) : { command: shellCommand, args: [] };
13207
13225
  this.trackPendingPaneLaunch(requestId, {
13208
13226
  kind: 'process',
13209
- command: shellCommand,
13210
- args: [],
13227
+ command: launched.command,
13228
+ args: launched.args,
13211
13229
  cwd,
13212
13230
  title,
13213
13231
  paneType: 'process',
@@ -13215,11 +13233,12 @@ export class RatatuiApp {
13215
13233
  return this.tui.send({
13216
13234
  type: 'create_pane',
13217
13235
  request_id: requestId,
13218
- command: shellCommand,
13219
- args: [],
13236
+ command: launched.command,
13237
+ args: launched.args,
13220
13238
  cwd,
13221
13239
  title,
13222
13240
  pane_type: 'process',
13241
+ ...(motsi ? { env: { ...MOTSI_PANE_ENV } } : {}),
13223
13242
  ...(options?.direction ? { direction: options.direction } : {}),
13224
13243
  ...(options?.ratio !== undefined ? { ratio: options.ratio } : {}),
13225
13244
  ...(options?.target !== undefined ? { target: options.target } : {}),
@@ -13244,10 +13263,12 @@ export class RatatuiApp {
13244
13263
  const requestId = `create-command-${generateSessionId()}`;
13245
13264
  const cwd = options?.cwd || this.shellCwd || process.cwd();
13246
13265
  const title = options?.title || fallbackTitle;
13266
+ const motsi = this.paneMotsi();
13267
+ const launched = motsi ? throughMotsi(motsi, title, { command: shellCommand, args: shellArgs }) : { command: shellCommand, args: shellArgs };
13247
13268
  this.trackPendingPaneLaunch(requestId, {
13248
13269
  kind: 'process',
13249
- command: shellCommand,
13250
- args: shellArgs,
13270
+ command: launched.command,
13271
+ args: launched.args,
13251
13272
  cwd,
13252
13273
  title,
13253
13274
  paneType: 'command',
@@ -13255,11 +13276,12 @@ export class RatatuiApp {
13255
13276
  return this.tui.send({
13256
13277
  type: 'create_pane',
13257
13278
  request_id: requestId,
13258
- command: shellCommand,
13259
- args: shellArgs,
13279
+ command: launched.command,
13280
+ args: launched.args,
13260
13281
  cwd,
13261
13282
  title,
13262
13283
  pane_type: 'command',
13284
+ ...(motsi ? { env: { ...MOTSI_PANE_ENV } } : {}),
13263
13285
  })
13264
13286
  ? null
13265
13287
  : 'failed to create command pane';
@@ -279,6 +279,14 @@ export interface CustomPaneOption {
279
279
  export interface PanesSettings {
280
280
  /** Extra chooser rows, in the order they are offered. */
281
281
  custom?: CustomPaneOption[];
282
+ /**
283
+ * Start panes through MotSi (`motsi run`) so Zerg Router's Command Central
284
+ * can show them. Unset: whenever MotSi is installed and its remote view is
285
+ * on. False: never.
286
+ */
287
+ remoteView?: boolean;
288
+ /** The ZTC sign-in profile new Claude Code / Codex panes start on; unset for the CLI's own sign-in. */
289
+ agentProfiles?: Partial<Record<'claude' | 'codex', string>>;
282
290
  }
283
291
  /**
284
292
  * Which rule triggers auto-compaction. `percent` watches the model's context
@@ -35,6 +35,8 @@ export interface WorkspaceMcpOptions {
35
35
  env?: NodeJS.ProcessEnv;
36
36
  /** Working directory for panes opened without one. */
37
37
  cwd?: string;
38
+ /** `motsi` to start panes through (Zerg Router's Command Central); null for none; default: MotSi's remote view. */
39
+ motsi?: string | null;
38
40
  }
39
41
  /** The MCP server, connected to a workspace through `transport`. */
40
42
  export declare function createWorkspaceMcpServer(options: WorkspaceMcpOptions): Server;
@@ -18,6 +18,8 @@ import { childZtcPaneEnv, closeTerminalPaneTool, listWorkspacePanes, readTermina
18
18
  import { AGENT_CLIS, findAgentCli, nextPaneTitle, workspaceMcpProgram } from '../panes/agent_cli.js';
19
19
  import { TYPED_ENTER_DELAY_MS, formatAgentPaneMessage, openAgentPane } from '../panes/agent_panes.js';
20
20
  import { sessionNamePaneTitle } from '../panes/labels.js';
21
+ import { MOTSI_PANE_ENV, remoteViewMotsi, throughMotsi } from '../panes/motsi.js';
22
+ import { configStore } from '../config.js';
21
23
  import { buildZtcPaneLaunchCommand, createZtcLaunchSpec } from '../panes/ztc_launch.js';
22
24
  import { PaneRelayClient } from '../ui/ratatui/pane_relay.js';
23
25
  import { buildUserSubprocessEnvOverrides } from '../utils/subprocess_env.js';
@@ -117,6 +119,8 @@ const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
117
119
  export function createWorkspaceMcpServer(options) {
118
120
  const { transport, ownPaneId } = options;
119
121
  const env = options.env ?? process.env;
122
+ // Panes this agent opens show in Zerg Router's Command Central too, when MotSi's remote view is on.
123
+ const paneMotsi = () => (options.motsi !== undefined ? options.motsi : remoteViewMotsi(configStore.get().panes, { env }));
120
124
  if (transport) {
121
125
  setPaneToolsTuiProcess(transport);
122
126
  if (ownPaneId !== null)
@@ -184,18 +188,21 @@ export function createWorkspaceMcpServer(options) {
184
188
  const requestId = `ztc-pane-${randomUUID()}`;
185
189
  const created = waitForPaneCreatedOrError(tui, { requestId }, 15000);
186
190
  const placement = placementOf(args.placement, ownPaneId);
191
+ const motsi = paneMotsi();
192
+ const child = { command: built.command, args: task ? [...built.args, '--', task] : built.args };
193
+ const launched = motsi ? throughMotsi(motsi, title, child) : child;
187
194
  const sent = tui.send({
188
195
  type: 'create_pane',
189
196
  request_id: requestId,
190
- command: built.command,
191
- args: task ? [...built.args, '--', task] : built.args,
197
+ command: launched.command,
198
+ args: launched.args,
192
199
  cwd: built.cwd || cwd,
193
200
  title,
194
201
  pane_type: 'ztc',
195
202
  ...(placement.direction ? { direction: placement.direction, ratio: 0.5 } : {}),
196
203
  ...(placement.target !== undefined ? { target: placement.target } : {}),
197
204
  ...(placement.newWindow ? { new_window: true } : {}),
198
- env: childZtcPaneEnv(),
205
+ env: { ...childZtcPaneEnv(), ...(motsi ? MOTSI_PANE_ENV : {}) },
199
206
  });
200
207
  if (!sent) {
201
208
  created.catch(() => { });
@@ -216,18 +223,21 @@ export function createWorkspaceMcpServer(options) {
216
223
  const requestId = `create-shell-${randomUUID()}`;
217
224
  const created = waitForPaneCreatedOrError(tui, { requestId }, 15000);
218
225
  const placement = placementOf(args.placement, ownPaneId);
226
+ const motsi = paneMotsi();
227
+ const shell = { command: env.SHELL || '/bin/sh', args: [] };
228
+ const launched = motsi ? throughMotsi(motsi, title, shell) : shell;
219
229
  const sent = tui.send({
220
230
  type: 'create_pane',
221
231
  request_id: requestId,
222
- command: env.SHELL || '/bin/sh',
223
- args: [],
232
+ command: launched.command,
233
+ args: launched.args,
224
234
  cwd,
225
235
  title,
226
236
  pane_type: 'process',
227
237
  ...(placement.direction ? { direction: placement.direction, ratio: 0.5 } : {}),
228
238
  ...(placement.target !== undefined ? { target: placement.target } : {}),
229
239
  ...(placement.newWindow ? { new_window: true } : {}),
230
- env: buildUserSubprocessEnvOverrides(),
240
+ env: { ...buildUserSubprocessEnvOverrides(), ...(motsi ? MOTSI_PANE_ENV : {}) },
231
241
  });
232
242
  if (!sent) {
233
243
  created.catch(() => { });
@@ -278,6 +288,7 @@ export function createWorkspaceMcpServer(options) {
278
288
  findBinary: options.findBinary ?? findAgentCli,
279
289
  server: options.server === undefined ? workspaceMcpProgram(process.argv.slice(0, 2)) : options.server,
280
290
  env,
291
+ motsi: paneMotsi(),
281
292
  });
282
293
  if (result.error)
283
294
  throw new ToolError(result.error);
@@ -46,17 +46,19 @@ export declare class CliArgumentsError extends Error {
46
46
  export declare function splitCliArguments(raw: string): string[];
47
47
  /** Words as a shell would accept them back: quoted only where needed. */
48
48
  export declare function formatCliArguments(words: readonly string[]): string;
49
- /** What to start the CLI with: its own flags, an opening prompt, or both. */
49
+ /** What to start the CLI with: its own flags, an opening prompt, or both, on a ZTC profile or not. */
50
50
  export interface AgentLaunchRequest {
51
51
  words: string[];
52
52
  prompt?: string;
53
+ /** A ZTC sign-in profile (see panes/agent_profiles.ts). */
54
+ profile?: string;
53
55
  }
54
56
  /**
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`).
57
+ * Read the text after `/claude` or `/codex`. A leading `@name` picks a ZTC
58
+ * sign-in profile. Text that starts with a flag is the CLI's own arguments,
59
+ * split like a shell command line (`/claude --continue`, `/codex -m gpt-5
60
+ * "review this"`). Anything else is the opening prompt, kept exactly as typed,
61
+ * apostrophes and all (`/claude don't touch the migrations`).
60
62
  */
61
63
  export declare function parseAgentCommandBody(body: string): AgentLaunchRequest;
62
64
  export interface PaneProgram {
@@ -16,6 +16,7 @@ import { accessSync, constants, existsSync, statSync } from 'node:fs';
16
16
  import { homedir } from 'node:os';
17
17
  import { basename, dirname, join } from 'node:path';
18
18
  import { resolveOwnedCliLaunch } from '../desktop/cli_launch.js';
19
+ import { isProfileName } from './agent_profiles.js';
19
20
  import { buildUserSubprocessEnvOverrides } from '../utils/subprocess_env.js';
20
21
  export const AGENT_CLIS = Object.freeze({
21
22
  claude: {
@@ -160,19 +161,29 @@ export function formatCliArguments(words) {
160
161
  .join(' ');
161
162
  }
162
163
  /**
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`).
164
+ * Read the text after `/claude` or `/codex`. A leading `@name` picks a ZTC
165
+ * sign-in profile. Text that starts with a flag is the CLI's own arguments,
166
+ * split like a shell command line (`/claude --continue`, `/codex -m gpt-5
167
+ * "review this"`). Anything else is the opening prompt, kept exactly as typed,
168
+ * apostrophes and all (`/claude don't touch the migrations`).
168
169
  */
169
170
  export function parseAgentCommandBody(body) {
170
- const text = body.trim();
171
+ let text = body.trim();
172
+ let profile;
173
+ const named = /^@(\S+)(?:\s+|$)/.exec(text);
174
+ if (named) {
175
+ profile = named[1];
176
+ if (!isProfileName(profile)) {
177
+ throw new CliArgumentsError(`"${profile}" is not a profile name: use letters, digits, ".", "_" or "-", up to 32.`);
178
+ }
179
+ text = text.slice(named[0].length).trim();
180
+ }
181
+ const withProfile = (request) => (profile ? { ...request, profile } : request);
171
182
  if (!text)
172
- return { words: [] };
183
+ return withProfile({ words: [] });
173
184
  if (text.startsWith('-'))
174
- return { words: splitCliArguments(text) };
175
- return { words: [], prompt: text };
185
+ return withProfile({ words: splitCliArguments(text) });
186
+ return withProfile({ words: [], prompt: text });
176
187
  }
177
188
  export const WORKSPACE_MCP_SERVER_NAME = 'ztc';
178
189
  /**
@@ -28,6 +28,19 @@ export interface OpenAgentPaneOptions {
28
28
  server?: PaneProgram | Error | null;
29
29
  /** Source of SHELL and the workspace variables (default: this process). */
30
30
  env?: NodeJS.ProcessEnv;
31
+ /**
32
+ * The `motsi` to start the pane through, so Zerg Router's Command Central
33
+ * shows it; null for none. Default: when MotSi's remote view is on and
34
+ * ZTC's `panes.remoteView` isn't false.
35
+ */
36
+ motsi?: string | null;
37
+ /**
38
+ * A ZTC sign-in profile to start the CLI on; null for the CLI's own sign-in.
39
+ * Default: the request's `@profile`, else `panes.agentProfiles[cli]`.
40
+ */
41
+ profile?: string | null;
42
+ /** Home directory the profiles live under (default: the user's). */
43
+ profilesHome?: string;
31
44
  }
32
45
  export interface OpenAgentPaneResult {
33
46
  paneId?: number;
@@ -10,6 +10,9 @@ import { randomUUID } from 'node:crypto';
10
10
  import { listWorkspacePanes, waitForPaneCreatedOrError } from '../agent/tools/pane.js';
11
11
  import { AGENT_CLIS, agentCliArgs, agentPaneEnv, findAgentCli, isAgentCliId, loginShellExec, nextPaneTitle, workspaceMcpProgram, } from './agent_cli.js';
12
12
  import { sessionNamePaneTitle } from './labels.js';
13
+ import { MOTSI_PANE_ENV, remoteViewMotsi, throughMotsi } from './motsi.js';
14
+ import { agentProfileEnv, ensureAgentProfile } from './agent_profiles.js';
15
+ import { configStore } from '../config.js';
13
16
  /** Request ids of agent pane launches start with this, so the overseer can tell them apart. */
14
17
  export const AGENT_PANE_REQUEST_PREFIX = 'agent-pane:';
15
18
  /** Which agent CLI a `create_pane` request id launched, if any. */
@@ -36,8 +39,30 @@ export async function openAgentPane(transport, options) {
36
39
  const env = options.env ?? process.env;
37
40
  const server = options.server === undefined ? workspaceMcpProgram() : options.server;
38
41
  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);
42
+ const profile = options.profile !== undefined
43
+ ? options.profile
44
+ : options.request?.profile ?? configStore.get().panes?.agentProfiles?.[options.cli] ?? null;
45
+ let profileEnv = {};
46
+ if (profile) {
47
+ try {
48
+ profileEnv = agentProfileEnv(options.cli, ensureAgentProfile(options.cli, profile, options.profilesHome));
49
+ }
50
+ catch (error) {
51
+ return { error: `Could not prepare the "${profile}" profile for ${spec.label}: ${error.message}` };
52
+ }
53
+ }
54
+ const label = profile ? `${spec.label} (${profile})` : spec.label;
55
+ const title = options.title?.trim() || nextPaneTitle(label, await existingTitles(transport));
56
+ const cliArgs = [
57
+ // On a profile, Codex keeps the sign-in in the profile's files, never a keyring entry other terminals share.
58
+ ...(profile && options.cli === 'codex' ? ['-c', 'cli_auth_credentials_store="file"'] : []),
59
+ ...agentCliArgs(options.cli, options.request ?? { words: [] }, attached),
60
+ ];
61
+ const motsi = options.motsi === undefined ? remoteViewMotsi(configStore.get().panes, { env }) : options.motsi;
62
+ // Through MotSi, the login shell still runs first, then `motsi run` around the CLI.
63
+ const program = motsi
64
+ ? loginShellExec(motsi, throughMotsi(motsi, title, { command: binary, args: cliArgs }).args, env.SHELL)
65
+ : loginShellExec(binary, cliArgs, env.SHELL);
41
66
  const requestId = `${AGENT_PANE_REQUEST_PREFIX}${options.cli}:${randomUUID()}`;
42
67
  const created = waitForPaneCreatedOrError(transport, { requestId }, 15000);
43
68
  const sent = transport.send({
@@ -52,7 +77,7 @@ export async function openAgentPane(transport, options) {
52
77
  ...(options.ratio !== undefined ? { ratio: options.ratio } : {}),
53
78
  ...(options.target !== undefined ? { target: options.target } : {}),
54
79
  ...(options.newWindow ? { new_window: true } : {}),
55
- env: agentPaneEnv(env),
80
+ env: { ...agentPaneEnv(env), ...(motsi ? MOTSI_PANE_ENV : {}), ...profileEnv },
56
81
  });
57
82
  if (!sent) {
58
83
  created.catch(() => { });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * ZTC's own sign-in profiles for Claude Code and Codex panes.
3
+ *
4
+ * A profile is a config directory of its own, handed to the CLI through its
5
+ * documented variable (Claude Code's CLAUDE_CONFIG_DIR, Codex's CODEX_HOME).
6
+ * The user signs in there with the CLI's own login the first time a pane opens
7
+ * on it, and the CLI keeps that sign-in there, so a ZTC pane can use another
8
+ * account than the CLI uses in iTerm or Ghostty without changing it there.
9
+ * ZTC creates the directory and never reads what the CLI stores in it, and it
10
+ * never moves a pane to another profile on its own: the user picks.
11
+ */
12
+ import type { AgentCliId } from './agent_cli.js';
13
+ /** Letters, digits, `.`, `_` and `-`, up to 32, starting with a letter or digit. */
14
+ export declare const PROFILE_NAME: RegExp;
15
+ export declare function isProfileName(value: string): boolean;
16
+ export declare function agentProfileDir(cli: AgentCliId, name: string, home?: string): string;
17
+ /** The profile's directory, created private to the user if it is new. */
18
+ export declare function ensureAgentProfile(cli: AgentCliId, name: string, home?: string): string;
19
+ /** The CLI's profiles, by name. */
20
+ export declare function listAgentProfiles(cli: AgentCliId, home?: string): string[];
21
+ /** The variable that points the CLI at the profile. */
22
+ export declare function agentProfileEnv(cli: AgentCliId, dir: string): Record<string, string>;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * ZTC's own sign-in profiles for Claude Code and Codex panes.
3
+ *
4
+ * A profile is a config directory of its own, handed to the CLI through its
5
+ * documented variable (Claude Code's CLAUDE_CONFIG_DIR, Codex's CODEX_HOME).
6
+ * The user signs in there with the CLI's own login the first time a pane opens
7
+ * on it, and the CLI keeps that sign-in there, so a ZTC pane can use another
8
+ * account than the CLI uses in iTerm or Ghostty without changing it there.
9
+ * ZTC creates the directory and never reads what the CLI stores in it, and it
10
+ * never moves a pane to another profile on its own: the user picks.
11
+ */
12
+ import { mkdirSync, readdirSync } from 'node:fs';
13
+ import { homedir } from 'node:os';
14
+ import { join } from 'node:path';
15
+ /** Letters, digits, `.`, `_` and `-`, up to 32, starting with a letter or digit. */
16
+ export const PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/;
17
+ export function isProfileName(value) {
18
+ return PROFILE_NAME.test(value) && value !== '.' && value !== '..';
19
+ }
20
+ function profilesRoot(cli, home) {
21
+ return join(home, '.ztc', 'agent-profiles', cli);
22
+ }
23
+ export function agentProfileDir(cli, name, home = homedir()) {
24
+ if (!isProfileName(name))
25
+ throw new Error(`"${name}" is not a profile name.`);
26
+ return join(profilesRoot(cli, home), name);
27
+ }
28
+ /** The profile's directory, created private to the user if it is new. */
29
+ export function ensureAgentProfile(cli, name, home = homedir()) {
30
+ const dir = agentProfileDir(cli, name, home);
31
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
32
+ return dir;
33
+ }
34
+ /** The CLI's profiles, by name. */
35
+ export function listAgentProfiles(cli, home = homedir()) {
36
+ try {
37
+ return readdirSync(profilesRoot(cli, home), { withFileTypes: true })
38
+ .filter(entry => entry.isDirectory() && isProfileName(entry.name))
39
+ .map(entry => entry.name)
40
+ .sort();
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ }
46
+ /** The variable that points the CLI at the profile. */
47
+ export function agentProfileEnv(cli, dir) {
48
+ return cli === 'claude' ? { CLAUDE_CONFIG_DIR: dir } : { CODEX_HOME: dir };
49
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Panes Zerg Router can show, through MotSi.
3
+ *
4
+ * MotSi's `motsi run -- <command>` runs a program in a pseudo-terminal it
5
+ * passes through unchanged and, while the owner watches it in Zerg Router's
6
+ * Command Central, streams it there, redacted on this computer first. Whether
7
+ * a viewer may type is MotSi's to decide, and it allows it only into an agent
8
+ * (Claude Code, Codex, ZTC), never a shell.
9
+ *
10
+ * ZTC starts its panes through it when MotSi is installed and MotSi's own
11
+ * remote view switch is on (`motsi remote view on|off`, kept on this computer),
12
+ * unless ZTC's `panes.remoteView` setting is false. ZTC reads only that switch
13
+ * from MotSi's settings, nothing else of MotSi's.
14
+ */
15
+ import type { PaneProgram } from './agent_cli.js';
16
+ export interface MotsiLookup {
17
+ env?: NodeJS.ProcessEnv;
18
+ home?: string;
19
+ systemDirs?: readonly string[];
20
+ }
21
+ /** The `motsi` binary: PATH first, then `go install`'s and the installers' directories. */
22
+ export declare function findMotsi(lookup?: MotsiLookup): string | null;
23
+ /** MotSi's own switch, from its settings (`$MOTSI_HOME` or `~/.config/motsi`). Anything but a true is off. */
24
+ export declare function motsiRemoteViewOn(lookup?: MotsiLookup): boolean;
25
+ /** The `motsi` to start panes through, or null when panes start as they always have. */
26
+ export declare function remoteViewMotsi(panes: {
27
+ remoteView?: boolean;
28
+ } | undefined, lookup?: MotsiLookup): string | null;
29
+ /** The same program and arguments, as a MotSi terminal named after the pane. */
30
+ export declare function throughMotsi(motsi: string, title: string, program: PaneProgram): PaneProgram;
31
+ /**
32
+ * Pane environment for a MotSi terminal: an inherited terminal id would make
33
+ * `motsi run` treat the pane as part of the terminal ZTC itself runs in.
34
+ */
35
+ export declare const MOTSI_PANE_ENV: Readonly<Record<string, string>>;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Panes Zerg Router can show, through MotSi.
3
+ *
4
+ * MotSi's `motsi run -- <command>` runs a program in a pseudo-terminal it
5
+ * passes through unchanged and, while the owner watches it in Zerg Router's
6
+ * Command Central, streams it there, redacted on this computer first. Whether
7
+ * a viewer may type is MotSi's to decide, and it allows it only into an agent
8
+ * (Claude Code, Codex, ZTC), never a shell.
9
+ *
10
+ * ZTC starts its panes through it when MotSi is installed and MotSi's own
11
+ * remote view switch is on (`motsi remote view on|off`, kept on this computer),
12
+ * unless ZTC's `panes.remoteView` setting is false. ZTC reads only that switch
13
+ * from MotSi's settings, nothing else of MotSi's.
14
+ */
15
+ import { accessSync, constants, readFileSync, statSync } from 'node:fs';
16
+ import { homedir } from 'node:os';
17
+ import { join } from 'node:path';
18
+ const SYSTEM_BIN_DIRS = ['/opt/homebrew/bin', '/usr/local/bin', '/usr/bin'];
19
+ function isExecutableFile(path) {
20
+ try {
21
+ if (!statSync(path).isFile())
22
+ return false;
23
+ accessSync(path, constants.X_OK);
24
+ return true;
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /** The `motsi` binary: PATH first, then `go install`'s and the installers' directories. */
31
+ export function findMotsi(lookup = {}) {
32
+ const env = lookup.env ?? process.env;
33
+ const home = lookup.home ?? homedir();
34
+ const dirs = [
35
+ ...(env.PATH ?? '').split(':').filter(Boolean),
36
+ join(home, 'go', 'bin'),
37
+ join(home, '.local', 'bin'),
38
+ ...(lookup.systemDirs ?? SYSTEM_BIN_DIRS),
39
+ ];
40
+ for (const dir of dirs) {
41
+ const candidate = join(dir, 'motsi');
42
+ if (isExecutableFile(candidate))
43
+ return candidate;
44
+ }
45
+ return null;
46
+ }
47
+ /** MotSi's own switch, from its settings (`$MOTSI_HOME` or `~/.config/motsi`). Anything but a true is off. */
48
+ export function motsiRemoteViewOn(lookup = {}) {
49
+ const env = lookup.env ?? process.env;
50
+ const dir = env.MOTSI_HOME || join(lookup.home ?? homedir(), '.config', 'motsi');
51
+ try {
52
+ const settings = JSON.parse(readFileSync(join(dir, 'settings.json'), 'utf8'));
53
+ return settings?.remote_view === true;
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ /** The `motsi` to start panes through, or null when panes start as they always have. */
60
+ export function remoteViewMotsi(panes, lookup = {}) {
61
+ if (panes?.remoteView === false)
62
+ return null;
63
+ if (!motsiRemoteViewOn(lookup))
64
+ return null;
65
+ return findMotsi(lookup);
66
+ }
67
+ /** The same program and arguments, as a MotSi terminal named after the pane. */
68
+ export function throughMotsi(motsi, title, program) {
69
+ return { command: motsi, args: ['run', `--title=${title}`, '--', program.command, ...program.args] };
70
+ }
71
+ /**
72
+ * Pane environment for a MotSi terminal: an inherited terminal id would make
73
+ * `motsi run` treat the pane as part of the terminal ZTC itself runs in.
74
+ */
75
+ export const MOTSI_PANE_ENV = Object.freeze({ MOTSI_TERMINAL_ID: '' });
@@ -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.9.1-dev.202611052132" : "development";
78505
+ var ZLM_ZTC_VERSION = true ? "0.10.1-dev.202611052216" : "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.9.1-dev.202611052132",
3
+ "version": "0.10.1-dev.202611052216",
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": "development",
279
279
  "channel": "dev",
280
- "sourceCommit": "f987bf5a7f5b3b8c9b6ed6e5ace077242bdb2be6",
280
+ "sourceCommit": "eedc94d57cd6224856beab9b16b5d6c235978ece",
281
281
  "sourceVersion": "0.2.0-canary.2"
282
282
  },
283
283
  "publishConfig": {