@myagentroam/node 0.9.131 → 0.9.133

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/connector.js CHANGED
@@ -1480,7 +1480,8 @@ export class NodeConnector {
1480
1480
  this.operationRouter.register('node.upgrade.request', (data) => this.upgradeCoordinator.request(data));
1481
1481
  this.operationRouter.register('node.upgrade.cancel', () => this.upgradeCoordinator.cancel());
1482
1482
  this.operationRouter.register('node.workbench.capabilities', async () => ({
1483
- manifest: await this.workbenchManifestService.read()
1483
+ manifest: await this.workbenchManifestService.read(),
1484
+ binaryTerminal: true
1484
1485
  }));
1485
1486
  }
1486
1487
  async initializeConversationHosting() {
package/dist/marmgr.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // Keep the CLI implementation in its own package; the Node install exposes the same entrypoint.
3
+ const entrypoint = '@myagentroam/marmgr/bin/marmgr.mjs';
4
+ await import(entrypoint);
5
+ export {};
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ const entrypoint = '@myagentroam/marmgr/bin/marnodes.mjs';
3
+ await import(entrypoint);
4
+ export {};
package/dist/marscp.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ const entrypoint = '@myagentroam/marmgr/bin/marscp.mjs';
3
+ await import(entrypoint);
4
+ export {};
package/dist/marssh.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ const entrypoint = '@myagentroam/marmgr/bin/marssh.mjs';
3
+ await import(entrypoint);
4
+ export {};
@@ -433,6 +433,7 @@ async function readTranscript(path, runner, byteLimit, page, targetLines = Numbe
433
433
  const toolIndexes = new Map();
434
434
  const metaRecordIds = new Set();
435
435
  const claudeCompactCommandIds = new Set();
436
+ let codexTurnId;
436
437
  forEachJsonlRecord(content, (value) => {
437
438
  if (derivedTranscriptRecord(runner, value))
438
439
  derivedSession = true;
@@ -444,6 +445,13 @@ async function readTranscript(path, runner, byteLimit, page, targetLines = Numbe
444
445
  claudeCompactCommandIds.add(value['uuid']);
445
446
  });
446
447
  forEachJsonlRecord(truncated ? window.pageContent : content, (value) => {
448
+ if (runner === 'codex' && value['type'] === 'event_msg' && isRecord(value['payload'])) {
449
+ const payload = value['payload'];
450
+ if (payload['type'] === 'task_started')
451
+ codexTurnId = typeof payload['turn_id'] === 'string' ? payload['turn_id'] : undefined;
452
+ else if (payload['type'] === 'task_complete')
453
+ codexTurnId = undefined;
454
+ }
447
455
  const recordId = typeof value['uuid'] === 'string' ? value['uuid'] : undefined;
448
456
  const parentId = typeof value['parentUuid'] === 'string' ? value['parentUuid'] : undefined;
449
457
  if (value['isMeta'] === true && recordId !== undefined)
@@ -469,7 +477,16 @@ async function readTranscript(path, runner, byteLimit, page, targetLines = Numbe
469
477
  if (generatedImage !== undefined)
470
478
  items.push(generatedImage);
471
479
  for (const parsedItem of transcriptItems(runner, value)) {
472
- const item = recordId === undefined ? parsedItem : { ...parsedItem, nativeId: recordId };
480
+ const item = {
481
+ ...parsedItem,
482
+ ...(recordId === undefined ? {} : { nativeId: recordId }),
483
+ ...(runner === 'codex' &&
484
+ parsedItem.kind === 'message' &&
485
+ parsedItem.role === 'USER' &&
486
+ codexTurnId !== undefined
487
+ ? { nativeTurnId: codexTurnId }
488
+ : {})
489
+ };
473
490
  if (item.kind === 'tool_result') {
474
491
  const toolIndex = toolIndexes.get(item.toolUseId);
475
492
  if (toolIndex !== undefined)
@@ -0,0 +1,34 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ /** Environment for processes started by this Node, not for the Node service itself. */
5
+ export function nodeExecutionEnvironment(provided = {}, inherited = process.env, options = {}) {
6
+ const environment = Object.fromEntries(Object.entries(inherited).filter((entry) => typeof entry[1] === 'string'));
7
+ Object.assign(environment, provided);
8
+ environment['MAR_NODE_CONTEXT'] = '1';
9
+ const platform = options.platform ?? process.platform;
10
+ const packageRoot = options.packageRoot ?? resolve(dirname(fileURLToPath(import.meta.url)), '..');
11
+ const cliBin = join(packageRoot, 'node_modules', '.bin');
12
+ if (!existsSync(join(cliBin, platform === 'win32' ? 'marnodes.cmd' : 'marnodes')))
13
+ return environment;
14
+ const separator = platform === 'win32' ? ';' : ':';
15
+ const existingKeys = Object.keys(environment).filter((key) => key.toLowerCase() === 'path');
16
+ const key = platform === 'win32'
17
+ ? (Object.keys(provided).find((name) => name.toLowerCase() === 'path') ??
18
+ existingKeys[0] ??
19
+ 'Path')
20
+ : 'PATH';
21
+ const current = environment[key] ?? environment[existingKeys[0] ?? key] ?? '';
22
+ if (platform === 'win32')
23
+ for (const other of existingKeys)
24
+ if (other !== key)
25
+ delete environment[other];
26
+ const samePath = (entry) => platform === 'win32'
27
+ ? entry.replaceAll('/', '\\').toLowerCase() === cliBin.replaceAll('/', '\\').toLowerCase()
28
+ : entry === cliBin;
29
+ environment[key] = [
30
+ cliBin,
31
+ ...current.split(separator).filter((entry) => entry && !samePath(entry))
32
+ ].join(separator);
33
+ return environment;
34
+ }
@@ -1,4 +1,5 @@
1
1
  import { mcpRuntimeDescriptorSchema } from '@myagentroam/protocol';
2
+ import { nodeExecutionEnvironment } from '../node-execution-environment.js';
2
3
  /**
3
4
  * Stable lifecycle boundary for every Node Runner implementation.
4
5
  *
@@ -166,6 +167,16 @@ export class AbstractRunner {
166
167
  function resolvedMcpServers(raw, secrets) {
167
168
  if (!Array.isArray(raw))
168
169
  return [];
170
+ const marmgrEnvironment = {
171
+ ...(secrets['MAR_MGR_SERVER_URL'] === undefined
172
+ ? {}
173
+ : { MAR_MGR_SERVER_URL: secrets['MAR_MGR_SERVER_URL'] }),
174
+ ...(secrets['MAR_MGR_USER_TOKEN'] === undefined
175
+ ? {}
176
+ : { MAR_MGR_USER_TOKEN: secrets['MAR_MGR_USER_TOKEN'] }),
177
+ MAR_MGR_CONTEXT_REQUIRED: '1'
178
+ };
179
+ const mcpEnvironment = (provided) => nodeExecutionEnvironment({ ...provided, ...marmgrEnvironment }, { PATH: process.env['PATH'], Path: process.env['Path'] });
169
180
  const result = [];
170
181
  for (const value of raw) {
171
182
  if (value === null || typeof value !== 'object' || Array.isArray(value))
@@ -188,7 +199,7 @@ function resolvedMcpServers(raw, secrets) {
188
199
  transport: 'STDIO',
189
200
  command: record['command'],
190
201
  args: record['args'],
191
- environment: record['__marGoal'] === true
202
+ environment: mcpEnvironment(record['__marGoal'] === true
192
203
  ? {
193
204
  MAR_GOAL_MCP_ENDPOINT: record['endpoint'],
194
205
  MAR_GOAL_MCP_TOKEN: secrets[record['secretName']]
@@ -202,7 +213,7 @@ function resolvedMcpServers(raw, secrets) {
202
213
  : {
203
214
  MAR_MAIL_MCP_ENDPOINT: record['endpoint'],
204
215
  MAR_MAIL_MCP_TOKEN: secrets[record['secretName']]
205
- }
216
+ })
206
217
  });
207
218
  continue;
208
219
  }
@@ -239,10 +250,10 @@ function resolvedMcpServers(raw, secrets) {
239
250
  ? configuration['arguments'].filter((item) => typeof item === 'string')
240
251
  : [])
241
252
  ],
242
- environment: {
253
+ environment: mcpEnvironment({
243
254
  ...runtimeEnvironment,
244
255
  ...configuredEnvironment
245
- }
256
+ })
246
257
  });
247
258
  }
248
259
  else {
@@ -1,5 +1,6 @@
1
1
  import { createEnvelope } from '@myagentroam/protocol';
2
2
  import { ClaudeChannelGate } from '../../claude-channel.js';
3
+ import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
3
4
  import { boundedConversationItemId, claudePlanText, compactRunnerText, extractClaudeText, extractClaudeThinkingSummaries, extractClaudeToolResults, extractClaudeToolUses } from '../codex/conversation-parser.js';
4
5
  import { normalizeClaudeTool } from './tool-normalizer.js';
5
6
  import { claudeContentBlocks } from '../../util/node-operation-parsers.js';
@@ -170,7 +171,10 @@ export class ClaudeManagedRunController {
170
171
  ...(typeof payload.model === 'string' ? { model: payload.model } : {}),
171
172
  ...(typeof payload.effort === 'string' ? { effort: payload.effort } : {}),
172
173
  ...(typeof payload.access === 'string' ? { access: payload.access } : {}),
173
- environment: secretEnvironment,
174
+ environment: nodeExecutionEnvironment({
175
+ ...secretEnvironment,
176
+ MAR_MGR_CONTEXT_REQUIRED: '1'
177
+ }, { PATH: process.env['PATH'], Path: process.env['Path'] }),
174
178
  ...(typeof payload.personalInstructions === 'string'
175
179
  ? { personalInstructions: payload.personalInstructions }
176
180
  : {}),
@@ -1,3 +1,4 @@
1
+ import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
1
2
  import { extractId } from '../../util/runner-native-session-parsers.js';
2
3
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
3
4
  import { safeErrorCode } from '../../util/safe-error.js';
@@ -65,7 +66,7 @@ export class CodexManagedRunController {
65
66
  throw new Error('RUNNER_COMMAND_UNAVAILABLE');
66
67
  if (command.id === 'compact') {
67
68
  const runId = crypto.randomUUID();
68
- this.runner.prepareEnvironment(context.secretEnvironment, runId);
69
+ this.runner.prepareEnvironment(nodeExecutionEnvironment({ ...context.secretEnvironment, MAR_MGR_CONTEXT_REQUIRED: '1' }, { PATH: process.env['PATH'], Path: process.env['Path'] }), runId);
69
70
  try {
70
71
  await this.runner.start(runId);
71
72
  return await this.compact(session, threadId, runId, context.initiatedByUserId);
@@ -385,7 +386,7 @@ export class CodexManagedRunController {
385
386
  return;
386
387
  }
387
388
  try {
388
- this.runner.prepareEnvironment(secretEnvironment, runId);
389
+ this.runner.prepareEnvironment(nodeExecutionEnvironment({ ...secretEnvironment, MAR_MGR_CONTEXT_REQUIRED: '1' }, { PATH: process.env['PATH'], Path: process.env['Path'] }), runId);
389
390
  }
390
391
  catch (error) {
391
392
  this.host.emit(runId, 'run.rejected', { code: safeErrorCode(error, 'SECRET_ENVIRONMENT_INVALID') }, 'FAILED');
@@ -2,6 +2,7 @@ import { computerUseHostSkill } from './computer-use-skill.js';
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { homedir } from 'node:os';
4
4
  import { access, constants } from 'node:fs/promises';
5
+ import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
5
6
  import { BUNDLED_RIPGREP_VERSION, bundledRipgrepPath, prependBundledRipgrepToPath } from '../../ripgrep-binary.js';
6
7
  import { LocalAgentHost, createMarAgent } from '@myagentroam/agent';
7
8
  import { marAgentReasoningEffortSchema, marAgentRuntimeModelConfigurationSchema } from '@myagentroam/protocol';
@@ -686,7 +687,10 @@ export function allowsUntrustedWebFetchTls(environment) {
686
687
  return environment['MAR_AGENT_WEB_FETCH_ALLOW_UNTRUSTED_TLS'] !== '0';
687
688
  }
688
689
  async function resolveMarAgentHostRuntimeConfiguration(secretEnvironment = {}) {
689
- const environment = { ...process.env, ...secretEnvironment };
690
+ const environment = nodeExecutionEnvironment({
691
+ ...secretEnvironment,
692
+ MAR_MGR_CONTEXT_REQUIRED: '1'
693
+ });
690
694
  const ripgrep = await availableBundledRipgrep();
691
695
  return {
692
696
  environment: ripgrep === undefined ? environment : prependBundledRipgrepToPath(environment, ripgrep),
@@ -221,24 +221,36 @@ export class MarAgentRunner extends AbstractRunner {
221
221
  let catalog;
222
222
  try {
223
223
  catalog = await openMarAgentSessionCatalog({ homeDirectory: this.homeDirectory });
224
- const listed = await catalog.list({ limit: 200 });
224
+ const activeCatalog = catalog;
225
+ const listed = await activeCatalog.list({ limit: 200 });
225
226
  const workspaces = workspace ? [workspace] : context.workspaces;
226
- return listed.sessions.flatMap((session) => {
227
+ return (await Promise.all(listed.sessions.map(async (session) => {
227
228
  const target = workspaces.find((candidate) => resolve(candidate.path) === resolve(session.workspace));
228
229
  if (!target)
229
230
  return [];
231
+ const executionConfiguration = session.modelId === undefined
232
+ ? await activeCatalog.executionConfiguration(session.id)
233
+ : session;
230
234
  return [
231
- context.createProjection({
232
- workspace: target,
233
- runner: this.name,
234
- externalSessionId: session.id,
235
- cwd: session.workspace,
236
- title: session.title,
237
- titleOrigin: 'NATIVE',
238
- lastActivityAt: Date.parse(session.lastActivityAt)
239
- })
235
+ {
236
+ ...context.createProjection({
237
+ workspace: target,
238
+ runner: this.name,
239
+ externalSessionId: session.id,
240
+ cwd: session.workspace,
241
+ title: session.title,
242
+ titleOrigin: 'NATIVE',
243
+ lastActivityAt: Date.parse(session.lastActivityAt)
244
+ }),
245
+ ...(executionConfiguration?.modelId === undefined
246
+ ? {}
247
+ : { model: executionConfiguration.modelId }),
248
+ ...(executionConfiguration?.reasoningEffort === undefined
249
+ ? {}
250
+ : { effort: executionConfiguration.reasoningEffort })
251
+ }
240
252
  ];
241
- });
253
+ }))).flat();
242
254
  }
243
255
  catch {
244
256
  return [];
@@ -1,3 +1,4 @@
1
+ import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
1
2
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
2
3
  import { runnerErrorProjection } from '../../util/runner-error.js';
3
4
  import { boundedConversationItemId, compactRunnerText } from '../codex/conversation-parser.js';
@@ -49,7 +50,7 @@ export class OpenCodeManagedRunController {
49
50
  this.host.hasActiveSessionLease(session.id))
50
51
  throw new Error('RUNNER_COMMAND_UNAVAILABLE');
51
52
  const runId = crypto.randomUUID();
52
- const client = this.runner.managedClient(environment);
53
+ const client = this.runner.managedClient(nodeExecutionEnvironment({ ...environment, MAR_MGR_CONTEXT_REQUIRED: '1' }, { PATH: process.env['PATH'], Path: process.env['Path'] }));
53
54
  this.host.adopt({
54
55
  runId,
55
56
  sessionId: session.id,
@@ -108,7 +109,10 @@ export class OpenCodeManagedRunController {
108
109
  let mcp;
109
110
  let attachments;
110
111
  try {
111
- environment = parseSecretEnvironment(payload.secretEnvironment);
112
+ environment = nodeExecutionEnvironment({
113
+ ...parseSecretEnvironment(payload.secretEnvironment),
114
+ MAR_MGR_CONTEXT_REQUIRED: '1'
115
+ }, { PATH: process.env['PATH'], Path: process.env['Path'] });
112
116
  mcpSecretEnvironment = parseSecretEnvironment(payload.mcpSecretEnvironment);
113
117
  mcp = this.runner.mcpConfiguration(payload.mcpInstallations, {
114
118
  ...environment,
@@ -369,20 +369,22 @@ export class ConversationHistoryService {
369
369
  }
370
370
  async readManagedRuntime(session, input, cursor, snapshotSequence, readAt) {
371
371
  const page = await this.readManagedActive(session, input, cursor, snapshotSequence, readAt);
372
+ const codexActive = session.runner === 'codex' && this.options.hasManagedActiveRun(session);
372
373
  if (input.cursor !== undefined ||
373
374
  session.externalSessionId === null ||
374
- this.options.hasManagedActiveRun(session) ||
375
- !this.hasManagedRuntimeHistory(session) ||
375
+ (!codexActive &&
376
+ (this.options.hasManagedActiveRun(session) || !this.hasManagedRuntimeHistory(session))) ||
376
377
  !page.turns.some((turn) => turn.rewind === undefined &&
377
378
  terminalConversationTurn(turn.status) &&
378
379
  conversationClientMessageId(turn) !== null))
379
380
  return page;
380
381
  // A completed managed Turn keeps its live items and Run identity until Runtime cleanup.
381
- // Only the SDK's matched native Turn may supply a destructive history boundary.
382
- const official = await this.readOfficial(session, {
383
- limit: Math.min(500, Math.max(30, page.turns.length + 1))
384
- }).catch(() => undefined);
385
- if (official === undefined)
382
+ // Only its matched native Turn may supply a Rewind boundary.
383
+ const historyInput = { limit: Math.min(500, Math.max(30, page.turns.length + 1)) };
384
+ const authoritative = codexActive
385
+ ? (await this.readRunnerHistory(session, historyInput).catch(() => undefined))?.page
386
+ : await this.readOfficial(session, historyInput).catch(() => undefined);
387
+ if (authoritative === undefined)
386
388
  return page;
387
389
  return {
388
390
  ...page,
@@ -392,7 +394,7 @@ export class ConversationHistoryService {
392
394
  const messageId = conversationClientMessageId(turn);
393
395
  if (messageId === null)
394
396
  return turn;
395
- const native = official.turns.find((candidate) => candidate.rewind !== undefined &&
397
+ const native = authoritative.turns.find((candidate) => candidate.rewind !== undefined &&
396
398
  candidate.userItemId !== null &&
397
399
  conversationClientMessageId(candidate) === messageId);
398
400
  return native?.rewind === undefined ? turn : { ...turn, rewind: native.rewind };
@@ -38,8 +38,11 @@ export class TerminalService {
38
38
  if (commandMode &&
39
39
  (input.workspaceId !== undefined ||
40
40
  typeof input.command !== 'string' ||
41
- input.command.trim().length === 0))
41
+ input.command.trim().length === 0 ||
42
+ (input.binary !== undefined && typeof input.binary !== 'boolean')))
42
43
  throw new Error('TERMINAL_COMMAND_INVALID');
44
+ if (!commandMode && input.binary !== undefined)
45
+ throw new Error('TERMINAL_OPEN_INVALID');
43
46
  const secretEnvironment = parseSecretEnvironment(input.secretEnvironment);
44
47
  if ((commandMode || secretEnvironment.MAR_GIT_TOKEN !== undefined) &&
45
48
  (!Number.isSafeInteger(input.__requestUserId) || Number(input.__requestUserId) <= 0))
@@ -58,11 +61,11 @@ export class TerminalService {
58
61
  const options = {
59
62
  ...input,
60
63
  workspaceId: typeof input.workspaceId === 'string' ? input.workspaceId : null,
61
- environment: secretEnvironment,
64
+ environment: { ...secretEnvironment, MAR_MGR_CONTEXT_REQUIRED: '1' },
62
65
  ...(cwd === undefined ? {} : { cwd })
63
66
  };
64
67
  const terminal = commandMode
65
- ? this.terminalManager.openCommand(options, input.command)
68
+ ? this.terminalManager.openCommand(options, input.command, input.binary === true)
66
69
  : this.terminalManager.open(options);
67
70
  if (commandMode || secretEnvironment.MAR_GIT_TOKEN !== undefined)
68
71
  this.terminalOwners.set(terminal.id, {
package/dist/terminal.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { spawn as spawnProcess } from 'node:child_process';
2
3
  import { existsSync } from 'node:fs';
3
4
  import { homedir } from 'node:os';
4
5
  import { join } from 'node:path';
5
6
  import { spawn } from 'node-pty';
6
7
  import { terminalDataFrameSchema } from '@myagentroam/protocol';
8
+ import { nodeExecutionEnvironment } from './node-execution-environment.js';
7
9
  import { terminateProcessTree } from './process-tree.js';
8
10
  const MAX_FRAME_CHARACTERS = 65_536;
9
11
  const TERMINAL_COMMAND_MAX_BYTES = 64 * 1024;
@@ -26,6 +28,7 @@ export class TerminalManager {
26
28
  idleTimeoutMs;
27
29
  now;
28
30
  ptyFactory;
31
+ pipeFactory;
29
32
  terminate;
30
33
  terminals = new Map();
31
34
  attachments = new Map();
@@ -61,6 +64,7 @@ export class TerminalManager {
61
64
  }
62
65
  this.now = options.now ?? Date.now;
63
66
  this.ptyFactory = options.ptyFactory ?? nodePtyFactory;
67
+ this.pipeFactory = options.pipeFactory ?? nodePipeFactory;
64
68
  this.terminate =
65
69
  options.terminate ?? ((child, platform) => terminateProcessTree(child, platform));
66
70
  this.idleTimer = setInterval(() => void this.closeExpired(), Math.min(this.idleTimeoutMs, 60_000));
@@ -81,13 +85,13 @@ export class TerminalManager {
81
85
  return this.openInternal(input);
82
86
  }
83
87
  /** A command starts only after a relay or direct Terminal attachment is active. */
84
- openCommand(input, command) {
88
+ openCommand(input, command, binary = false) {
85
89
  if (typeof command !== 'string' ||
86
90
  command.trim().length === 0 ||
87
91
  Buffer.byteLength(command, 'utf8') > TERMINAL_COMMAND_MAX_BYTES ||
88
92
  command.includes('\0'))
89
93
  throw new Error('TERMINAL_COMMAND_INVALID');
90
- return this.openInternal(input, command);
94
+ return this.openInternal(input, command, binary);
91
95
  }
92
96
  startCommand(terminalId) {
93
97
  const terminal = this.requireTerminal(terminalId);
@@ -96,17 +100,25 @@ export class TerminalManager {
96
100
  return;
97
101
  delete terminal.deferredCommand;
98
102
  try {
99
- const pty = this.ptyFactory(deferred.shell, deferred.args, deferred.options);
100
- terminal.pty = pty;
101
- pty.onData((data) => this.recordOutput(terminal, data));
102
- pty.onExit((event) => this.exited(terminal, event.exitCode));
103
+ if (terminal.binary) {
104
+ const pipe = this.pipeFactory(deferred.shell, deferred.args, deferred.options);
105
+ terminal.pipe = pipe;
106
+ pipe.onData((data) => this.recordBinaryOutput(terminal, data));
107
+ pipe.onExit((exitCode) => this.exited(terminal, exitCode));
108
+ }
109
+ else {
110
+ const pty = this.ptyFactory(deferred.shell, deferred.args, deferred.options);
111
+ terminal.pty = pty;
112
+ pty.onData((data) => this.recordOutput(terminal, data));
113
+ pty.onExit((event) => this.exited(terminal, event.exitCode));
114
+ }
103
115
  }
104
116
  catch {
105
117
  this.emit({ type: 'error', terminalId, code: 'TERMINAL_COMMAND_START_FAILED' });
106
118
  this.exited(terminal, 127);
107
119
  }
108
120
  }
109
- openInternal(input = {}, command) {
121
+ openInternal(input = {}, command, binary = false) {
110
122
  const profileId = input.profileId ?? this.profiles.find((profile) => profile.isDefault)?.id;
111
123
  if (typeof profileId !== 'string')
112
124
  throw new Error('TERMINAL_PROFILE_INVALID');
@@ -127,7 +139,7 @@ export class TerminalManager {
127
139
  cols,
128
140
  rows,
129
141
  cwd: typeof input.cwd === 'string' ? input.cwd : this.cwd,
130
- env: { ...process.env, ...input.environment }
142
+ env: nodeExecutionEnvironment(input.environment, process.env, { platform: this.platform })
131
143
  };
132
144
  const commandArgs = command === undefined
133
145
  ? undefined
@@ -143,6 +155,8 @@ export class TerminalManager {
143
155
  workspaceId: input.workspaceId ?? null,
144
156
  profile,
145
157
  pty,
158
+ pipe: undefined,
159
+ binary,
146
160
  ...(commandArgs === undefined
147
161
  ? {}
148
162
  : { deferredCommand: { shell: profile.shell, args: commandArgs, options } }),
@@ -252,12 +266,23 @@ export class TerminalManager {
252
266
  terminal.updatedAt = this.now();
253
267
  terminal.lastUsedAt = terminal.updatedAt;
254
268
  if (parsed.data.type === 'input') {
269
+ if (terminal.binary) {
270
+ if (terminal.pipe === undefined)
271
+ throw new Error('TERMINAL_COMMAND_NOT_STARTED');
272
+ const bytes = Buffer.from(parsed.data.data, 'base64');
273
+ if (bytes.length > 32 * 1024 || bytes.toString('base64') !== parsed.data.data)
274
+ throw new Error('TERMINAL_FRAME_INVALID');
275
+ terminal.pipe.write(bytes);
276
+ return;
277
+ }
255
278
  if (terminal.pty === undefined)
256
279
  throw new Error('TERMINAL_COMMAND_NOT_STARTED');
257
280
  terminal.pty.write(parsed.data.data);
258
281
  return;
259
282
  }
260
283
  if (parsed.data.type === 'resize') {
284
+ if (terminal.binary)
285
+ throw new Error('TERMINAL_FRAME_INVALID');
261
286
  if (terminal.pty === undefined)
262
287
  throw new Error('TERMINAL_COMMAND_NOT_STARTED');
263
288
  terminal.pty.resize(parsed.data.cols, parsed.data.rows);
@@ -279,6 +304,8 @@ export class TerminalManager {
279
304
  try {
280
305
  if (terminal.pty !== undefined)
281
306
  await this.terminate(terminal.pty, this.platform);
307
+ if (terminal.pipe !== undefined)
308
+ await this.terminate(terminal.pipe, this.platform);
282
309
  }
283
310
  finally {
284
311
  this.emit({ type: 'exit', terminalId: terminal.id, exitCode: terminal.exitCode });
@@ -331,6 +358,31 @@ export class TerminalManager {
331
358
  });
332
359
  }
333
360
  }
361
+ recordBinaryOutput(terminal, data) {
362
+ if (terminal.state !== 'RUNNING')
363
+ return;
364
+ for (let offset = 0; offset < data.length; offset += 32 * 1024) {
365
+ const part = data.subarray(offset, offset + 32 * 1024);
366
+ const frame = {
367
+ sequence: ++terminal.sequence,
368
+ data: part.toString('base64'),
369
+ bytes: part.length
370
+ };
371
+ terminal.replay.push(frame);
372
+ terminal.replayBytes += frame.bytes;
373
+ while (terminal.replayBytes > this.maxReplayBytes && terminal.replay.length > 0) {
374
+ const removed = terminal.replay.shift();
375
+ if (removed !== undefined)
376
+ terminal.replayBytes -= removed.bytes;
377
+ }
378
+ this.emit({
379
+ type: 'output',
380
+ terminalId: terminal.id,
381
+ sequence: frame.sequence,
382
+ data: frame.data
383
+ });
384
+ }
385
+ }
334
386
  exited(terminal, exitCode) {
335
387
  if (terminal.state !== 'RUNNING')
336
388
  return;
@@ -433,6 +485,33 @@ function nodePtyFactory(shell, args, options) {
433
485
  });
434
486
  return pty;
435
487
  }
488
+ function nodePipeFactory(shell, args, options) {
489
+ const child = spawnProcess(shell, [...args], {
490
+ cwd: options.cwd,
491
+ env: options.env,
492
+ stdio: ['pipe', 'pipe', 'pipe'],
493
+ detached: process.platform !== 'win32',
494
+ windowsHide: true
495
+ });
496
+ child.stdin.on('error', () => undefined);
497
+ child.stderr.resume();
498
+ return {
499
+ pid: child.pid,
500
+ kill: (signal) => child.kill(signal),
501
+ write: (bytes) => {
502
+ if (child.stdin.destroyed)
503
+ throw new Error('TERMINAL_PIPE_CLOSED');
504
+ child.stdin.write(bytes);
505
+ },
506
+ onData: (listener) => {
507
+ child.stdout.on('data', listener);
508
+ },
509
+ onExit: (listener) => {
510
+ child.on('error', () => listener(127));
511
+ child.on('close', (exitCode) => listener(exitCode ?? 1));
512
+ }
513
+ };
514
+ }
436
515
  export function defaultTerminalProfiles(platform, findWindowsExecutable = resolveWindowsExecutable) {
437
516
  if (platform === 'win32') {
438
517
  const pwsh = findWindowsExecutable('pwsh.exe');
@@ -182,6 +182,11 @@ export function nativeConversationPage(session, history, input, runtimeTurns = [
182
182
  const startedAt = items[0]?.startedAt ?? null;
183
183
  const completedAt = items.at(-1)?.completedAt ?? startedAt;
184
184
  const after = lastNativeItemId(entries);
185
+ const nativeIndex = start + relativeIndex;
186
+ const turnBoundaryAfter = nativeUser?.kind === 'message' ? nativeUser.nativeTurnId : undefined;
187
+ const previousNativeUser = nativeTurns[nativeIndex - 1]?.find(({ entry }) => entry.kind === 'message' && entry.role === 'USER')?.entry;
188
+ const turnBoundaryBefore = previousNativeUser?.kind === 'message' ? previousNativeUser.nativeTurnId : undefined;
189
+ const firstNativeTurn = nativeIndex === 0 && history.truncated !== true;
185
190
  return {
186
191
  id: turnId,
187
192
  sessionId: session.id,
@@ -199,6 +204,10 @@ export function nativeConversationPage(session, history, input, runtimeTurns = [
199
204
  after
200
205
  }
201
206
  }),
207
+ ...(turnBoundaryAfter === undefined ||
208
+ (!firstNativeTurn && turnBoundaryBefore === undefined)
209
+ ? {}
210
+ : { rewind: { before: turnBoundaryBefore ?? null, after: turnBoundaryAfter } }),
202
211
  items,
203
212
  startedAt,
204
213
  completedAt
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.131",
3
+ "version": "0.9.133",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -12,7 +12,11 @@
12
12
  "NOTICE"
13
13
  ],
14
14
  "bin": {
15
- "mar-node": "./dist/main.js"
15
+ "mar-node": "./dist/main.js",
16
+ "marmgr": "./dist/marmgr.js",
17
+ "marnodes": "./dist/marnodes.js",
18
+ "marssh": "./dist/marssh.js",
19
+ "marscp": "./dist/marscp.js"
16
20
  },
17
21
  "engines": {
18
22
  "node": ">=22.13"
@@ -30,8 +34,9 @@
30
34
  "npm": "11.12.1",
31
35
  "ws": "^8.21.3",
32
36
  "zod": "4.4.3",
33
- "@myagentroam/agent": "0.9.131",
34
- "@myagentroam/protocol": "0.9.131",
37
+ "@myagentroam/marmgr": "0.9.116",
38
+ "@myagentroam/agent": "0.9.133",
39
+ "@myagentroam/protocol": "0.9.133",
35
40
  "@myagentroam/code-navigation": "0.9.129"
36
41
  },
37
42
  "devDependencies": {