@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 +2 -1
- package/dist/marmgr.js +5 -0
- package/dist/marnodes.js +4 -0
- package/dist/marscp.js +4 -0
- package/dist/marssh.js +4 -0
- package/dist/native-session-history.js +18 -1
- package/dist/node-execution-environment.js +34 -0
- package/dist/runner/abstract-runner.js +15 -4
- package/dist/runner/claude/managed-run-controller.js +5 -1
- package/dist/runner/codex/managed-run-controller.js +3 -2
- package/dist/runner/mar-agent/managed-run-controller.js +5 -1
- package/dist/runner/mar-agent-runner.js +24 -12
- package/dist/runner/opencode/managed-run-controller.js +6 -2
- package/dist/service/conversation-history-service.js +10 -8
- package/dist/service/terminal-service.js +6 -3
- package/dist/terminal.js +87 -8
- package/dist/util/runner-native-session-parsers.js +9 -0
- package/package.json +9 -4
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
package/dist/marnodes.js
ADDED
package/dist/marscp.js
ADDED
package/dist/marssh.js
ADDED
|
@@ -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 =
|
|
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:
|
|
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 = {
|
|
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
|
|
224
|
+
const activeCatalog = catalog;
|
|
225
|
+
const listed = await activeCatalog.list({ limit: 200 });
|
|
225
226
|
const workspaces = workspace ? [workspace] : context.workspaces;
|
|
226
|
-
return listed.sessions.
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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 =
|
|
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
|
-
|
|
375
|
-
|
|
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
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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 =
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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:
|
|
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.
|
|
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/
|
|
34
|
-
"@myagentroam/
|
|
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": {
|