martty 0.2.33 → 0.2.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -11
- package/bin/martty.js +52 -5
- package/lib/acp-client.js +296 -20
- package/lib/acp-registry.snapshot.json +1450 -0
- package/lib/acp-session-config.js +23 -0
- package/lib/acp-session-plan.js +14 -2
- package/lib/acp-session-stats.js +22 -5
- package/lib/acp-session-status.js +70 -25
- package/lib/agent.js +24 -4
- package/lib/boot.js +29 -5
- package/lib/command-args.js +28 -0
- package/lib/download.js +165 -0
- package/lib/harness-discovery.js +41 -0
- package/lib/harness-package.js +200 -0
- package/lib/harness-registry.js +468 -0
- package/lib/harness-removal.js +88 -0
- package/lib/harness-view.js +639 -46
- package/lib/harnesses.js +1053 -41
- package/lib/index.js +4 -0
- package/lib/mux.js +23 -0
- package/lib/plan-view.js +18 -3
- package/lib/status-view.js +2 -0
- package/lib/tui-commands.js +7 -1
- package/lib/tui-overlay.js +27 -5
- package/package.json +7 -4
- package/vendor/darwin-arm64/martty +0 -0
- package/vendor/darwin-x64/martty +0 -0
- package/vendor/linux-arm64/martty +0 -0
- package/vendor/linux-x64/martty +0 -0
- package/vendor/win32-x64/martty.exe +0 -0
|
@@ -64,6 +64,7 @@ class AcpSessionConfigService extends Service {
|
|
|
64
64
|
/** Install the `ctx.acpSessionConfig` standard ACP state service. */
|
|
65
65
|
export function installAcpSessionConfig(ctx) {
|
|
66
66
|
let sessionId
|
|
67
|
+
let sessionGeneration = 0
|
|
67
68
|
let selectionKnown = false
|
|
68
69
|
const optionsBySession = new Map()
|
|
69
70
|
let requestTui
|
|
@@ -142,6 +143,15 @@ export function installAcpSessionConfig(ctx) {
|
|
|
142
143
|
|
|
143
144
|
function observeClient(message) {
|
|
144
145
|
if (!isObject(message) || message.id === undefined || typeof message.method !== 'string') return
|
|
146
|
+
if (message.method === 'initialize') {
|
|
147
|
+
sessionGeneration++
|
|
148
|
+
pending.clear()
|
|
149
|
+
sessionId = undefined
|
|
150
|
+
selectionKnown = false
|
|
151
|
+
optionsBySession.clear()
|
|
152
|
+
publish()
|
|
153
|
+
return
|
|
154
|
+
}
|
|
145
155
|
if (SETUP_METHODS.has(message.method)) {
|
|
146
156
|
const requested = message.method !== 'session/new'
|
|
147
157
|
? readString(message.params, 'sessionId', 'session_id')
|
|
@@ -209,12 +219,15 @@ export function installAcpSessionConfig(ctx) {
|
|
|
209
219
|
throw new Error(`acpSessionConfig.set: option "${id}" is not advertised by the current Session`)
|
|
210
220
|
}
|
|
211
221
|
validateValue(option, value)
|
|
222
|
+
const requestedSession = targetSessionId
|
|
223
|
+
const requestedGeneration = sessionGeneration
|
|
212
224
|
const result = await requestTui(CORDIS_METHODS.sessionConfigSet, {
|
|
213
225
|
protocol: CORDIS_PROTOCOL,
|
|
214
226
|
sessionId: targetSessionId,
|
|
215
227
|
configId: id,
|
|
216
228
|
value,
|
|
217
229
|
})
|
|
230
|
+
assertSession(requestedSession, requestedGeneration)
|
|
218
231
|
if (!isObject(result)) {
|
|
219
232
|
throw new Error('acpSessionConfig.set: native ACP client returned an invalid response')
|
|
220
233
|
}
|
|
@@ -227,6 +240,12 @@ export function installAcpSessionConfig(ctx) {
|
|
|
227
240
|
return setForSession(sessionId, id, value)
|
|
228
241
|
}
|
|
229
242
|
|
|
243
|
+
function assertSession(expectedSession, expectedGeneration) {
|
|
244
|
+
if (sessionGeneration !== expectedGeneration || !optionsBySession.has(expectedSession)) {
|
|
245
|
+
throw new Error('acpSessionConfig: Session changed while configuring options')
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
230
249
|
function transaction(selector) {
|
|
231
250
|
const transactionSessionId = sessionId
|
|
232
251
|
if (transactionSessionId === undefined) {
|
|
@@ -234,6 +253,8 @@ export function installAcpSessionConfig(ctx) {
|
|
|
234
253
|
}
|
|
235
254
|
const transactionOptions = optionsBySession.get(transactionSessionId) ?? []
|
|
236
255
|
const option = resolveTransactionOption(selector, transactionOptions)
|
|
256
|
+
const originalSession = transactionSessionId
|
|
257
|
+
const originalGeneration = sessionGeneration
|
|
237
258
|
const original = cloneJson(option.currentValue ?? option.current_value)
|
|
238
259
|
if (original === undefined) {
|
|
239
260
|
throw new Error(
|
|
@@ -248,6 +269,7 @@ export function installAcpSessionConfig(ctx) {
|
|
|
248
269
|
let settled = false
|
|
249
270
|
|
|
250
271
|
const write = async (value) => {
|
|
272
|
+
assertSession(originalSession, originalGeneration)
|
|
251
273
|
const result = await setForSession(transactionSessionId, option.id, value)
|
|
252
274
|
applied = cloneJson(value)
|
|
253
275
|
return result
|
|
@@ -269,6 +291,7 @@ export function installAcpSessionConfig(ctx) {
|
|
|
269
291
|
if (finalizing) return finalizing
|
|
270
292
|
desired = cloneJson(value)
|
|
271
293
|
const settle = async () => {
|
|
294
|
+
assertSession(originalSession, originalGeneration)
|
|
272
295
|
if (!Object.is(applied, desired)) await write(desired)
|
|
273
296
|
settled = true
|
|
274
297
|
return listFor(transactionSessionId)
|
package/lib/acp-session-plan.js
CHANGED
|
@@ -101,12 +101,22 @@ export function installAcpSessionPlan(ctx) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
function observeClient(message) {
|
|
104
|
-
if (!isObject(message) || message.id === undefined
|
|
104
|
+
if (!isObject(message) || message.id === undefined) return
|
|
105
|
+
if (message.method === 'initialize') {
|
|
106
|
+
pending.clear()
|
|
107
|
+
plansBySession.clear()
|
|
108
|
+
sessionId = undefined
|
|
109
|
+
selectionKnown = false
|
|
110
|
+
publish()
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if (!SETUP_METHODS.has(message.method)) return
|
|
105
114
|
pending.set(message.id, {
|
|
106
115
|
sessionId: message.method !== 'session/new'
|
|
107
116
|
? readString(message.params, 'sessionId', 'session_id')
|
|
108
117
|
: undefined,
|
|
109
118
|
})
|
|
119
|
+
if (message.method === 'session/load') reset(readString(message.params, 'sessionId', 'session_id'))
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
function observeAgent(message) {
|
|
@@ -117,7 +127,9 @@ export function installAcpSessionPlan(ctx) {
|
|
|
117
127
|
if (message.error !== undefined || !isObject(message.result)) return
|
|
118
128
|
const bound = readString(message.result, 'sessionId', 'session_id') ?? tracked.sessionId
|
|
119
129
|
if (!selectionKnown) sessionId = bound
|
|
120
|
-
|
|
130
|
+
// Replay notifications precede the setup response. Preserve their state.
|
|
131
|
+
if (!plansBySession.has(bound)) reset(bound)
|
|
132
|
+
else if (bound === sessionId) publish()
|
|
121
133
|
return
|
|
122
134
|
}
|
|
123
135
|
if (message.method !== 'session/update' || !isObject(message.params)) return
|
package/lib/acp-session-stats.js
CHANGED
|
@@ -114,12 +114,24 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
114
114
|
|
|
115
115
|
function observeClient(message) {
|
|
116
116
|
if (!object(message) || message.id === undefined || typeof message.method !== 'string') return
|
|
117
|
+
if (message.method === 'initialize') {
|
|
118
|
+
pendingSetup.clear()
|
|
119
|
+
pendingPrompts.clear()
|
|
120
|
+
activePrompts.clear()
|
|
121
|
+
toolStarts.clear()
|
|
122
|
+
values.clear()
|
|
123
|
+
sessionId = undefined
|
|
124
|
+
selectionKnown = false
|
|
125
|
+
publish()
|
|
126
|
+
return
|
|
127
|
+
}
|
|
117
128
|
if (SETUP_METHODS.has(message.method)) {
|
|
118
129
|
pendingSetup.set(message.id, {
|
|
119
130
|
sessionId: message.method !== 'session/new'
|
|
120
131
|
? readString(message.params, 'sessionId', 'session_id')
|
|
121
132
|
: undefined,
|
|
122
133
|
})
|
|
134
|
+
if (message.method === 'session/load') reset(readString(message.params, 'sessionId', 'session_id'))
|
|
123
135
|
return
|
|
124
136
|
}
|
|
125
137
|
if (message.method !== 'session/prompt') return
|
|
@@ -128,13 +140,16 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
128
140
|
if (!selectionKnown && sessionId === undefined) sessionId = promptSessionId
|
|
129
141
|
const prompt = {
|
|
130
142
|
sessionId: promptSessionId,
|
|
143
|
+
primary: !activePrompts.has(promptSessionId),
|
|
131
144
|
started: now(),
|
|
132
145
|
firstToken: undefined,
|
|
133
146
|
toolMillis: 0,
|
|
134
147
|
}
|
|
135
148
|
pendingPrompts.set(message.id, prompt)
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
if (prompt.primary) {
|
|
150
|
+
activePrompts.set(promptSessionId, prompt)
|
|
151
|
+
stateFor(promptSessionId).stats.turns += 1
|
|
152
|
+
}
|
|
138
153
|
publishIf(promptSessionId)
|
|
139
154
|
}
|
|
140
155
|
|
|
@@ -146,19 +161,21 @@ export function installAcpSessionStats(ctx, options = {}) {
|
|
|
146
161
|
if (message.error !== undefined || !object(message.result)) return
|
|
147
162
|
const bound = readString(message.result, 'sessionId', 'session_id') ?? tracked.sessionId
|
|
148
163
|
if (!selectionKnown) sessionId = bound
|
|
149
|
-
|
|
164
|
+
// Replay notifications precede the setup response. Preserve their state.
|
|
165
|
+
if (!values.has(bound)) reset(bound)
|
|
166
|
+
else if (bound === sessionId) publish()
|
|
150
167
|
return
|
|
151
168
|
}
|
|
152
169
|
if (message.id !== undefined && pendingPrompts.has(message.id)) {
|
|
153
170
|
const prompt = pendingPrompts.get(message.id)
|
|
154
171
|
pendingPrompts.delete(message.id)
|
|
155
|
-
activePrompts.delete(prompt.sessionId)
|
|
172
|
+
if (activePrompts.get(prompt.sessionId) === prompt) activePrompts.delete(prompt.sessionId)
|
|
156
173
|
const value = stateFor(prompt.sessionId)
|
|
157
174
|
if (message.error === undefined && object(message.result)) {
|
|
158
175
|
addUsage(value, message.result.usage)
|
|
159
176
|
}
|
|
160
177
|
const elapsed = Math.max(0, now() - prompt.started)
|
|
161
|
-
value.stats.llmMillis += Math.max(0, elapsed - prompt.toolMillis)
|
|
178
|
+
if (prompt.primary) value.stats.llmMillis += Math.max(0, elapsed - prompt.toolMillis)
|
|
162
179
|
publishIf(prompt.sessionId)
|
|
163
180
|
return
|
|
164
181
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Standard-ACP-backed run-state projection for the Client tree.
|
|
3
3
|
*
|
|
4
4
|
* Folds the non-statistics facts `/status` needs — connection, server,
|
|
5
|
-
* authenticate state, session binding, model, effort, permission, plan,
|
|
5
|
+
* authenticate state, session binding/use, model, effort, permission, plan,
|
|
6
6
|
* agent preset, and the running/idle state — from messages the mux already
|
|
7
7
|
* observes. Statistics stay in `acpSessionStats`; this service never
|
|
8
8
|
* counts tokens or timings, so there is exactly one stats source.
|
|
@@ -43,7 +43,7 @@ class AcpSessionStatusService extends Service {
|
|
|
43
43
|
function zeroSession(sessionId, bound = sessionId !== undefined) {
|
|
44
44
|
return {
|
|
45
45
|
state: 'idle',
|
|
46
|
-
session: { sessionId, bound },
|
|
46
|
+
session: { sessionId, bound, started: false },
|
|
47
47
|
model: undefined,
|
|
48
48
|
effort: undefined,
|
|
49
49
|
permission: undefined,
|
|
@@ -68,6 +68,8 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
68
68
|
let server
|
|
69
69
|
const auth = { status: undefined, method: undefined }
|
|
70
70
|
let initializeId
|
|
71
|
+
let authMethods = []
|
|
72
|
+
const pendingRequests = new Set()
|
|
71
73
|
const pendingAuthenticate = new Set()
|
|
72
74
|
const pendingSetup = new Map()
|
|
73
75
|
const pendingPrompts = new Map()
|
|
@@ -131,22 +133,43 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
131
133
|
function observeClient(message) {
|
|
132
134
|
if (!object(message) || typeof message.method !== 'string') return
|
|
133
135
|
if (message.method === 'initialize' && message.id !== undefined) {
|
|
136
|
+
pendingRequests.clear()
|
|
137
|
+
sessions.clear()
|
|
138
|
+
Object.assign(fallback, zeroSession(undefined, false))
|
|
139
|
+
delete fallback.error
|
|
140
|
+
sessionId = undefined
|
|
141
|
+
selectionKnown = false
|
|
142
|
+
connection = 'connecting'
|
|
143
|
+
server = undefined
|
|
144
|
+
auth.status = undefined
|
|
145
|
+
auth.method = undefined
|
|
146
|
+
delete auth.message
|
|
147
|
+
authMethods = []
|
|
148
|
+
pendingAuthenticate.clear()
|
|
149
|
+
pendingSetup.clear()
|
|
150
|
+
pendingPrompts.clear()
|
|
134
151
|
initializeId = message.id
|
|
152
|
+
publish()
|
|
135
153
|
return
|
|
136
154
|
}
|
|
155
|
+
if (message.id !== undefined) pendingRequests.add(message.id)
|
|
137
156
|
if (message.method === 'authenticate' && message.id !== undefined) {
|
|
138
157
|
pendingAuthenticate.add(message.id)
|
|
158
|
+
const methodId = readString(message.params, 'methodId')
|
|
159
|
+
const method = authMethods.find((candidate) => candidate?.id === methodId)
|
|
160
|
+
if (methodId !== undefined) auth.method = readString(method, 'name', 'label') ?? methodId
|
|
161
|
+
delete auth.message
|
|
139
162
|
auth.status = 'signing in'
|
|
140
163
|
publish()
|
|
141
164
|
return
|
|
142
165
|
}
|
|
143
166
|
if (SETUP_METHODS.has(message.method) && message.id !== undefined) {
|
|
144
|
-
pendingSetup.set(
|
|
145
|
-
message.
|
|
146
|
-
message.method !== 'session/new'
|
|
167
|
+
pendingSetup.set(message.id, {
|
|
168
|
+
sessionId: message.method !== 'session/new'
|
|
147
169
|
? readString(message.params, 'sessionId', 'session_id')
|
|
148
170
|
: undefined,
|
|
149
|
-
|
|
171
|
+
started: message.method !== 'session/new',
|
|
172
|
+
})
|
|
150
173
|
return
|
|
151
174
|
}
|
|
152
175
|
if (message.method === 'session/prompt' && message.id !== undefined) {
|
|
@@ -155,6 +178,7 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
155
178
|
if (!selectionKnown && sessionId === undefined) sessionId = promptSessionId
|
|
156
179
|
pendingPrompts.set(message.id, promptSessionId)
|
|
157
180
|
const value = stateFor(promptSessionId)
|
|
181
|
+
value.session.started = true
|
|
158
182
|
if (value.state === 'idle') {
|
|
159
183
|
value.state = 'starting'
|
|
160
184
|
publishIf(promptSessionId)
|
|
@@ -166,42 +190,61 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
166
190
|
if (!object(message)) return
|
|
167
191
|
if (message.id !== undefined && message.id === initializeId) {
|
|
168
192
|
initializeId = undefined
|
|
169
|
-
connection = 'attached'
|
|
193
|
+
connection = message.error === undefined ? 'attached' : 'failed'
|
|
194
|
+
fallback.error = message.error?.message
|
|
170
195
|
const result = object(message.result) ? message.result : undefined
|
|
171
196
|
server = readString(result?.agentInfo, 'name')
|
|
172
197
|
const methods = Array.isArray(result?.authMethods) ? result.authMethods : []
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (method !== undefined && auth.status === undefined) {
|
|
176
|
-
auth.method = readString(method, 'name', 'label') ?? method.id
|
|
177
|
-
}
|
|
198
|
+
authMethods = methods
|
|
199
|
+
// initialize advertises choices, not the credential used by this process.
|
|
178
200
|
publish()
|
|
179
201
|
return
|
|
180
202
|
}
|
|
203
|
+
// Unknown replies can belong to a replaced Agent. Never let them restore
|
|
204
|
+
// its authentication or run state in the new connection.
|
|
205
|
+
if (response(message) && !pendingRequests.delete(message.id)) return
|
|
181
206
|
if (message.id !== undefined && pendingAuthenticate.has(message.id)) {
|
|
182
207
|
pendingAuthenticate.delete(message.id)
|
|
183
208
|
if (message.error === undefined) {
|
|
184
209
|
auth.status = 'configured'
|
|
185
|
-
|
|
186
|
-
auth.status = 'needs sign-in'
|
|
210
|
+
delete auth.message
|
|
187
211
|
} else {
|
|
188
|
-
auth.status =
|
|
212
|
+
auth.status = 'sign-in failed'
|
|
213
|
+
auth.message = readString(message.error?.data, 'details', 'message')
|
|
214
|
+
?? readString(message.error, 'message') ?? 'Authentication failed'
|
|
189
215
|
}
|
|
190
216
|
publish()
|
|
191
217
|
return
|
|
192
218
|
}
|
|
193
219
|
if (message.id !== undefined && pendingSetup.has(message.id)) {
|
|
194
|
-
const
|
|
220
|
+
const setup = pendingSetup.get(message.id)
|
|
195
221
|
pendingSetup.delete(message.id)
|
|
196
222
|
if (message.error !== undefined) {
|
|
197
223
|
if (isAuthRequired(message.error)) auth.status = 'needs sign-in'
|
|
198
|
-
|
|
199
|
-
|
|
224
|
+
else {
|
|
225
|
+
connection = 'failed'
|
|
226
|
+
const value = stateFor(setup.sessionId, false)
|
|
227
|
+
value.error = readString(message.error, 'message') ?? 'Session setup failed'
|
|
228
|
+
value.state = 'idle'
|
|
229
|
+
value.model = undefined
|
|
230
|
+
value.effort = undefined
|
|
200
231
|
}
|
|
201
|
-
|
|
232
|
+
stateFor(setup.sessionId, false).session = {
|
|
233
|
+
sessionId: setup.sessionId, bound: false, started: false,
|
|
234
|
+
}
|
|
235
|
+
if (!selectionKnown) sessionId = setup.sessionId
|
|
202
236
|
} else {
|
|
203
|
-
|
|
204
|
-
|
|
237
|
+
connection = 'attached'
|
|
238
|
+
delete fallback.error
|
|
239
|
+
const bound = readString(message.result, 'sessionId', 'session_id') ?? setup.sessionId
|
|
240
|
+
if (bound !== undefined) {
|
|
241
|
+
stateFor(bound).session = { sessionId: bound, bound: true, started: setup.started }
|
|
242
|
+
delete stateFor(bound).error
|
|
243
|
+
if (authMethods.length > 0) {
|
|
244
|
+
auth.status = 'configured'
|
|
245
|
+
delete auth.message
|
|
246
|
+
}
|
|
247
|
+
}
|
|
205
248
|
if (!selectionKnown) sessionId = bound
|
|
206
249
|
}
|
|
207
250
|
publish()
|
|
@@ -304,14 +347,16 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
304
347
|
? snapshot.sessionId
|
|
305
348
|
: sessionId
|
|
306
349
|
const value = stateFor(configSessionId)
|
|
307
|
-
|
|
308
|
-
value.
|
|
350
|
+
if (!selectionKnown && sessionId === undefined) sessionId = configSessionId
|
|
351
|
+
value.model = optionValue(snapshot.options, 'model', 'model')
|
|
352
|
+
value.effort = optionValue(snapshot.options, 'thought_level', 'effort')
|
|
309
353
|
publishIf(configSessionId)
|
|
310
354
|
}
|
|
311
355
|
|
|
312
|
-
function optionValue(options, id) {
|
|
356
|
+
function optionValue(options, category, id) {
|
|
313
357
|
if (!Array.isArray(options)) return undefined
|
|
314
|
-
const option = options.find((candidate) => candidate?.
|
|
358
|
+
const option = options.find((candidate) => candidate?.category === category)
|
|
359
|
+
?? options.find((candidate) => candidate?.id === id)
|
|
315
360
|
if (option === undefined) return undefined
|
|
316
361
|
const raw = option.currentValue ?? option.current_value
|
|
317
362
|
return typeof raw === 'string' ? raw : undefined
|
package/lib/agent.js
CHANGED
|
@@ -6,11 +6,12 @@ import { homedir } from 'node:os'
|
|
|
6
6
|
import { dirname, join, resolve } from 'node:path'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
8
|
import { selectedHarness } from './harnesses.js'
|
|
9
|
+
import { tokenizeCommandArgs } from './command-args.js'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Resolve the ACP package and TUI's internal Creator overlay bundle.
|
|
12
13
|
* @param {string | URL} [anchor]
|
|
13
|
-
* @returns {{ command: string, args: string[] }}
|
|
14
|
+
* @returns {{ command: string, args: string[], env?: Record<string, string> }}
|
|
14
15
|
*/
|
|
15
16
|
export function resolveDependencyStack(anchor = import.meta.url) {
|
|
16
17
|
const req = createRequire(anchor)
|
|
@@ -31,20 +32,39 @@ export function resolveDependencyStack(anchor = import.meta.url) {
|
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Resolve the standalone ACP command without affecting the profile path.
|
|
35
|
+
* `forcedHarness` is an internal product initialization value, not a CLI
|
|
36
|
+
* argument; null/omitted means that no product Harness is forced.
|
|
34
37
|
* @param {string | URL} [anchor]
|
|
35
|
-
* @param {{ settingsPath?: string }} [options]
|
|
38
|
+
* @param {{ settingsPath?: string, forcedHarness?: { id: string, label: string, command: string, args?: string[] } | null }} [options]
|
|
36
39
|
* @returns {{ command: string, args: string[] }}
|
|
37
40
|
*/
|
|
38
41
|
export function resolveStackedAgent(anchor = import.meta.url, options = {}) {
|
|
39
42
|
const envCmd = process.env.DSH_TUI_AGENT
|
|
40
43
|
if (typeof envCmd === 'string' && envCmd.trim().length > 0) {
|
|
41
|
-
const tokens = envCmd
|
|
44
|
+
const tokens = tokenizeCommandArgs(envCmd)
|
|
45
|
+
if (!tokens[0]) throw new Error('DSH_TUI_AGENT needs a non-empty command')
|
|
42
46
|
return { command: tokens[0], args: tokens.slice(1) }
|
|
43
47
|
}
|
|
48
|
+
if (options.forcedHarness !== undefined && options.forcedHarness !== null) {
|
|
49
|
+
const configured = options.forcedHarness
|
|
50
|
+
if (typeof configured !== 'object' || Array.isArray(configured)
|
|
51
|
+
|| typeof configured.command !== 'string' || configured.command.trim().length === 0) {
|
|
52
|
+
throw new Error('forcedHarness must declare a non-empty command')
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
command: configured.command,
|
|
56
|
+
args: Array.isArray(configured.args) ? configured.args.map(String) : [],
|
|
57
|
+
...(configured.env !== undefined ? { env: { ...configured.env } } : {}),
|
|
58
|
+
}
|
|
59
|
+
}
|
|
44
60
|
if (typeof options.settingsPath === 'string') {
|
|
45
61
|
const selected = selectedHarness(options.settingsPath)
|
|
46
62
|
if (selected !== undefined) {
|
|
47
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
command: selected.command,
|
|
65
|
+
args: selected.args,
|
|
66
|
+
...(selected.env !== undefined ? { env: { ...selected.env } } : {}),
|
|
67
|
+
}
|
|
48
68
|
}
|
|
49
69
|
}
|
|
50
70
|
try {
|
package/lib/boot.js
CHANGED
|
@@ -67,6 +67,8 @@ import { installTuiLocalPlugins } from './tui-local-plugins.js'
|
|
|
67
67
|
* @param {{ stdin: import('node:stream').Writable, stdout: import('node:stream').Readable, child?: import('node:child_process').ChildProcess }} [options.stream]
|
|
68
68
|
* @param {{ stdin: number | 'inherit', stdout: number | 'inherit' }} [options.tty]
|
|
69
69
|
* @param {string} [options.settingsPath]
|
|
70
|
+
* Internal product-only forced Harness; null means no pinned Harness.
|
|
71
|
+
* @param {{ id: string, label: string, command: string, args?: string[] } | null} [options.forcedHarness]
|
|
70
72
|
* @param {string} [options.artifactRoot]
|
|
71
73
|
* @param {Array<{ id: string, label: string, command: string, args?: string[], source?: string }>} [options.harnessDefaults]
|
|
72
74
|
* @param {string} [options.harnessPathValue]
|
|
@@ -75,10 +77,16 @@ import { installTuiLocalPlugins } from './tui-local-plugins.js'
|
|
|
75
77
|
export async function bootClient(options = {}) {
|
|
76
78
|
const { Context } = await import('@deepseek-ai/cordis')
|
|
77
79
|
const ctx = new Context()
|
|
80
|
+
const settingsPath = options.settingsPath ?? uiSettingsPath(options.extraArgs ?? [])
|
|
81
|
+
const forcedHarness = options.forcedHarness ?? null
|
|
78
82
|
const acpConfig = options.stream !== undefined
|
|
79
83
|
? { stream: options.stream }
|
|
80
|
-
: {
|
|
81
|
-
|
|
84
|
+
: {
|
|
85
|
+
agent: options.agent ?? resolveStackedAgent(import.meta.url, {
|
|
86
|
+
settingsPath,
|
|
87
|
+
forcedHarness,
|
|
88
|
+
}),
|
|
89
|
+
}
|
|
82
90
|
if (options.settingsPath === undefined) {
|
|
83
91
|
migrateLegacyUiSettings(settingsPath, legacyUiSettingsPaths(options.extraArgs ?? []))
|
|
84
92
|
}
|
|
@@ -96,9 +104,16 @@ export async function bootClient(options = {}) {
|
|
|
96
104
|
harnessDefaults = []
|
|
97
105
|
}
|
|
98
106
|
}
|
|
107
|
+
if (forcedHarness !== null) {
|
|
108
|
+
const forcedEntry = { ...forcedHarness, source: 'forced' }
|
|
109
|
+
const forcedIndex = harnessDefaults.findIndex(({ id }) => id === forcedHarness.id)
|
|
110
|
+
if (forcedIndex === -1) harnessDefaults = [forcedEntry, ...harnessDefaults]
|
|
111
|
+
else harnessDefaults = harnessDefaults.map((entry, index) => index === forcedIndex ? forcedEntry : entry)
|
|
112
|
+
}
|
|
99
113
|
const harnessConfig = {
|
|
100
114
|
settingsPath,
|
|
101
115
|
defaults: harnessDefaults,
|
|
116
|
+
forcedHarness,
|
|
102
117
|
pathValue: options.harnessPathValue,
|
|
103
118
|
hostOwned: options.stream !== undefined,
|
|
104
119
|
}
|
|
@@ -292,10 +307,12 @@ export function migrateLegacyUiSettings(settingsPath, legacyPaths) {
|
|
|
292
307
|
|
|
293
308
|
/**
|
|
294
309
|
* Parse `--agent` / `--agent-arg` from argv. Remaining flags pass through to Rust.
|
|
310
|
+
* `forcedHarness` is an internal product initialization value, not a CLI
|
|
311
|
+
* argument; null/omitted means that no product Harness is forced.
|
|
295
312
|
* `--agent` is also forwarded via {@link painterArgs} so Terminal Auth can
|
|
296
313
|
* re-exec the same command.
|
|
297
314
|
* @param {string[]} argv
|
|
298
|
-
* @param {{ settingsPath?: string }} [options]
|
|
315
|
+
* @param {{ settingsPath?: string, forcedHarness?: { id: string, label: string, command: string, args?: string[] } | null }} [options]
|
|
299
316
|
* @returns {{ agent: { command: string, args: string[] }, rustArgs: string[] }}
|
|
300
317
|
*/
|
|
301
318
|
export function parseClientArgv(argv, options = {}) {
|
|
@@ -305,12 +322,16 @@ export function parseClientArgv(argv, options = {}) {
|
|
|
305
322
|
for (let i = 0; i < argv.length; i += 1) {
|
|
306
323
|
const token = argv[i]
|
|
307
324
|
if (token === '--agent') {
|
|
325
|
+
if (!argv[i + 1]?.trim() || argv[i + 1].startsWith('--')) {
|
|
326
|
+
throw Object.assign(new Error('--agent needs a value'), { exitCode: 2 })
|
|
327
|
+
}
|
|
308
328
|
command = argv[i + 1]
|
|
309
329
|
i += 1
|
|
310
330
|
continue
|
|
311
331
|
}
|
|
312
332
|
if (token === '--agent-arg') {
|
|
313
|
-
|
|
333
|
+
if (argv[i + 1] === undefined) throw Object.assign(new Error('--agent-arg needs a value'), { exitCode: 2 })
|
|
334
|
+
args.push(argv[i + 1])
|
|
314
335
|
i += 1
|
|
315
336
|
continue
|
|
316
337
|
}
|
|
@@ -319,7 +340,10 @@ export function parseClientArgv(argv, options = {}) {
|
|
|
319
340
|
if (command === undefined) {
|
|
320
341
|
const settingsPath = options.settingsPath ?? uiSettingsPath(rustArgs)
|
|
321
342
|
return {
|
|
322
|
-
agent: resolveStackedAgent(import.meta.url, {
|
|
343
|
+
agent: resolveStackedAgent(import.meta.url, {
|
|
344
|
+
settingsPath,
|
|
345
|
+
forcedHarness: options.forcedHarness,
|
|
346
|
+
}),
|
|
323
347
|
rustArgs,
|
|
324
348
|
}
|
|
325
349
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Split an argv string without shell execution, expansion, or substitution. */
|
|
2
|
+
export function tokenizeCommandArgs(value) {
|
|
3
|
+
const tokens = []
|
|
4
|
+
let token = '', quote
|
|
5
|
+
let escaped = false, started = false
|
|
6
|
+
for (const char of String(value ?? '')) {
|
|
7
|
+
if (escaped) {
|
|
8
|
+
if (quote === '"' && !['\\', '"', '$', '`', '\n'].includes(char)) token += '\\'
|
|
9
|
+
token += char
|
|
10
|
+
escaped = false
|
|
11
|
+
} else if (char === '\\' && quote !== "'") {
|
|
12
|
+
escaped = true; started = true
|
|
13
|
+
} else if (quote !== undefined) {
|
|
14
|
+
if (char === quote) quote = undefined
|
|
15
|
+
else token += char
|
|
16
|
+
} else if (char === '"' || char === "'") {
|
|
17
|
+
quote = char; started = true
|
|
18
|
+
} else if (/\s/.test(char)) {
|
|
19
|
+
if (started) { tokens.push(token); token = ''; started = false }
|
|
20
|
+
} else {
|
|
21
|
+
token += char; started = true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (escaped) token += '\\'
|
|
25
|
+
if (quote !== undefined) throw Object.assign(new Error(`Unclosed ${quote} quote in command`), { exitCode: 2 })
|
|
26
|
+
if (started) tokens.push(token)
|
|
27
|
+
return tokens
|
|
28
|
+
}
|