@myagentroam/node 0.11.74 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/code-navigation/index.js +2 -0
- package/dist/code-navigation/languages/c-family.js +9 -0
- package/dist/code-navigation/languages/c-preprocessor.js +30 -0
- package/dist/code-navigation/languages/c.js +8 -0
- package/dist/code-navigation/languages/cpp.js +10 -0
- package/dist/code-navigation/languages/csharp.js +18 -0
- package/dist/code-navigation/languages/css.js +7 -0
- package/dist/code-navigation/languages/ecmascript.js +44 -0
- package/dist/code-navigation/languages/file-target.js +9 -0
- package/dist/code-navigation/languages/go.js +32 -0
- package/dist/code-navigation/languages/html.js +7 -0
- package/dist/code-navigation/languages/jvm.js +17 -0
- package/dist/code-navigation/languages/less.js +7 -0
- package/dist/code-navigation/languages/markdown.js +113 -0
- package/dist/code-navigation/languages/markup-reference.js +27 -0
- package/dist/code-navigation/languages/objective-c.js +11 -0
- package/dist/code-navigation/languages/objective-cpp.js +10 -0
- package/dist/code-navigation/languages/python.js +17 -0
- package/dist/code-navigation/languages/rust.js +18 -0
- package/dist/code-navigation/languages/sass.js +7 -0
- package/dist/code-navigation/languages/scss.js +7 -0
- package/dist/code-navigation/languages/stylesheet-reference.js +37 -0
- package/dist/code-navigation/languages/vue.js +75 -0
- package/dist/code-navigation/registry.js +72 -0
- package/dist/code-navigation/source-ranges.js +17 -0
- package/dist/code-navigation/symbol-maps.js +257 -0
- package/dist/code-navigation/symbol-matcher.js +48 -0
- package/dist/code-navigation/symbol-navigation.js +1 -0
- package/dist/code-navigation/types.js +1 -0
- package/dist/ctags-binary.js +3 -3
- package/dist/marctl/bin/marctl.mjs +6 -0
- package/dist/marctl/bin/marnodes.mjs +6 -0
- package/dist/marctl/bin/marscp.mjs +6 -0
- package/dist/marctl/bin/marssh.mjs +6 -0
- package/dist/marctl/package.json +1 -0
- package/dist/marctl/src/cli.mjs +271 -0
- package/dist/marctl/src/client.mjs +291 -0
- package/dist/marctl/src/config.mjs +46 -0
- package/dist/marctl/src/direct-control.mjs +225 -0
- package/dist/marctl/src/direct-errors.mjs +48 -0
- package/dist/marctl/src/direct-process.mjs +374 -0
- package/dist/marctl/src/direct-transport.mjs +235 -0
- package/dist/marctl/src/direct.mjs +421 -0
- package/dist/marctl/src/errors.mjs +11 -0
- package/dist/marctl/src/i18n/en-US.mjs +285 -0
- package/dist/marctl/src/i18n/index.mjs +20 -0
- package/dist/marctl/src/i18n/zh-CN.mjs +269 -0
- package/dist/marctl/src/scp-channel.mjs +99 -0
- package/dist/marctl/src/scp-copy.mjs +46 -0
- package/dist/marctl/src/scp-endpoint.mjs +29 -0
- package/dist/marctl/src/scp-protocol.mjs +2 -0
- package/dist/marctl/src/scp-worker.cjs +266 -0
- package/dist/marctl/src/scp-workers.mjs +179 -0
- package/dist/marctl/src/scp.mjs +64 -0
- package/dist/marctl/src/session-args.mjs +233 -0
- package/dist/marctl/src/session-input.mjs +43 -0
- package/dist/marctl/src/session-prepare.mjs +87 -0
- package/dist/marctl/src/session-target.mjs +26 -0
- package/dist/marctl/src/session-wait.mjs +232 -0
- package/dist/marctl/src/sessions.mjs +93 -0
- package/dist/marctl/src/terminal-frame.mjs +48 -0
- package/dist/marctl/src/terminal-interaction.mjs +177 -0
- package/dist/marctl/src/terminal-race.mjs +117 -0
- package/dist/marctl/src/terminal-relay-sender.mjs +129 -0
- package/dist/marctl/src/terminal-relay.mjs +305 -0
- package/dist/marctl/src/terminal.mjs +152 -0
- package/dist/marctl.js +1 -1
- package/dist/marnodes.js +1 -1
- package/dist/marscp.js +1 -1
- package/dist/marssh.js +1 -1
- package/dist/ripgrep-binary.js +3 -3
- package/dist/runner/mar-agent/agent-operation-presentation.js +22 -10
- package/dist/runner/mar-agent/hosted-conversation-runtime.js +3 -2
- package/dist/runner/mar-agent/runtime-extensions.js +33 -0
- package/dist/runner/mar-agent/session-runtime.js +5 -4
- package/dist/service/code-navigation/file-reference-parser.js +1 -1
- package/dist/service/node-terminal-channel.js +1 -1
- package/dist/session/native-session-projection-service.js +1 -2
- package/dist/session/session-catalog-service.js +74 -25
- package/dist/workspace/workspace-code-navigation-service.js +2 -2
- package/package.json +11 -9
- package/vendor/ctags/NOTICE +0 -22
- package/vendor/ctags/darwin-arm64/ctags +0 -0
- package/vendor/ctags/darwin-x64/ctags +0 -0
- package/vendor/ctags/licenses/COPYING +0 -340
- package/vendor/ctags/licenses/COPYING.MinGW-w64-runtime.txt +0 -240
- package/vendor/ctags/licenses/LICENSE.janssen +0 -19
- package/vendor/ctags/licenses/LICENSE.libyaml +0 -19
- package/vendor/ctags/licenses/official-snapshot/darwin-arm64/COPYING +0 -340
- package/vendor/ctags/licenses/official-snapshot/darwin-arm64/RECEIPT.yml +0 -27
- package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libiconv.yml +0 -24
- package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libjansson.yml +0 -19
- package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libxml2.yml +0 -20
- package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libyaml.yml +0 -7
- package/vendor/ctags/licenses/official-snapshot/darwin-arm64/dependencies/libz.yml +0 -13
- package/vendor/ctags/licenses/official-snapshot/darwin-x64/COPYING +0 -340
- package/vendor/ctags/licenses/official-snapshot/darwin-x64/RECEIPT.yml +0 -27
- package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libiconv.yml +0 -24
- package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libjansson.yml +0 -19
- package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libxml2.yml +0 -20
- package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libyaml.yml +0 -7
- package/vendor/ctags/licenses/official-snapshot/darwin-x64/dependencies/libz.yml +0 -13
- package/vendor/ctags/licenses/official-snapshot/linux-arm64/COPYING +0 -340
- package/vendor/ctags/licenses/official-snapshot/linux-arm64/RECEIPT.yml +0 -27
- package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libiconv.yml +0 -24
- package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libjansson.yml +0 -19
- package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libxml2.yml +0 -20
- package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libyaml.yml +0 -7
- package/vendor/ctags/licenses/official-snapshot/linux-arm64/dependencies/libz.yml +0 -13
- package/vendor/ctags/licenses/official-snapshot/linux-x64/COPYING +0 -340
- package/vendor/ctags/licenses/official-snapshot/linux-x64/RECEIPT.yml +0 -27
- package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libiconv.yml +0 -24
- package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libjansson.yml +0 -19
- package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libxml2.yml +0 -20
- package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libyaml.yml +0 -7
- package/vendor/ctags/licenses/official-snapshot/linux-x64/dependencies/libz.yml +0 -13
- package/vendor/ctags/licenses/win32-x64/COPYING +0 -340
- package/vendor/ctags/licenses/win32-x64/COPYING.MinGW-w64-runtime.txt +0 -240
- package/vendor/ctags/licenses/win32-x64/Copyright.libxml2 +0 -23
- package/vendor/ctags/licenses/win32-x64/LICENCE.pcre2 +0 -94
- package/vendor/ctags/licenses/win32-x64/LICENSE.janssen +0 -19
- package/vendor/ctags/licenses/win32-x64/LICENSE.libyaml +0 -19
- package/vendor/ctags/linux-arm64/ctags +0 -0
- package/vendor/ctags/linux-x64/ctags +0 -0
- package/vendor/ctags/manifest.json +0 -42
- package/vendor/ctags/source/universal-ctags-ab95af198f8eaeb6d50dc96e3adf52e769d3e5f0.tar.gz +0 -0
- package/vendor/ctags/win32-x64/ctags.exe +0 -0
- package/vendor/ripgrep/LICENSE-MIT +0 -21
- package/vendor/ripgrep/UNLICENSE +0 -24
- package/vendor/ripgrep/darwin-arm64/rg +0 -0
- package/vendor/ripgrep/darwin-x64/rg +0 -0
- package/vendor/ripgrep/linux-arm64/rg +0 -0
- package/vendor/ripgrep/linux-x64/rg +0 -0
- package/vendor/ripgrep/manifest.json +0 -42
- package/vendor/ripgrep/win32-arm64/rg.exe +0 -0
- package/vendor/ripgrep/win32-x64/rg.exe +0 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { MarctlError } from './errors.mjs';
|
|
2
|
+
import { createTranslator } from './i18n/index.mjs';
|
|
3
|
+
import { sessionTarget } from './session-target.mjs';
|
|
4
|
+
|
|
5
|
+
const commands = {
|
|
6
|
+
current: [],
|
|
7
|
+
list: ['scope', 'query', 'limit'],
|
|
8
|
+
graph: ['limit'],
|
|
9
|
+
show: [],
|
|
10
|
+
view: [],
|
|
11
|
+
history: ['cursor', 'limit'],
|
|
12
|
+
start: [
|
|
13
|
+
'runner',
|
|
14
|
+
'model-id',
|
|
15
|
+
'effort',
|
|
16
|
+
'title',
|
|
17
|
+
'message',
|
|
18
|
+
'message-file',
|
|
19
|
+
'client-message-id',
|
|
20
|
+
'parent',
|
|
21
|
+
'goal',
|
|
22
|
+
'clear-goal'
|
|
23
|
+
],
|
|
24
|
+
config: ['model-id', 'effort'],
|
|
25
|
+
rename: ['title'],
|
|
26
|
+
'prompt.get': [],
|
|
27
|
+
'prompt.set': ['message', 'message-file'],
|
|
28
|
+
'prompt.clear': [],
|
|
29
|
+
'approval.decide': ['run', 'approval', 'decision'],
|
|
30
|
+
'schedule.list': ['cursor', 'limit'],
|
|
31
|
+
'schedule.get': [],
|
|
32
|
+
'schedule.create': ['cron', 'message', 'message-file'],
|
|
33
|
+
'schedule.update': ['cron', 'message', 'message-file', 'expected-revision'],
|
|
34
|
+
send: [
|
|
35
|
+
'message',
|
|
36
|
+
'message-file',
|
|
37
|
+
'steer',
|
|
38
|
+
'queue',
|
|
39
|
+
'client-message-id',
|
|
40
|
+
'goal',
|
|
41
|
+
'clear-goal',
|
|
42
|
+
'approval',
|
|
43
|
+
'decision'
|
|
44
|
+
],
|
|
45
|
+
stop: [],
|
|
46
|
+
'goal.get': [],
|
|
47
|
+
'goal.set': ['message', 'message-file'],
|
|
48
|
+
'goal.clear': [],
|
|
49
|
+
wait: ['timeout', 'wait-for', 'mode', 'agent', 'run', 'until']
|
|
50
|
+
};
|
|
51
|
+
const fieldNames = {
|
|
52
|
+
'model-id': 'modelId',
|
|
53
|
+
parent: 'parentSessionId',
|
|
54
|
+
'expected-revision': 'expectedRevision',
|
|
55
|
+
'message-file': 'messageFile',
|
|
56
|
+
'client-message-id': 'clientMessageId',
|
|
57
|
+
run: 'runId',
|
|
58
|
+
approval: 'approvalId',
|
|
59
|
+
timeout: 'timeoutMs',
|
|
60
|
+
'wait-for': 'waitFor',
|
|
61
|
+
'clear-goal': 'clearGoal'
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function parseSessionArgs(argv, translate = createTranslator()) {
|
|
65
|
+
const invalid = (key = 'errors.invalidArguments', details) =>
|
|
66
|
+
new MarctlError(translate(key), {
|
|
67
|
+
code: 'MARCTL_SESSION_INPUT_INVALID',
|
|
68
|
+
details:
|
|
69
|
+
details ??
|
|
70
|
+
(argv[0] === 'prompt'
|
|
71
|
+
? 'Use prompt get|clear [session-id], or prompt set [session-id] with exactly one of -m/--message or --message-file.'
|
|
72
|
+
: argv[0] === 'approval'
|
|
73
|
+
? 'Use approval decide <session-id> --run <run-id> --approval <approval-id> --decision approve|reject.'
|
|
74
|
+
: undefined)
|
|
75
|
+
});
|
|
76
|
+
if (
|
|
77
|
+
argv.length === 0 ||
|
|
78
|
+
argv.some(
|
|
79
|
+
(value, index) =>
|
|
80
|
+
(value === '--help' || value === '-h') && !['-m', '--message'].includes(argv[index - 1])
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return { command: 'agents', help: true };
|
|
84
|
+
const input = [...argv];
|
|
85
|
+
let action = input.shift();
|
|
86
|
+
if (['goal', 'schedule', 'prompt', 'approval'].includes(action)) action += '.' + input.shift();
|
|
87
|
+
if (!Object.hasOwn(commands, action)) throw invalid();
|
|
88
|
+
const args = { command: 'agents', action };
|
|
89
|
+
if (sessionTarget[action] !== 'workspace') {
|
|
90
|
+
if (
|
|
91
|
+
(input[0] === undefined || input[0].startsWith('-')) &&
|
|
92
|
+
sessionTarget[action] === 'explicit'
|
|
93
|
+
)
|
|
94
|
+
throw invalid();
|
|
95
|
+
if (input[0] !== undefined && !input[0].startsWith('-')) {
|
|
96
|
+
const sessionId = input.shift();
|
|
97
|
+
if (!sessionId || sessionId.length > 512) throw invalid();
|
|
98
|
+
args.sessionId = sessionId;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
while (input.length > 0) {
|
|
102
|
+
const rawFlag = input.shift();
|
|
103
|
+
const flag = rawFlag === '-m' ? '--message' : rawFlag;
|
|
104
|
+
if (flag === '--json' && args.json === undefined) {
|
|
105
|
+
args.json = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (action === 'send' && (flag === '--steer' || flag === '--queue')) {
|
|
109
|
+
const key = flag.slice(2);
|
|
110
|
+
if (Object.hasOwn(args, key)) throw invalid();
|
|
111
|
+
args[key] = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (flag === '--clear-goal' && ['start', 'send'].includes(action)) {
|
|
115
|
+
const key = 'clearGoal';
|
|
116
|
+
if (Object.hasOwn(args, key)) throw invalid();
|
|
117
|
+
args[key] = true;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const name = flag.startsWith('--') ? flag.slice(2) : '';
|
|
121
|
+
const key = fieldNames[name] ?? name;
|
|
122
|
+
if (
|
|
123
|
+
!commands[action].includes(name) ||
|
|
124
|
+
(!(name === 'agent' && action === 'wait') && Object.hasOwn(args, key))
|
|
125
|
+
)
|
|
126
|
+
throw invalid();
|
|
127
|
+
const value = input.shift();
|
|
128
|
+
if (
|
|
129
|
+
value === undefined ||
|
|
130
|
+
(value === '' && name !== 'effort') ||
|
|
131
|
+
(name !== 'message' && value.startsWith('--'))
|
|
132
|
+
)
|
|
133
|
+
throw invalid();
|
|
134
|
+
if (name === 'agent' && action === 'wait') {
|
|
135
|
+
(args.agentIds ??= []).push(value);
|
|
136
|
+
if (args.agentIds.length > 8) throw invalid();
|
|
137
|
+
} else if (name === 'limit' || name === 'expected-revision') {
|
|
138
|
+
if (
|
|
139
|
+
!/^[1-9][0-9]*$/u.test(value) ||
|
|
140
|
+
!Number.isSafeInteger(Number(value)) ||
|
|
141
|
+
(name === 'limit' && Number(value) > 100)
|
|
142
|
+
)
|
|
143
|
+
throw invalid();
|
|
144
|
+
args[key] = Number(value);
|
|
145
|
+
} else if (name === 'timeout') {
|
|
146
|
+
const match = /^(0|[1-9][0-9]*)(ms|s|m)$/u.exec(value);
|
|
147
|
+
const ms = match === null ? 0 : Number(match[1]) * { ms: 1, s: 1000, m: 60000 }[match[2]];
|
|
148
|
+
if (match === null || !Number.isSafeInteger(ms) || ms > 300000) throw invalid();
|
|
149
|
+
args[key] = ms;
|
|
150
|
+
} else args[key] = value;
|
|
151
|
+
}
|
|
152
|
+
if (action === 'send' && args.steer === true && args.queue === true)
|
|
153
|
+
throw invalid('errors.sessionDeliveryRequired');
|
|
154
|
+
if (
|
|
155
|
+
action === 'send' &&
|
|
156
|
+
((args.approvalId === undefined) !== (args.decision === undefined) ||
|
|
157
|
+
(args.decision !== undefined && !['approve', 'reject'].includes(args.decision)))
|
|
158
|
+
)
|
|
159
|
+
throw invalid();
|
|
160
|
+
if (args.goal !== undefined && (args.clearGoal || !args.goal.trim() || args.goal.length > 20000))
|
|
161
|
+
throw invalid();
|
|
162
|
+
if (args.message !== undefined && args.messageFile !== undefined) throw invalid();
|
|
163
|
+
if (
|
|
164
|
+
['start', 'send', 'goal.set', 'schedule.create', 'prompt.set'].includes(action) &&
|
|
165
|
+
args.messageFile === undefined &&
|
|
166
|
+
args.message === undefined &&
|
|
167
|
+
!(
|
|
168
|
+
action === 'send' &&
|
|
169
|
+
(args.goal !== undefined || args.clearGoal || args.approvalId !== undefined)
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
throw invalid();
|
|
173
|
+
if (
|
|
174
|
+
action === 'start' &&
|
|
175
|
+
args.runner !== undefined &&
|
|
176
|
+
!['mar-agent', 'codex', 'claude-code', 'opencode'].includes(args.runner)
|
|
177
|
+
)
|
|
178
|
+
throw invalid();
|
|
179
|
+
if (action === 'wait' && args.sessionId && args.agentIds?.length) throw invalid();
|
|
180
|
+
if (
|
|
181
|
+
args.until !== undefined &&
|
|
182
|
+
(args.until !== 'approval' ||
|
|
183
|
+
!args.sessionId ||
|
|
184
|
+
!args.runId ||
|
|
185
|
+
!args.timeoutMs ||
|
|
186
|
+
args.agentIds?.length)
|
|
187
|
+
)
|
|
188
|
+
throw invalid();
|
|
189
|
+
if (
|
|
190
|
+
action === 'wait' &&
|
|
191
|
+
args.runId !== undefined &&
|
|
192
|
+
(!args.sessionId ||
|
|
193
|
+
args.timeoutMs === 0 ||
|
|
194
|
+
args.agentIds?.length ||
|
|
195
|
+
args.mode !== undefined ||
|
|
196
|
+
args.waitFor !== undefined)
|
|
197
|
+
)
|
|
198
|
+
throw invalid();
|
|
199
|
+
if (args.waitFor !== undefined && !['message', 'completion'].includes(args.waitFor))
|
|
200
|
+
throw invalid();
|
|
201
|
+
if (args.mode !== undefined && !['any', 'all'].includes(args.mode)) throw invalid();
|
|
202
|
+
if (args.scope !== undefined && !['tree', 'active'].includes(args.scope)) throw invalid();
|
|
203
|
+
if (
|
|
204
|
+
action === 'approval.decide' &&
|
|
205
|
+
(!args.runId || !args.approvalId || !['approve', 'reject'].includes(args.decision))
|
|
206
|
+
)
|
|
207
|
+
throw invalid(
|
|
208
|
+
undefined,
|
|
209
|
+
'Approval decisions require --run, --approval and --decision approve|reject.'
|
|
210
|
+
);
|
|
211
|
+
if (action === 'rename' && args.title !== undefined) args.title = args.title.trim();
|
|
212
|
+
if (args.title !== undefined && args.title.length > 160) throw invalid();
|
|
213
|
+
if (args.modelId !== undefined && (args.modelId.trim().length === 0 || args.modelId.length > 120))
|
|
214
|
+
throw invalid();
|
|
215
|
+
if (args.effort !== undefined && args.effort.length > 120) throw invalid();
|
|
216
|
+
if (action === 'config' && args.modelId === undefined && args.effort === undefined)
|
|
217
|
+
throw invalid();
|
|
218
|
+
if (action === 'rename') {
|
|
219
|
+
if (args.title === undefined || args.title.trim().length === 0) throw invalid();
|
|
220
|
+
}
|
|
221
|
+
if (action === 'schedule.create' && args.cron === undefined) throw invalid();
|
|
222
|
+
if (args.cron !== undefined && (args.cron.trim().length === 0 || args.cron.length > 512))
|
|
223
|
+
throw invalid();
|
|
224
|
+
if (
|
|
225
|
+
action === 'schedule.update' &&
|
|
226
|
+
(args.expectedRevision === undefined ||
|
|
227
|
+
(args.cron === undefined && args.message === undefined && args.messageFile === undefined))
|
|
228
|
+
)
|
|
229
|
+
throw invalid();
|
|
230
|
+
if (args.query !== undefined && args.query.length > 160) throw invalid();
|
|
231
|
+
if (args.parentSessionId !== undefined && args.parentSessionId.length > 512) throw invalid();
|
|
232
|
+
return args;
|
|
233
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { open } from 'node:fs/promises';
|
|
2
|
+
import { MarctlError } from './errors.mjs';
|
|
3
|
+
|
|
4
|
+
export async function messageContent(
|
|
5
|
+
{ message, messageFile: path },
|
|
6
|
+
stdin,
|
|
7
|
+
limit,
|
|
8
|
+
translate,
|
|
9
|
+
diagnostic = false
|
|
10
|
+
) {
|
|
11
|
+
let file;
|
|
12
|
+
try {
|
|
13
|
+
let content = message;
|
|
14
|
+
if (content === undefined) {
|
|
15
|
+
const input =
|
|
16
|
+
path === '-'
|
|
17
|
+
? stdin
|
|
18
|
+
: (file = await open(path, 'r')).createReadStream({ autoClose: false });
|
|
19
|
+
const parts = [];
|
|
20
|
+
let bytes = 0;
|
|
21
|
+
for await (const part of input) {
|
|
22
|
+
const chunk = Buffer.from(part);
|
|
23
|
+
bytes += chunk.length;
|
|
24
|
+
if (bytes > limit * 4) throw new Error('MESSAGE_TOO_LONG');
|
|
25
|
+
parts.push(chunk);
|
|
26
|
+
}
|
|
27
|
+
content = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(parts));
|
|
28
|
+
}
|
|
29
|
+
if (content.trim().length === 0 || content.length > limit) throw new Error('MESSAGE_INVALID');
|
|
30
|
+
return content;
|
|
31
|
+
} catch (error) {
|
|
32
|
+
throw new MarctlError(translate('errors.sessionMessageInvalid'), {
|
|
33
|
+
code: 'MARCTL_SESSION_INPUT_INVALID',
|
|
34
|
+
...(diagnostic
|
|
35
|
+
? {
|
|
36
|
+
details: `${path === undefined ? 'message' : path === '-' ? 'stdin' : 'message-file'}: provide non-blank UTF-8 text of at most ${limit} characters.${typeof error.code === 'string' && /^[A-Z_]+$/u.test(error.code) ? ' File error: ' + error.code + '.' : ''}`
|
|
37
|
+
}
|
|
38
|
+
: {})
|
|
39
|
+
});
|
|
40
|
+
} finally {
|
|
41
|
+
await file?.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { messageContent } from './session-input.mjs';
|
|
3
|
+
|
|
4
|
+
const networkActions = new Set(['start', 'send', 'wait', 'stop', 'list', 'view']);
|
|
5
|
+
|
|
6
|
+
/** Adapt CLI syntax to the same Node Agent Network command used by native tools. */
|
|
7
|
+
export async function prepareAgentNetworkRequest({ args, stdin, translate, sessionId }) {
|
|
8
|
+
const input = {};
|
|
9
|
+
for (const name of [
|
|
10
|
+
'runner',
|
|
11
|
+
'modelId',
|
|
12
|
+
'effort',
|
|
13
|
+
'title',
|
|
14
|
+
'cursor',
|
|
15
|
+
'limit',
|
|
16
|
+
'query',
|
|
17
|
+
'parentSessionId',
|
|
18
|
+
'scope',
|
|
19
|
+
'waitFor',
|
|
20
|
+
'mode'
|
|
21
|
+
])
|
|
22
|
+
if (args[name] !== undefined) input[name] = args[name];
|
|
23
|
+
if (['send', 'stop', 'view', 'wait'].includes(args.action) && sessionId)
|
|
24
|
+
input.agentId = sessionId;
|
|
25
|
+
if (args.action === 'wait' && args.agentIds?.length) input.agentIds = args.agentIds;
|
|
26
|
+
if (args.action === 'wait' && args.timeoutMs !== undefined) input.waitMs = args.timeoutMs;
|
|
27
|
+
if (args.message !== undefined || args.messageFile !== undefined)
|
|
28
|
+
input.content = await messageContent(args, stdin, 100000, translate);
|
|
29
|
+
if (args.goal !== undefined) input.goal = { objective: args.goal.trim() };
|
|
30
|
+
if (args.clearGoal) input.goal = { clear: true };
|
|
31
|
+
if (args.action === 'send' && args.approvalId !== undefined)
|
|
32
|
+
input.approval = { approvalId: args.approvalId, decision: args.decision };
|
|
33
|
+
if (args.action === 'start' || args.action === 'send')
|
|
34
|
+
input.clientMessageId = args.clientMessageId ?? randomUUID();
|
|
35
|
+
if (args.action === 'send')
|
|
36
|
+
input.deliveryIntent = args.steer ? 'REPLACE_CURRENT' : args.queue ? 'QUEUE' : 'SEND';
|
|
37
|
+
return { action: 'network', command: { operation: args.action, input } };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isAgentNetworkAction(action) {
|
|
41
|
+
return networkActions.has(action);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function prepareSessionRequest({ args, env, stdin, translate, sessionId, targeted }) {
|
|
45
|
+
const body = { action: args.action };
|
|
46
|
+
for (const name of [
|
|
47
|
+
'sessionId',
|
|
48
|
+
'parentSessionId',
|
|
49
|
+
'runner',
|
|
50
|
+
'modelId',
|
|
51
|
+
'effort',
|
|
52
|
+
'title',
|
|
53
|
+
'cursor',
|
|
54
|
+
'limit',
|
|
55
|
+
'query',
|
|
56
|
+
'runId',
|
|
57
|
+
'approvalId',
|
|
58
|
+
'cron',
|
|
59
|
+
'expectedRevision'
|
|
60
|
+
])
|
|
61
|
+
if (args[name] !== undefined) body[name] = args[name];
|
|
62
|
+
if (targeted) body.sessionId = sessionId;
|
|
63
|
+
if (args.action === 'current' && env.MAR_CTL_SESSION_ID) body.sessionId = env.MAR_CTL_SESSION_ID;
|
|
64
|
+
if (args.action === 'start' && env.MAR_CTL_SESSION_ID)
|
|
65
|
+
body.parentSessionId = env.MAR_CTL_SESSION_ID;
|
|
66
|
+
if (['start', 'send'].includes(args.action) && env.MAR_CTL_SESSION_ID)
|
|
67
|
+
body.sourceSessionId = env.MAR_CTL_SESSION_ID;
|
|
68
|
+
if (args.messageFile !== undefined || args.message !== undefined)
|
|
69
|
+
body.content = await messageContent(
|
|
70
|
+
args,
|
|
71
|
+
stdin,
|
|
72
|
+
args.action === 'goal.set' ||
|
|
73
|
+
args.action.startsWith('schedule.') ||
|
|
74
|
+
args.action === 'prompt.set'
|
|
75
|
+
? 20000
|
|
76
|
+
: 100000,
|
|
77
|
+
translate,
|
|
78
|
+
args.action === 'prompt.set'
|
|
79
|
+
);
|
|
80
|
+
if (args.action === 'approval.decide')
|
|
81
|
+
body.decision = args.decision === 'approve' ? 'APPROVED' : 'REJECTED';
|
|
82
|
+
if (args.action === 'start' || args.action === 'send')
|
|
83
|
+
body.clientMessageId = args.clientMessageId ?? randomUUID();
|
|
84
|
+
if (args.action === 'send')
|
|
85
|
+
body.deliveryIntent = args.steer === true ? 'REPLACE_CURRENT' : 'QUEUE';
|
|
86
|
+
return Object.freeze(body);
|
|
87
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Scope is a command property, independent of input or the remote session state.
|
|
2
|
+
export const sessionTarget = Object.freeze({
|
|
3
|
+
current: 'workspace',
|
|
4
|
+
list: 'workspace',
|
|
5
|
+
start: 'workspace',
|
|
6
|
+
'schedule.list': 'workspace',
|
|
7
|
+
show: 'current',
|
|
8
|
+
view: 'current',
|
|
9
|
+
config: 'current',
|
|
10
|
+
history: 'current',
|
|
11
|
+
graph: 'current',
|
|
12
|
+
rename: 'current',
|
|
13
|
+
'prompt.get': 'current',
|
|
14
|
+
'prompt.set': 'current',
|
|
15
|
+
'prompt.clear': 'current',
|
|
16
|
+
'schedule.get': 'current',
|
|
17
|
+
'schedule.create': 'current',
|
|
18
|
+
'schedule.update': 'current',
|
|
19
|
+
send: 'explicit',
|
|
20
|
+
stop: 'explicit',
|
|
21
|
+
wait: 'optional',
|
|
22
|
+
'approval.decide': 'explicit',
|
|
23
|
+
'goal.get': 'explicit',
|
|
24
|
+
'goal.set': 'explicit',
|
|
25
|
+
'goal.clear': 'explicit'
|
|
26
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { MarctlError } from './errors.mjs';
|
|
3
|
+
import { createTranslator } from './i18n/index.mjs';
|
|
4
|
+
|
|
5
|
+
const terminal = new Set(['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED']);
|
|
6
|
+
|
|
7
|
+
/** One CLI-owned Workbench connection and one existing Session Watch, never history polling. */
|
|
8
|
+
export async function waitForSessionRun({
|
|
9
|
+
client,
|
|
10
|
+
sessionId,
|
|
11
|
+
runId,
|
|
12
|
+
timeoutMs,
|
|
13
|
+
until,
|
|
14
|
+
WebSocketClass,
|
|
15
|
+
translate = createTranslator(),
|
|
16
|
+
signal
|
|
17
|
+
}) {
|
|
18
|
+
let socket;
|
|
19
|
+
const watchId = randomUUID();
|
|
20
|
+
let connectionId;
|
|
21
|
+
let renewal;
|
|
22
|
+
let settled = false;
|
|
23
|
+
let subscribed = false;
|
|
24
|
+
let progress;
|
|
25
|
+
let accept;
|
|
26
|
+
let rejectResult;
|
|
27
|
+
const result = new Promise((resolve, fail) => {
|
|
28
|
+
accept = resolve;
|
|
29
|
+
rejectResult = fail;
|
|
30
|
+
});
|
|
31
|
+
const reject = (error, stage) => {
|
|
32
|
+
if (settled) return;
|
|
33
|
+
settled = true;
|
|
34
|
+
Object.assign(error, { sessionId, runId, stage });
|
|
35
|
+
rejectResult(error);
|
|
36
|
+
};
|
|
37
|
+
const fail = (code, key, { stage, upstreamCode, ...options } = {}) => {
|
|
38
|
+
const error = new MarctlError(translate(key), { ...options, code });
|
|
39
|
+
if (upstreamCode !== undefined) error.upstreamCode = upstreamCode;
|
|
40
|
+
if (stage === 'timeout' && progress !== undefined) error.progress = progress;
|
|
41
|
+
reject(error, stage);
|
|
42
|
+
};
|
|
43
|
+
const unknown = (stage, detail, source) =>
|
|
44
|
+
fail('MARCTL_WAIT_UNKNOWN', 'errors.sessionWaitUnknown', {
|
|
45
|
+
stage,
|
|
46
|
+
upstreamCode:
|
|
47
|
+
typeof source?.code === 'string' && /^[A-Za-z][A-Za-z0-9_]{0,127}$/u.test(source.code)
|
|
48
|
+
? source.code
|
|
49
|
+
: undefined,
|
|
50
|
+
requestId: typeof source?.requestId === 'string' ? source.requestId.slice(0, 160) : undefined,
|
|
51
|
+
upstreamRequestId:
|
|
52
|
+
typeof source?.upstreamRequestId === 'string'
|
|
53
|
+
? source.upstreamRequestId.slice(0, 160)
|
|
54
|
+
: undefined,
|
|
55
|
+
details: [detail, source?.message, source?.details]
|
|
56
|
+
.filter((value) => typeof value === 'string')
|
|
57
|
+
.join(' ')
|
|
58
|
+
.slice(0, 4000)
|
|
59
|
+
});
|
|
60
|
+
const command = (mode) =>
|
|
61
|
+
client.sessionCommand({
|
|
62
|
+
action: 'watch',
|
|
63
|
+
sessionId,
|
|
64
|
+
workbenchConnectionId: connectionId,
|
|
65
|
+
watchId,
|
|
66
|
+
mode
|
|
67
|
+
});
|
|
68
|
+
const scheduleRenewal = (expiresAt) => {
|
|
69
|
+
clearTimeout(renewal);
|
|
70
|
+
const remaining = expiresAt - Date.now();
|
|
71
|
+
if (!Number.isFinite(remaining) || remaining <= 0) {
|
|
72
|
+
unknown('lease', 'The Watch lease is missing or expired.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
renewal = setTimeout(
|
|
76
|
+
() => {
|
|
77
|
+
command('renew')
|
|
78
|
+
.then((ack) => {
|
|
79
|
+
if (!settled) scheduleRenewal(ack?.expiresAt);
|
|
80
|
+
})
|
|
81
|
+
.catch((error) => {
|
|
82
|
+
if ([401, 403].includes(error.statusCode)) reject(error, 'renew');
|
|
83
|
+
else
|
|
84
|
+
unknown(
|
|
85
|
+
'renew',
|
|
86
|
+
'The Watch lease could not be renewed.',
|
|
87
|
+
error instanceof MarctlError ? error : undefined
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
Math.max(50, Math.floor(remaining / 2))
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
const check = (run) => {
|
|
95
|
+
if (run?.id === runId && run.sessionId === sessionId && terminal.has(run.status) && !settled) {
|
|
96
|
+
settled = true;
|
|
97
|
+
accept({ run });
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const checkApproval = () => {
|
|
101
|
+
if (settled || until !== 'approval' || progress === undefined) return;
|
|
102
|
+
if (!Array.isArray(progress.pendingApprovals)) {
|
|
103
|
+
fail('NODE_OPERATION_UNSUPPORTED', 'errors.sessionApprovalUnsupported', {
|
|
104
|
+
stage: 'approval'
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const pendingApprovals = progress.pendingApprovals.filter(
|
|
109
|
+
(approval) => approval.runId === runId
|
|
110
|
+
);
|
|
111
|
+
if (pendingApprovals.length === 0) return;
|
|
112
|
+
settled = true;
|
|
113
|
+
accept({
|
|
114
|
+
sessionId,
|
|
115
|
+
runId,
|
|
116
|
+
stopReason: 'APPROVAL_REQUIRED',
|
|
117
|
+
pendingApprovals,
|
|
118
|
+
...(progress.pendingApprovalsTruncated ? { pendingApprovalsTruncated: true } : {})
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
const onMessage = (raw) => {
|
|
122
|
+
if (settled) return;
|
|
123
|
+
let message;
|
|
124
|
+
try {
|
|
125
|
+
message = JSON.parse(String(raw));
|
|
126
|
+
} catch {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (message?.type === 'watch.event') message = message.event;
|
|
130
|
+
if (!message || typeof message !== 'object') return;
|
|
131
|
+
if (
|
|
132
|
+
message.type === 'workbench.ready' &&
|
|
133
|
+
connectionId === undefined &&
|
|
134
|
+
typeof message.workbenchConnectionId === 'string'
|
|
135
|
+
) {
|
|
136
|
+
connectionId = message.workbenchConnectionId;
|
|
137
|
+
subscribed = true;
|
|
138
|
+
command('initial').catch((error) => reject(error, 'subscribe'));
|
|
139
|
+
}
|
|
140
|
+
if (message.watchId !== watchId) return;
|
|
141
|
+
if (message.type === 'watch.failed') {
|
|
142
|
+
unknown('watch', 'The Session Watch failed.', message);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (message.type === 'watch.session.snapshot') {
|
|
146
|
+
const runs = message.payload?.runs;
|
|
147
|
+
if (!Array.isArray(runs)) {
|
|
148
|
+
unknown('snapshot', 'The Watch snapshot did not contain a Run list.');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const run = runs.find((value) => value?.id === runId && value.sessionId === sessionId);
|
|
152
|
+
if (run === undefined) {
|
|
153
|
+
unknown('snapshot', 'The requested Run was absent from the Watch snapshot.');
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (message.payload?.session?.id === sessionId && message.payload?.progress?.runId === runId)
|
|
157
|
+
progress = message.payload.progress;
|
|
158
|
+
check(run);
|
|
159
|
+
if (!settled && until === 'approval' && progress === undefined) {
|
|
160
|
+
fail('NODE_OPERATION_UNSUPPORTED', 'errors.sessionApprovalUnsupported', {
|
|
161
|
+
stage: 'approval'
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
checkApproval();
|
|
165
|
+
} else if (message.type === 'watch.session.event') {
|
|
166
|
+
const payload = message.event?.payload;
|
|
167
|
+
if (
|
|
168
|
+
(payload?.sessionId ?? payload?.run?.sessionId) === sessionId &&
|
|
169
|
+
payload?.progress?.runId === runId
|
|
170
|
+
)
|
|
171
|
+
progress = payload.progress;
|
|
172
|
+
check(payload?.run);
|
|
173
|
+
checkApproval();
|
|
174
|
+
}
|
|
175
|
+
if (!settled && message.expiresAt !== undefined) scheduleRenewal(message.expiresAt);
|
|
176
|
+
};
|
|
177
|
+
const onClose = (code, reason) => {
|
|
178
|
+
if (code === 1008 && String(reason) === 'UNAUTHENTICATED')
|
|
179
|
+
fail('UNAUTHENTICATED', 'errors.userContextUnavailable', {
|
|
180
|
+
statusCode: 401,
|
|
181
|
+
stage: 'connection'
|
|
182
|
+
});
|
|
183
|
+
else
|
|
184
|
+
unknown(
|
|
185
|
+
'connection',
|
|
186
|
+
Number.isInteger(code)
|
|
187
|
+
? `The Watch connection closed (code ${code}).`
|
|
188
|
+
: 'The Watch connection closed.'
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
const onError = (error) =>
|
|
192
|
+
unknown('connection', 'The Watch connection failed.', { code: error?.code });
|
|
193
|
+
const onOpen = () => {
|
|
194
|
+
try {
|
|
195
|
+
socket.send(JSON.stringify({ type: 'resume' }));
|
|
196
|
+
} catch (error) {
|
|
197
|
+
onError(error);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const onAbort = () =>
|
|
201
|
+
fail('MARCTL_WAIT_CANCELLED', 'errors.sessionWaitCancelled', { stage: 'cancel' });
|
|
202
|
+
try {
|
|
203
|
+
socket = client.openWorkbenchWebSocket(WebSocketClass ?? (await import('ws')).default);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (error instanceof MarctlError) reject(error, 'connection');
|
|
206
|
+
else unknown('connection', 'The Watch connection could not be opened.');
|
|
207
|
+
return await result;
|
|
208
|
+
}
|
|
209
|
+
const timer = setTimeout(
|
|
210
|
+
() => fail('MARCTL_WAIT_TIMEOUT', 'errors.sessionWaitTimeout', { stage: 'timeout' }),
|
|
211
|
+
timeoutMs
|
|
212
|
+
);
|
|
213
|
+
socket.on('open', onOpen);
|
|
214
|
+
socket.on('message', onMessage);
|
|
215
|
+
socket.on('error', onError);
|
|
216
|
+
socket.on('close', onClose);
|
|
217
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
218
|
+
if (signal?.aborted) onAbort();
|
|
219
|
+
try {
|
|
220
|
+
return await result;
|
|
221
|
+
} finally {
|
|
222
|
+
settled = true;
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
clearTimeout(renewal);
|
|
225
|
+
signal?.removeEventListener('abort', onAbort);
|
|
226
|
+
if (subscribed) await command('stop').catch(() => undefined);
|
|
227
|
+
socket.off('open', onOpen);
|
|
228
|
+
socket.off('message', onMessage);
|
|
229
|
+
socket.off('close', onClose);
|
|
230
|
+
socket.close();
|
|
231
|
+
}
|
|
232
|
+
}
|