@myagentroam/node 0.9.130 → 0.9.132

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
@@ -263,6 +263,7 @@ export class NodeConnector {
263
263
  goalSubmissionAttempts = new Map();
264
264
  goalContinuationEpochs = new Map();
265
265
  goalExecutionProgress = new Set();
266
+ nativeGoalBindings = new Map();
266
267
  sessionScheduleService;
267
268
  goalMcpServer = new GoalMcpServer(this.goalRuntime, (sessionId) => {
268
269
  const goal = this.goalRuntime.get(sessionId);
@@ -270,6 +271,7 @@ export class NodeConnector {
270
271
  if (goal === undefined || session === undefined)
271
272
  return;
272
273
  if (goal.status !== 'active') {
274
+ this.nativeGoalBindings.delete(goal.id);
273
275
  this.retireGoalContinuation(sessionId);
274
276
  this.continuousRunCoordinator.removeGoalQueue(sessionId);
275
277
  }
@@ -384,11 +386,9 @@ export class NodeConnector {
384
386
  failed: status === 'FAILED',
385
387
  progressed: status !== 'SUCCEEDED' || progressed
386
388
  });
387
- if (disposition === 'blocked' || disposition === 'paused') {
388
- if (disposition === 'blocked')
389
- this.retireGoalContinuation(pending.sessionId);
390
- else
391
- this.invalidateGoalContinuation(pending.sessionId);
389
+ if (disposition === 'blocked' || disposition === 'completed') {
390
+ this.nativeGoalBindings.delete(pending.goal.id);
391
+ this.retireGoalContinuation(pending.sessionId);
392
392
  this.continuousRunCoordinator.removeGoalQueue(pending.sessionId);
393
393
  const terminalGoal = this.goalRuntime.get(pending.sessionId);
394
394
  const session = this.runtime.getAgentSession(pending.sessionId);
@@ -528,6 +528,7 @@ export class NodeConnector {
528
528
  promoteSession: (sessionId, nativeSessionId) => this.sessionIdentityService.promote(sessionId, nativeSessionId),
529
529
  send: (type, payload) => this.send(type, payload),
530
530
  emit: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
531
+ nativeGoalUpdated: (runId, goal) => this.handleNativeGoalUpdate(runId, goal),
531
532
  cancel: (runId, threadId, turnId, cwd, status) => this.runnerInteractionService.cancelCodexTurn(runId, threadId, turnId, cwd, status),
532
533
  createApproval: (input) => this.runtime.createApproval(input),
533
534
  publishApproval: (input) => this.publishApprovalForRun(input),
@@ -985,6 +986,9 @@ export class NodeConnector {
985
986
  goalAlreadyActive: true
986
987
  };
987
988
  if (existing !== undefined) {
989
+ const previousGoal = this.goalRuntime.get(session.id);
990
+ if (previousGoal !== undefined)
991
+ this.nativeGoalBindings.delete(previousGoal.id);
988
992
  this.goalRuntime.clear(session.id);
989
993
  this.commandStates.clear(session.id, 'goal');
990
994
  }
@@ -1476,7 +1480,8 @@ export class NodeConnector {
1476
1480
  this.operationRouter.register('node.upgrade.request', (data) => this.upgradeCoordinator.request(data));
1477
1481
  this.operationRouter.register('node.upgrade.cancel', () => this.upgradeCoordinator.cancel());
1478
1482
  this.operationRouter.register('node.workbench.capabilities', async () => ({
1479
- manifest: await this.workbenchManifestService.read()
1483
+ manifest: await this.workbenchManifestService.read(),
1484
+ binaryTerminal: true
1480
1485
  }));
1481
1486
  }
1482
1487
  async initializeConversationHosting() {
@@ -1720,10 +1725,46 @@ export class NodeConnector {
1720
1725
  const current = this.goalRuntime.get(sessionId);
1721
1726
  return ((this.goalContinuationEpochs.get(sessionId) ?? 0) === goal.epoch &&
1722
1727
  current?.status === 'active' &&
1723
- !current.continuationPaused &&
1724
1728
  current.id === goal.id &&
1725
1729
  current.revision === goal.revision);
1726
1730
  }
1731
+ handleNativeGoalUpdate(runId, nativeGoal) {
1732
+ const pending = this.continuousRunCoordinator.pending(runId);
1733
+ if (pending?.goal === undefined || !this.goalGenerationCurrent(pending.sessionId, pending.goal))
1734
+ return;
1735
+ const goal = this.goalRuntime.get(pending.sessionId);
1736
+ if (nativeGoal.status === 'active') {
1737
+ if ((nativeGoal.createdAt + 1) * 1_000 < goal.revisionStartedAt)
1738
+ return;
1739
+ const previous = this.nativeGoalBindings.get(goal.id);
1740
+ if (previous !== undefined &&
1741
+ previous.revision !== goal.revision &&
1742
+ previous.threadId === nativeGoal.threadId &&
1743
+ previous.createdAt === nativeGoal.createdAt &&
1744
+ previous.objective === nativeGoal.objective)
1745
+ return;
1746
+ this.nativeGoalBindings.set(goal.id, {
1747
+ revision: goal.revision,
1748
+ threadId: nativeGoal.threadId,
1749
+ createdAt: nativeGoal.createdAt,
1750
+ objective: nativeGoal.objective
1751
+ });
1752
+ return;
1753
+ }
1754
+ const bound = this.nativeGoalBindings.get(goal.id);
1755
+ if (bound?.revision !== goal.revision ||
1756
+ bound.threadId !== nativeGoal.threadId ||
1757
+ bound.createdAt !== nativeGoal.createdAt ||
1758
+ bound.objective !== nativeGoal.objective)
1759
+ return;
1760
+ this.nativeGoalBindings.delete(goal.id);
1761
+ const completed = this.goalRuntime.update(pending.sessionId, goal.id, goal.revision, 'completed');
1762
+ this.retireGoalContinuation(pending.sessionId);
1763
+ this.continuousRunCoordinator.removeGoalQueue(pending.sessionId);
1764
+ const session = this.runtime.getAgentSession(pending.sessionId);
1765
+ if (session !== undefined)
1766
+ this.publishGoalState(session, completed);
1767
+ }
1727
1768
  beginGoalSubmission(sessionId, continuationEpoch = this.goalContinuationEpochs.get(sessionId) ?? 0) {
1728
1769
  const attempt = { sessionId, epoch: continuationEpoch };
1729
1770
  const attempts = this.goalSubmissionAttempts.get(sessionId) ?? new Set();
@@ -1814,6 +1855,7 @@ export class NodeConnector {
1814
1855
  this.clearGoalRetry(attempt.sessionId);
1815
1856
  if (failedAttempts > 3) {
1816
1857
  const blocked = this.goalRuntime.blockAfterRuntimeFailure(attempt.sessionId, input.goal.id, input.goal.revision);
1858
+ this.nativeGoalBindings.delete(input.goal.id);
1817
1859
  this.continuousRunCoordinator.removeGoalQueue(attempt.sessionId);
1818
1860
  this.goalContinuationEpochs.delete(attempt.sessionId);
1819
1861
  this.finishIdleGoalRun(attempt.sessionId, 'FAILED');
@@ -1861,6 +1903,9 @@ export class NodeConnector {
1861
1903
  }
1862
1904
  clearGoalState(sessionId, status) {
1863
1905
  const goalExit = this.goalRuntime.exitSummary(sessionId, status);
1906
+ const goal = this.goalRuntime.get(sessionId);
1907
+ if (goal !== undefined)
1908
+ this.nativeGoalBindings.delete(goal.id);
1864
1909
  this.retireGoalContinuation(sessionId);
1865
1910
  const removed = this.goalRuntime.clear(sessionId);
1866
1911
  this.continuousRunCoordinator.removeGoalQueue(sessionId);
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 {};
@@ -166,6 +166,15 @@ export class AbstractRunner {
166
166
  function resolvedMcpServers(raw, secrets) {
167
167
  if (!Array.isArray(raw))
168
168
  return [];
169
+ const marmgrEnvironment = {
170
+ ...(secrets['MAR_MGR_SERVER_URL'] === undefined
171
+ ? {}
172
+ : { MAR_MGR_SERVER_URL: secrets['MAR_MGR_SERVER_URL'] }),
173
+ ...(secrets['MAR_MGR_USER_TOKEN'] === undefined
174
+ ? {}
175
+ : { MAR_MGR_USER_TOKEN: secrets['MAR_MGR_USER_TOKEN'] }),
176
+ MAR_MGR_CONTEXT_REQUIRED: '1'
177
+ };
169
178
  const result = [];
170
179
  for (const value of raw) {
171
180
  if (value === null || typeof value !== 'object' || Array.isArray(value))
@@ -191,17 +200,20 @@ function resolvedMcpServers(raw, secrets) {
191
200
  environment: record['__marGoal'] === true
192
201
  ? {
193
202
  MAR_GOAL_MCP_ENDPOINT: record['endpoint'],
194
- MAR_GOAL_MCP_TOKEN: secrets[record['secretName']]
203
+ MAR_GOAL_MCP_TOKEN: secrets[record['secretName']],
204
+ ...marmgrEnvironment
195
205
  }
196
206
  : record['__marScheduled'] === true
197
207
  ? {
198
208
  MAR_SCHEDULE_MCP_ENDPOINT: record['endpoint'],
199
209
  MAR_SCHEDULE_MCP_TOKEN: secrets[record['secretName']],
200
- MAR_SCHEDULE_MCP_READ_ONLY: record['readOnly'] === true ? '1' : '0'
210
+ MAR_SCHEDULE_MCP_READ_ONLY: record['readOnly'] === true ? '1' : '0',
211
+ ...marmgrEnvironment
201
212
  }
202
213
  : {
203
214
  MAR_MAIL_MCP_ENDPOINT: record['endpoint'],
204
- MAR_MAIL_MCP_TOKEN: secrets[record['secretName']]
215
+ MAR_MAIL_MCP_TOKEN: secrets[record['secretName']],
216
+ ...marmgrEnvironment
205
217
  }
206
218
  });
207
219
  continue;
@@ -241,7 +253,8 @@ function resolvedMcpServers(raw, secrets) {
241
253
  ],
242
254
  environment: {
243
255
  ...runtimeEnvironment,
244
- ...configuredEnvironment
256
+ ...configuredEnvironment,
257
+ ...marmgrEnvironment
245
258
  }
246
259
  });
247
260
  }
@@ -170,7 +170,7 @@ export class ClaudeManagedRunController {
170
170
  ...(typeof payload.model === 'string' ? { model: payload.model } : {}),
171
171
  ...(typeof payload.effort === 'string' ? { effort: payload.effort } : {}),
172
172
  ...(typeof payload.access === 'string' ? { access: payload.access } : {}),
173
- environment: secretEnvironment,
173
+ environment: { ...secretEnvironment, MAR_MGR_CONTEXT_REQUIRED: '1' },
174
174
  ...(typeof payload.personalInstructions === 'string'
175
175
  ? { personalInstructions: payload.personalInstructions }
176
176
  : {}),
@@ -142,6 +142,27 @@ export class CodexManagedRunController {
142
142
  this.host.emit(runId, 'run.running', { threadId, turnId }, 'RUNNING');
143
143
  return;
144
144
  }
145
+ if (notification.method === 'thread/goal/updated' &&
146
+ activeRun.goalMcp === true &&
147
+ turnId !== undefined &&
148
+ threadId !== undefined &&
149
+ isPlainRecord(params.goal) &&
150
+ params.goal.threadId === threadId &&
151
+ Number.isSafeInteger(params.goal.createdAt) &&
152
+ params.goal.createdAt > 0 &&
153
+ typeof params.goal.objective === 'string' &&
154
+ params.goal.objective.length <= 20_000 &&
155
+ (params.goal.status !== 'active' ||
156
+ (params.goal.tokensUsed === 0 && params.goal.timeUsedSeconds === 0)) &&
157
+ (params.goal.status === 'active' || params.goal.status === 'complete')) {
158
+ this.host.nativeGoalUpdated(runId, {
159
+ threadId,
160
+ createdAt: params.goal.createdAt,
161
+ objective: params.goal.objective,
162
+ status: params.goal.status === 'complete' ? 'completed' : 'active'
163
+ });
164
+ return;
165
+ }
145
166
  if (notification.requestId !== undefined &&
146
167
  (notification.method === 'item/commandExecution/requestApproval' ||
147
168
  notification.method === 'item/fileChange/requestApproval')) {
@@ -364,7 +385,7 @@ export class CodexManagedRunController {
364
385
  return;
365
386
  }
366
387
  try {
367
- this.runner.prepareEnvironment(secretEnvironment, runId);
388
+ this.runner.prepareEnvironment({ ...secretEnvironment, MAR_MGR_CONTEXT_REQUIRED: '1' }, runId);
368
389
  }
369
390
  catch (error) {
370
391
  this.host.emit(runId, 'run.rejected', { code: safeErrorCode(error, 'SECRET_ENVIRONMENT_INVALID') }, 'FAILED');
@@ -686,7 +686,7 @@ export function allowsUntrustedWebFetchTls(environment) {
686
686
  return environment['MAR_AGENT_WEB_FETCH_ALLOW_UNTRUSTED_TLS'] !== '0';
687
687
  }
688
688
  async function resolveMarAgentHostRuntimeConfiguration(secretEnvironment = {}) {
689
- const environment = { ...process.env, ...secretEnvironment };
689
+ const environment = { ...process.env, ...secretEnvironment, MAR_MGR_CONTEXT_REQUIRED: '1' };
690
690
  const ripgrep = await availableBundledRipgrep();
691
691
  return {
692
692
  environment: ripgrep === undefined ? environment : prependBundledRipgrepToPath(environment, ripgrep),
@@ -108,7 +108,10 @@ export class OpenCodeManagedRunController {
108
108
  let mcp;
109
109
  let attachments;
110
110
  try {
111
- environment = parseSecretEnvironment(payload.secretEnvironment);
111
+ environment = {
112
+ ...parseSecretEnvironment(payload.secretEnvironment),
113
+ MAR_MGR_CONTEXT_REQUIRED: '1'
114
+ };
112
115
  mcpSecretEnvironment = parseSecretEnvironment(payload.mcpSecretEnvironment);
113
116
  mcp = this.runner.mcpConfiguration(payload.mcpInstallations, {
114
117
  ...environment,
@@ -102,18 +102,19 @@ export class GoalRuntime {
102
102
  throw new Error('GOAL_OBJECTIVE_INVALID');
103
103
  const current = this.goals.get(sessionId);
104
104
  const continuing = current?.status === 'active';
105
+ const now = this.now();
105
106
  const goal = {
106
107
  id: continuing ? current.id : randomUUID(),
107
108
  sessionId,
108
109
  objective: normalized,
109
- startedAt: continuing ? current.startedAt : this.now(),
110
+ startedAt: continuing ? current.startedAt : now,
111
+ revisionStartedAt: now,
110
112
  revision: continuing ? current.revision + 1 : 1,
111
113
  status: 'active',
112
114
  turns: 0,
113
115
  retryRequired: false,
114
116
  retryStartedAtTurn: null,
115
- consecutiveNoProgressTurns: 0,
116
- continuationPaused: false
117
+ consecutiveNoProgressTurns: 0
117
118
  };
118
119
  this.goals.set(sessionId, goal);
119
120
  this.executionFailures.delete(sessionId);
@@ -144,34 +145,32 @@ export class GoalRuntime {
144
145
  if (!input.failed && !input.progressed) {
145
146
  this.executionFailures.delete(sessionId);
146
147
  const consecutiveNoProgressTurns = current.consecutiveNoProgressTurns + 1;
147
- const paused = consecutiveNoProgressTurns >= 3;
148
+ const completed = consecutiveNoProgressTurns >= 3;
148
149
  const goal = {
149
150
  ...current,
150
151
  consecutiveNoProgressTurns,
151
- continuationPaused: paused
152
+ ...(completed ? { status: 'completed' } : {})
152
153
  };
153
154
  this.goals.set(sessionId, goal);
154
155
  this.changed(goal);
155
- return paused ? 'paused' : 'continue';
156
+ return completed ? 'completed' : 'continue';
156
157
  }
157
158
  if (!input.failed) {
158
159
  this.executionFailures.delete(sessionId);
159
- if (current.consecutiveNoProgressTurns > 0 || current.continuationPaused) {
160
+ if (current.consecutiveNoProgressTurns > 0) {
160
161
  const goal = {
161
162
  ...current,
162
- consecutiveNoProgressTurns: 0,
163
- continuationPaused: false
163
+ consecutiveNoProgressTurns: 0
164
164
  };
165
165
  this.goals.set(sessionId, goal);
166
166
  this.changed(goal);
167
167
  }
168
168
  return 'continue';
169
169
  }
170
- if (current.consecutiveNoProgressTurns > 0 || current.continuationPaused) {
170
+ if (current.consecutiveNoProgressTurns > 0) {
171
171
  const goal = {
172
172
  ...current,
173
- consecutiveNoProgressTurns: 0,
174
- continuationPaused: false
173
+ consecutiveNoProgressTurns: 0
175
174
  };
176
175
  this.goals.set(sessionId, goal);
177
176
  }
@@ -191,12 +190,11 @@ export class GoalRuntime {
191
190
  const current = this.goals.get(sessionId);
192
191
  if (current?.status !== 'active')
193
192
  return current;
194
- if (!current.continuationPaused && current.consecutiveNoProgressTurns === 0)
193
+ if (current.consecutiveNoProgressTurns === 0)
195
194
  return current;
196
195
  const goal = {
197
196
  ...current,
198
- consecutiveNoProgressTurns: 0,
199
- continuationPaused: false
197
+ consecutiveNoProgressTurns: 0
200
198
  };
201
199
  this.goals.set(sessionId, goal);
202
200
  this.changed(goal);
@@ -247,7 +245,7 @@ export class GoalRuntime {
247
245
  const heading = kind === 'start' ? 'A Goal has started or changed.' : 'Continue the active Goal.';
248
246
  const open = kind === 'start' ? START_OPEN : CONTINUE_OPEN;
249
247
  const close = kind === 'start' ? START_CLOSE : CONTINUE_CLOSE;
250
- return `${open}\n${heading}\n\nOriginal objective:\n${goal.objective}\n\nContinue executing this Goal across turns:\n- Work on the objective now. If it is incomplete, use tools to finish a coherent unit or obtain decisive evidence; do not stop at an acknowledgement, status, plan, or promise.\n- Preserve the original scope. Before completion, review the full objective and verify it with real evidence, then call mar_update_goal with status=completed.\n- Adapt and retry after individual failures. Request blocked only after the same blocker persists for three turns and the required additional retry.\n- Use mar_get_goal only when you need to reread the authoritative objective or status.\n- After mar_update_goal confirms completed or finally blocked, summarize the completed work, verification evidence, remaining issues, and the tool-reported elapsedWallMs converted from milliseconds into a concise readable duration in your final user-facing response. Name whether the Goal completed or is finally blocked. That elapsed time is wall-clock time through the status update, including waiting and retries, not tool execution time or time through this final response. If blocked returns active for the required retry, continue work instead of claiming a final summary.\n${close}`;
248
+ return `${open}\n${heading}\n\nOriginal objective:\n${goal.objective}\n\nContinue executing this Goal across turns:\n- This Goal is owned by the Node's myagentroam_goal MCP server. Prefer myagentroam_goal.mar_update_goal to complete it. In Codex, a runner-native Goal created during this Goal may also complete it when Codex reports that native Goal complete; native Goal state from another task or turn does not count. Do not claim completion based only on a final message or an unrelated Goal.\n- Work on the objective now. If it is incomplete, use tools to finish a coherent unit or obtain decisive evidence; do not stop at an acknowledgement, status, plan, or promise.\n- Preserve the original scope. Before completion, review the full objective and verify it with real evidence, then call mar_update_goal with status=completed when available.\n- Adapt and retry after individual failures. Request blocked only after the same blocker persists for three turns and the required additional retry.\n- Use mar_get_goal only when you need to reread the authoritative objective or status.\n- After mar_update_goal confirms completed or finally blocked, summarize the completed work, verification evidence, remaining issues, and the tool-reported elapsedWallMs converted from milliseconds into a concise readable duration in your final user-facing response. If Codex instead confirms completion with its native Goal, summarize the work without claiming a MAR elapsedWallMs value. Name whether the Goal completed or is finally blocked. MAR elapsedWallMs includes waiting and retries through the status update, not tool execution time or the final response. If blocked returns active for the required retry, continue work instead of claiming a final summary.\n${close}`;
251
249
  }
252
250
  requireCurrent(sessionId, goalId, revision) {
253
251
  const current = this.goals.get(sessionId);
@@ -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,4 +1,5 @@
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';
@@ -26,6 +27,7 @@ export class TerminalManager {
26
27
  idleTimeoutMs;
27
28
  now;
28
29
  ptyFactory;
30
+ pipeFactory;
29
31
  terminate;
30
32
  terminals = new Map();
31
33
  attachments = new Map();
@@ -61,6 +63,7 @@ export class TerminalManager {
61
63
  }
62
64
  this.now = options.now ?? Date.now;
63
65
  this.ptyFactory = options.ptyFactory ?? nodePtyFactory;
66
+ this.pipeFactory = options.pipeFactory ?? nodePipeFactory;
64
67
  this.terminate =
65
68
  options.terminate ?? ((child, platform) => terminateProcessTree(child, platform));
66
69
  this.idleTimer = setInterval(() => void this.closeExpired(), Math.min(this.idleTimeoutMs, 60_000));
@@ -81,13 +84,13 @@ export class TerminalManager {
81
84
  return this.openInternal(input);
82
85
  }
83
86
  /** A command starts only after a relay or direct Terminal attachment is active. */
84
- openCommand(input, command) {
87
+ openCommand(input, command, binary = false) {
85
88
  if (typeof command !== 'string' ||
86
89
  command.trim().length === 0 ||
87
90
  Buffer.byteLength(command, 'utf8') > TERMINAL_COMMAND_MAX_BYTES ||
88
91
  command.includes('\0'))
89
92
  throw new Error('TERMINAL_COMMAND_INVALID');
90
- return this.openInternal(input, command);
93
+ return this.openInternal(input, command, binary);
91
94
  }
92
95
  startCommand(terminalId) {
93
96
  const terminal = this.requireTerminal(terminalId);
@@ -96,17 +99,25 @@ export class TerminalManager {
96
99
  return;
97
100
  delete terminal.deferredCommand;
98
101
  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));
102
+ if (terminal.binary) {
103
+ const pipe = this.pipeFactory(deferred.shell, deferred.args, deferred.options);
104
+ terminal.pipe = pipe;
105
+ pipe.onData((data) => this.recordBinaryOutput(terminal, data));
106
+ pipe.onExit((exitCode) => this.exited(terminal, exitCode));
107
+ }
108
+ else {
109
+ const pty = this.ptyFactory(deferred.shell, deferred.args, deferred.options);
110
+ terminal.pty = pty;
111
+ pty.onData((data) => this.recordOutput(terminal, data));
112
+ pty.onExit((event) => this.exited(terminal, event.exitCode));
113
+ }
103
114
  }
104
115
  catch {
105
116
  this.emit({ type: 'error', terminalId, code: 'TERMINAL_COMMAND_START_FAILED' });
106
117
  this.exited(terminal, 127);
107
118
  }
108
119
  }
109
- openInternal(input = {}, command) {
120
+ openInternal(input = {}, command, binary = false) {
110
121
  const profileId = input.profileId ?? this.profiles.find((profile) => profile.isDefault)?.id;
111
122
  if (typeof profileId !== 'string')
112
123
  throw new Error('TERMINAL_PROFILE_INVALID');
@@ -143,6 +154,8 @@ export class TerminalManager {
143
154
  workspaceId: input.workspaceId ?? null,
144
155
  profile,
145
156
  pty,
157
+ pipe: undefined,
158
+ binary,
146
159
  ...(commandArgs === undefined
147
160
  ? {}
148
161
  : { deferredCommand: { shell: profile.shell, args: commandArgs, options } }),
@@ -252,12 +265,23 @@ export class TerminalManager {
252
265
  terminal.updatedAt = this.now();
253
266
  terminal.lastUsedAt = terminal.updatedAt;
254
267
  if (parsed.data.type === 'input') {
268
+ if (terminal.binary) {
269
+ if (terminal.pipe === undefined)
270
+ throw new Error('TERMINAL_COMMAND_NOT_STARTED');
271
+ const bytes = Buffer.from(parsed.data.data, 'base64');
272
+ if (bytes.length > 32 * 1024 || bytes.toString('base64') !== parsed.data.data)
273
+ throw new Error('TERMINAL_FRAME_INVALID');
274
+ terminal.pipe.write(bytes);
275
+ return;
276
+ }
255
277
  if (terminal.pty === undefined)
256
278
  throw new Error('TERMINAL_COMMAND_NOT_STARTED');
257
279
  terminal.pty.write(parsed.data.data);
258
280
  return;
259
281
  }
260
282
  if (parsed.data.type === 'resize') {
283
+ if (terminal.binary)
284
+ throw new Error('TERMINAL_FRAME_INVALID');
261
285
  if (terminal.pty === undefined)
262
286
  throw new Error('TERMINAL_COMMAND_NOT_STARTED');
263
287
  terminal.pty.resize(parsed.data.cols, parsed.data.rows);
@@ -279,6 +303,8 @@ export class TerminalManager {
279
303
  try {
280
304
  if (terminal.pty !== undefined)
281
305
  await this.terminate(terminal.pty, this.platform);
306
+ if (terminal.pipe !== undefined)
307
+ await this.terminate(terminal.pipe, this.platform);
282
308
  }
283
309
  finally {
284
310
  this.emit({ type: 'exit', terminalId: terminal.id, exitCode: terminal.exitCode });
@@ -331,6 +357,31 @@ export class TerminalManager {
331
357
  });
332
358
  }
333
359
  }
360
+ recordBinaryOutput(terminal, data) {
361
+ if (terminal.state !== 'RUNNING')
362
+ return;
363
+ for (let offset = 0; offset < data.length; offset += 32 * 1024) {
364
+ const part = data.subarray(offset, offset + 32 * 1024);
365
+ const frame = {
366
+ sequence: ++terminal.sequence,
367
+ data: part.toString('base64'),
368
+ bytes: part.length
369
+ };
370
+ terminal.replay.push(frame);
371
+ terminal.replayBytes += frame.bytes;
372
+ while (terminal.replayBytes > this.maxReplayBytes && terminal.replay.length > 0) {
373
+ const removed = terminal.replay.shift();
374
+ if (removed !== undefined)
375
+ terminal.replayBytes -= removed.bytes;
376
+ }
377
+ this.emit({
378
+ type: 'output',
379
+ terminalId: terminal.id,
380
+ sequence: frame.sequence,
381
+ data: frame.data
382
+ });
383
+ }
384
+ }
334
385
  exited(terminal, exitCode) {
335
386
  if (terminal.state !== 'RUNNING')
336
387
  return;
@@ -433,6 +484,33 @@ function nodePtyFactory(shell, args, options) {
433
484
  });
434
485
  return pty;
435
486
  }
487
+ function nodePipeFactory(shell, args, options) {
488
+ const child = spawnProcess(shell, [...args], {
489
+ cwd: options.cwd,
490
+ env: options.env,
491
+ stdio: ['pipe', 'pipe', 'pipe'],
492
+ detached: process.platform !== 'win32',
493
+ windowsHide: true
494
+ });
495
+ child.stdin.on('error', () => undefined);
496
+ child.stderr.resume();
497
+ return {
498
+ pid: child.pid,
499
+ kill: (signal) => child.kill(signal),
500
+ write: (bytes) => {
501
+ if (child.stdin.destroyed)
502
+ throw new Error('TERMINAL_PIPE_CLOSED');
503
+ child.stdin.write(bytes);
504
+ },
505
+ onData: (listener) => {
506
+ child.stdout.on('data', listener);
507
+ },
508
+ onExit: (listener) => {
509
+ child.on('error', () => listener(127));
510
+ child.on('close', (exitCode) => listener(exitCode ?? 1));
511
+ }
512
+ };
513
+ }
436
514
  export function defaultTerminalProfiles(platform, findWindowsExecutable = resolveWindowsExecutable) {
437
515
  if (platform === 'win32') {
438
516
  const pwsh = findWindowsExecutable('pwsh.exe');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.130",
3
+ "version": "0.9.132",
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,9 +34,10 @@
30
34
  "npm": "11.12.1",
31
35
  "ws": "^8.21.3",
32
36
  "zod": "4.4.3",
33
- "@myagentroam/protocol": "0.9.130",
34
- "@myagentroam/code-navigation": "0.9.129",
35
- "@myagentroam/agent": "0.9.130"
37
+ "@myagentroam/marmgr": "0.9.115",
38
+ "@myagentroam/agent": "0.9.132",
39
+ "@myagentroam/protocol": "0.9.132",
40
+ "@myagentroam/code-navigation": "0.9.129"
36
41
  },
37
42
  "devDependencies": {
38
43
  "@types/ws": "^8.18.1"