martty 0.2.11
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/LICENSE +21 -0
- package/README.md +118 -0
- package/bin/dsh-tui.js +68 -0
- package/cordis.patch.yml +30 -0
- package/creator/cordis.patch.yml +6 -0
- package/creator/package.json +10 -0
- package/lib/acp-client-events.js +65 -0
- package/lib/acp-client.js +114 -0
- package/lib/acp-host.js +24 -0
- package/lib/acp-session-config.js +376 -0
- package/lib/acp-session-plan.js +196 -0
- package/lib/acp-session-stats.js +239 -0
- package/lib/agent.js +64 -0
- package/lib/boot.js +119 -0
- package/lib/client-process.js +11 -0
- package/lib/client-run.js +379 -0
- package/lib/cordis-protocol.js +51 -0
- package/lib/creator-overlay.js +77 -0
- package/lib/demo-skin.js +79 -0
- package/lib/ember.js +20 -0
- package/lib/index.js +226 -0
- package/lib/inspect.js +971 -0
- package/lib/jsonrpc-line-transport.js +155 -0
- package/lib/mux.js +281 -0
- package/lib/palettes/default.json +44 -0
- package/lib/palettes/ember.json +44 -0
- package/lib/plan-view.js +92 -0
- package/lib/profile-acp-client.js +11 -0
- package/lib/right-demo.js +55 -0
- package/lib/runner.js +94 -0
- package/lib/spawn-tui.js +179 -0
- package/lib/stats-view.js +90 -0
- package/lib/tui-commands.js +144 -0
- package/lib/tui-overlay.js +252 -0
- package/lib/tui-slots.js +351 -0
- package/lib/tui-theme.js +463 -0
- package/package.json +83 -0
- package/skills/tui-plugin-development/SKILL.md +172 -0
- package/vendor/darwin-arm64/dsh-tui +0 -0
- package/vendor/darwin-x64/dsh-tui +0 -0
- package/vendor/linux-arm64/dsh-tui +0 -0
- package/vendor/linux-x64/dsh-tui +0 -0
- package/vendor/win32-x64/dsh-tui.exe +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/** Standard-ACP-backed token and live prompt timing projection. */
|
|
2
|
+
|
|
3
|
+
import { Service } from '@deepseek-ai/cordis'
|
|
4
|
+
|
|
5
|
+
export const name = 'acp-session-stats'
|
|
6
|
+
export const inject = []
|
|
7
|
+
|
|
8
|
+
const SETUP_METHODS = new Set(['session/new', 'session/load'])
|
|
9
|
+
|
|
10
|
+
class AcpSessionStatsService extends Service {
|
|
11
|
+
constructor(ctx, core) {
|
|
12
|
+
super(ctx, 'acpSessionStats')
|
|
13
|
+
this.core = core
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
current() { return this.core.current() }
|
|
17
|
+
subscribe(listener) { return this.core.subscribe(this.ctx, listener) }
|
|
18
|
+
observeClient(message) { return this.core.observeClient(message) }
|
|
19
|
+
observeAgent(message) { return this.core.observeAgent(message) }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function zero(sessionId) {
|
|
23
|
+
return {
|
|
24
|
+
sessionId,
|
|
25
|
+
usage: {
|
|
26
|
+
input: 0, output: 0, cached: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0,
|
|
27
|
+
},
|
|
28
|
+
stats: {
|
|
29
|
+
turns: 0,
|
|
30
|
+
steps: 0,
|
|
31
|
+
llmMillis: 0,
|
|
32
|
+
toolMillis: 0,
|
|
33
|
+
ttftTotalMillis: 0,
|
|
34
|
+
ttftCount: 0,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function installAcpSessionStats(ctx, options = {}) {
|
|
40
|
+
const now = typeof options.now === 'function' ? options.now : () => performance.now()
|
|
41
|
+
const listeners = new Set()
|
|
42
|
+
const pendingSetup = new Map()
|
|
43
|
+
const pendingPrompts = new Map()
|
|
44
|
+
const activePrompts = new Map()
|
|
45
|
+
const toolStarts = new Map()
|
|
46
|
+
let value = zero(undefined)
|
|
47
|
+
|
|
48
|
+
function current() { return structuredClone(value) }
|
|
49
|
+
|
|
50
|
+
function publish() {
|
|
51
|
+
const snapshot = current()
|
|
52
|
+
for (const listener of [...listeners]) listener(snapshot)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function reset(sessionId) {
|
|
56
|
+
value = zero(sessionId)
|
|
57
|
+
activePrompts.clear()
|
|
58
|
+
pendingPrompts.clear()
|
|
59
|
+
toolStarts.clear()
|
|
60
|
+
publish()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function subscribe(effectCtx, listener) {
|
|
64
|
+
if (typeof listener !== 'function') {
|
|
65
|
+
throw new Error('acpSessionStats.subscribe: listener must be a function')
|
|
66
|
+
}
|
|
67
|
+
const setup = () => {
|
|
68
|
+
listeners.add(listener)
|
|
69
|
+
return () => listeners.delete(listener)
|
|
70
|
+
}
|
|
71
|
+
const release = typeof effectCtx?.effect === 'function'
|
|
72
|
+
? effectCtx.effect(setup, 'acpSessionStats.subscribe')
|
|
73
|
+
: setup()
|
|
74
|
+
let disposed = false
|
|
75
|
+
return () => {
|
|
76
|
+
if (disposed) return
|
|
77
|
+
disposed = true
|
|
78
|
+
return release?.()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function observeClient(message) {
|
|
83
|
+
if (!object(message) || message.id === undefined || typeof message.method !== 'string') return
|
|
84
|
+
if (SETUP_METHODS.has(message.method)) {
|
|
85
|
+
pendingSetup.set(message.id, {
|
|
86
|
+
sessionId: message.method === 'session/load'
|
|
87
|
+
? readString(message.params, 'sessionId', 'session_id')
|
|
88
|
+
: undefined,
|
|
89
|
+
})
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
if (message.method !== 'session/prompt') return
|
|
93
|
+
const sessionId = readString(message.params, 'sessionId', 'session_id')
|
|
94
|
+
if (sessionId === undefined) return
|
|
95
|
+
if (value.sessionId === undefined) value.sessionId = sessionId
|
|
96
|
+
if (value.sessionId !== sessionId) return
|
|
97
|
+
const prompt = { sessionId, started: now(), firstToken: undefined, toolMillis: 0 }
|
|
98
|
+
pendingPrompts.set(message.id, prompt)
|
|
99
|
+
activePrompts.set(sessionId, prompt)
|
|
100
|
+
value.stats.turns += 1
|
|
101
|
+
publish()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function observeAgent(message) {
|
|
105
|
+
if (!object(message)) return
|
|
106
|
+
if (message.id !== undefined && pendingSetup.has(message.id)) {
|
|
107
|
+
const tracked = pendingSetup.get(message.id)
|
|
108
|
+
pendingSetup.delete(message.id)
|
|
109
|
+
if (message.error !== undefined || !object(message.result)) return
|
|
110
|
+
reset(readString(message.result, 'sessionId', 'session_id') ?? tracked.sessionId)
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if (message.id !== undefined && pendingPrompts.has(message.id)) {
|
|
114
|
+
const prompt = pendingPrompts.get(message.id)
|
|
115
|
+
pendingPrompts.delete(message.id)
|
|
116
|
+
activePrompts.delete(prompt.sessionId)
|
|
117
|
+
if (message.error === undefined && object(message.result)) {
|
|
118
|
+
addUsage(message.result.usage)
|
|
119
|
+
}
|
|
120
|
+
const elapsed = Math.max(0, now() - prompt.started)
|
|
121
|
+
value.stats.llmMillis += Math.max(0, elapsed - prompt.toolMillis)
|
|
122
|
+
publish()
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
if (message.method !== 'session/update' || !object(message.params)) return
|
|
126
|
+
const sessionId = readString(message.params, 'sessionId', 'session_id')
|
|
127
|
+
if (value.sessionId !== undefined && sessionId !== value.sessionId) return
|
|
128
|
+
if (value.sessionId === undefined) value.sessionId = sessionId
|
|
129
|
+
const update = message.params.update
|
|
130
|
+
if (!object(update)) return
|
|
131
|
+
|
|
132
|
+
const replay = update?._meta?.dsh
|
|
133
|
+
if (object(replay) && replay.event === 'prompt/usage') {
|
|
134
|
+
value.usage = usageOf(replay.usage)
|
|
135
|
+
publish()
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const type = readString(update, 'sessionUpdate', 'session_update')
|
|
140
|
+
const prompt = sessionId === undefined ? undefined : activePrompts.get(sessionId)
|
|
141
|
+
if (prompt !== undefined && prompt.firstToken === undefined
|
|
142
|
+
&& (type === 'agent_message_chunk' || type === 'agent_thought_chunk')
|
|
143
|
+
&& textOf(update.content).length > 0) {
|
|
144
|
+
prompt.firstToken = now()
|
|
145
|
+
value.stats.ttftTotalMillis += Math.max(0, prompt.firstToken - prompt.started)
|
|
146
|
+
value.stats.ttftCount += 1
|
|
147
|
+
}
|
|
148
|
+
if (type === 'agent_message_chunk'
|
|
149
|
+
&& update?._meta?.dsh?.event === 'assistant_message') {
|
|
150
|
+
value.stats.steps += 1
|
|
151
|
+
publish()
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
const callId = readString(update, 'toolCallId', 'tool_call_id')
|
|
155
|
+
if (type === 'tool_call' && callId !== undefined) {
|
|
156
|
+
toolStarts.set(`${sessionId ?? ''}\u0000${callId}`, now())
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
if (type === 'tool_call_update' && callId !== undefined
|
|
160
|
+
&& ['completed', 'failed'].includes(update.status)) {
|
|
161
|
+
const key = `${sessionId ?? ''}\u0000${callId}`
|
|
162
|
+
const started = toolStarts.get(key)
|
|
163
|
+
toolStarts.delete(key)
|
|
164
|
+
if (started === undefined) return
|
|
165
|
+
const duration = Math.max(0, now() - started)
|
|
166
|
+
value.stats.toolMillis += duration
|
|
167
|
+
if (prompt !== undefined) prompt.toolMillis += duration
|
|
168
|
+
publish()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function addUsage(usage) {
|
|
173
|
+
const next = usageOf(usage)
|
|
174
|
+
value.usage.input += next.input
|
|
175
|
+
value.usage.output += next.output
|
|
176
|
+
value.usage.cached += next.cached
|
|
177
|
+
value.usage.cacheRead += next.cacheRead
|
|
178
|
+
value.usage.cacheWrite += next.cacheWrite
|
|
179
|
+
value.usage.reasoning += next.reasoning
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const core = { current, subscribe, observeClient, observeAgent }
|
|
183
|
+
const service = typeof ctx.provide === 'function'
|
|
184
|
+
? new AcpSessionStatsService(ctx, core)
|
|
185
|
+
: {
|
|
186
|
+
current,
|
|
187
|
+
subscribe(listener) { return subscribe(ctx, listener) },
|
|
188
|
+
observeClient,
|
|
189
|
+
observeAgent,
|
|
190
|
+
}
|
|
191
|
+
if (typeof ctx.provide !== 'function') ctx.acpSessionStats = service
|
|
192
|
+
return service
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function usageOf(value) {
|
|
196
|
+
if (!object(value)) {
|
|
197
|
+
return { input: 0, output: 0, cached: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0 }
|
|
198
|
+
}
|
|
199
|
+
const cacheRead = number(
|
|
200
|
+
value.cachedReadTokens ?? value.cacheReadTokens ?? value.cached_read_tokens,
|
|
201
|
+
)
|
|
202
|
+
const cacheWrite = number(
|
|
203
|
+
value.cachedWriteTokens ?? value.cacheWriteTokens ?? value.cached_write_tokens,
|
|
204
|
+
)
|
|
205
|
+
return {
|
|
206
|
+
input: number(value.inputTokens ?? value.input_tokens),
|
|
207
|
+
output: number(value.outputTokens ?? value.output_tokens),
|
|
208
|
+
cached: cacheRead + cacheWrite,
|
|
209
|
+
cacheRead,
|
|
210
|
+
cacheWrite,
|
|
211
|
+
reasoning: number(
|
|
212
|
+
value.thoughtTokens ?? value.reasoningTokens ?? value.thought_tokens ?? value.reasoning_tokens,
|
|
213
|
+
),
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function textOf(value) {
|
|
218
|
+
return object(value) && typeof value.text === 'string' ? value.text : ''
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function number(value) {
|
|
222
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function readString(value, ...keys) {
|
|
226
|
+
if (!object(value)) return undefined
|
|
227
|
+
for (const key of keys) {
|
|
228
|
+
if (typeof value[key] === 'string' && value[key].length > 0) return value[key]
|
|
229
|
+
}
|
|
230
|
+
return undefined
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function object(value) {
|
|
234
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function apply(ctx, options = {}) {
|
|
238
|
+
installAcpSessionStats(ctx, options)
|
|
239
|
+
}
|
package/lib/agent.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Resolve the default ACP agent used by the standalone TUI entry. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
4
|
+
import { createRequire } from 'node:module'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
6
|
+
import { dirname, join, resolve } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the ACP package and TUI's internal Creator overlay bundle.
|
|
11
|
+
* @param {string | URL} [anchor]
|
|
12
|
+
* @returns {{ command: string, args: string[] }}
|
|
13
|
+
*/
|
|
14
|
+
export function resolveDependencyStack(anchor = import.meta.url) {
|
|
15
|
+
const req = createRequire(anchor)
|
|
16
|
+
const acpPackageJson = req.resolve('@openma/deepseek-harness-acp/package.json')
|
|
17
|
+
const creatorBundle = fileURLToPath(new URL('../creator', import.meta.url))
|
|
18
|
+
const manifest = JSON.parse(readFileSync(acpPackageJson, 'utf8'))
|
|
19
|
+
const declared = typeof manifest.bin === 'string'
|
|
20
|
+
? manifest.bin
|
|
21
|
+
: manifest.bin?.['dsh-acp']
|
|
22
|
+
if (typeof declared !== 'string' || declared.length === 0) {
|
|
23
|
+
throw new Error('@openma/deepseek-harness-acp declares no dsh-acp bin')
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
command: resolve(dirname(acpPackageJson), declared),
|
|
27
|
+
args: ['--bundle', creatorBundle],
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve the standalone ACP command without affecting the profile path.
|
|
33
|
+
* @param {string | URL} [anchor]
|
|
34
|
+
* @returns {{ command: string, args: string[] }}
|
|
35
|
+
*/
|
|
36
|
+
export function resolveStackedAgent(anchor = import.meta.url) {
|
|
37
|
+
const envCmd = process.env.DSH_TUI_AGENT
|
|
38
|
+
if (typeof envCmd === 'string' && envCmd.trim().length > 0) {
|
|
39
|
+
const tokens = envCmd.trim().split(/\s+/)
|
|
40
|
+
return { command: tokens[0], args: tokens.slice(1) }
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return resolveDependencyStack(anchor)
|
|
44
|
+
} catch {
|
|
45
|
+
// Source checkouts may not have materialized the package dependencies.
|
|
46
|
+
}
|
|
47
|
+
const home = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
48
|
+
for (const profile of ['tui-test', 'tui']) {
|
|
49
|
+
const bin = join(home, 'profiles', profile, 'node_modules', '.bin', 'dsh-acp')
|
|
50
|
+
const creator = join(
|
|
51
|
+
home,
|
|
52
|
+
'profiles',
|
|
53
|
+
profile,
|
|
54
|
+
'node_modules',
|
|
55
|
+
'@openma',
|
|
56
|
+
'deepseek-harness-tui',
|
|
57
|
+
'creator',
|
|
58
|
+
)
|
|
59
|
+
if (existsSync(bin) && existsSync(join(creator, 'package.json'))) {
|
|
60
|
+
return { command: bin, args: ['--bundle', creator] }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { command: 'dsh-acp', args: [] }
|
|
64
|
+
}
|
package/lib/boot.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cordis client boot: tuiTheme + tuiSlots + acp-client + TUI shell.
|
|
3
|
+
*
|
|
4
|
+
* The process root is a client tree. A profile launch injects Host ACP stdio;
|
|
5
|
+
* standalone launches may spawn an agent instead. This module does not import
|
|
6
|
+
* dsh or dsh-acp.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { apply as applyAcpClient } from './acp-client.js'
|
|
10
|
+
import { apply as applyCordisClientRunner } from './inspect.js'
|
|
11
|
+
import { apply as applyShell } from './index.js'
|
|
12
|
+
import { resolveStackedAgent } from './agent.js'
|
|
13
|
+
import { apply as applySlots } from './tui-slots.js'
|
|
14
|
+
import { apply as applyTheme } from './tui-theme.js'
|
|
15
|
+
import { apply as applyCommands } from './tui-commands.js'
|
|
16
|
+
import { apply as applyOverlay } from './tui-overlay.js'
|
|
17
|
+
import { apply as applyPlanView, inject as planViewInject } from './plan-view.js'
|
|
18
|
+
import { apply as applyStatsView, inject as statsViewInject } from './stats-view.js'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} [options]
|
|
22
|
+
* @param {{ command: string, args?: string[] }} [options.agent]
|
|
23
|
+
* @param {{ stdin: import('node:stream').Writable, stdout: import('node:stream').Readable, child?: import('node:child_process').ChildProcess }} [options.stream]
|
|
24
|
+
* @param {{ stdin: number | 'inherit', stdout: number | 'inherit' }} [options.tty]
|
|
25
|
+
*/
|
|
26
|
+
export async function bootClient(options = {}) {
|
|
27
|
+
const { Context } = await import('@deepseek-ai/cordis')
|
|
28
|
+
const ctx = new Context()
|
|
29
|
+
const acpConfig = options.stream !== undefined
|
|
30
|
+
? { stream: options.stream }
|
|
31
|
+
: { agent: options.agent ?? resolveStackedAgent() }
|
|
32
|
+
if (typeof ctx.plugin === 'function') {
|
|
33
|
+
await ctx.plugin({ name: 'tui-theme', inject: [], apply: applyTheme })
|
|
34
|
+
await ctx.plugin({ name: 'tui-slots', inject: [], apply: applySlots })
|
|
35
|
+
await ctx.plugin({ name: 'tui-commands', inject: [], apply: applyCommands })
|
|
36
|
+
await ctx.plugin({ name: 'tui-overlay', inject: [], apply: applyOverlay })
|
|
37
|
+
await ctx.plugin({ name: 'acp-client', inject: [], apply: applyAcpClient }, acpConfig)
|
|
38
|
+
await ctx.plugin({ name: 'plan-view', inject: planViewInject, apply: applyPlanView })
|
|
39
|
+
await ctx.plugin({ name: 'stats-view', inject: statsViewInject, apply: applyStatsView })
|
|
40
|
+
await ctx.plugin({
|
|
41
|
+
name: 'tui-cordis-client-runner',
|
|
42
|
+
inject: [
|
|
43
|
+
'tuiTheme', 'tuiSlots', 'tuiCommands', 'tuiOverlay', 'acpSessionConfig',
|
|
44
|
+
'acpSessionPlan', 'acpSessionStats',
|
|
45
|
+
],
|
|
46
|
+
apply: applyCordisClientRunner,
|
|
47
|
+
})
|
|
48
|
+
await ctx.plugin(
|
|
49
|
+
{
|
|
50
|
+
name: 'dsh-tui-shell',
|
|
51
|
+
inject: [
|
|
52
|
+
'acpClient', 'tuiTheme', 'tuiSlots', 'tuiCommands', 'tuiOverlay',
|
|
53
|
+
'acpClientEvents', 'acpSessionConfig', 'tuiCordisClientRunner',
|
|
54
|
+
],
|
|
55
|
+
apply: applyShell,
|
|
56
|
+
},
|
|
57
|
+
{ extraArgs: options.extraArgs ?? [], tty: options.tty },
|
|
58
|
+
)
|
|
59
|
+
} else {
|
|
60
|
+
applyTheme(ctx)
|
|
61
|
+
applySlots(ctx)
|
|
62
|
+
applyCommands(ctx)
|
|
63
|
+
applyOverlay(ctx)
|
|
64
|
+
applyAcpClient(ctx, acpConfig)
|
|
65
|
+
applyPlanView(ctx)
|
|
66
|
+
applyStatsView(ctx)
|
|
67
|
+
applyCordisClientRunner(ctx)
|
|
68
|
+
await applyShell(ctx, { extraArgs: options.extraArgs ?? [], tty: options.tty })
|
|
69
|
+
}
|
|
70
|
+
return ctx
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse `--agent` / `--agent-arg` from argv. Remaining flags pass through to Rust.
|
|
75
|
+
* `--agent` is also forwarded via {@link painterArgs} so Terminal Auth can
|
|
76
|
+
* re-exec the same command.
|
|
77
|
+
* @param {string[]} argv
|
|
78
|
+
* @returns {{ agent: { command: string, args: string[] }, rustArgs: string[] }}
|
|
79
|
+
*/
|
|
80
|
+
export function parseClientArgv(argv) {
|
|
81
|
+
const rustArgs = []
|
|
82
|
+
let command
|
|
83
|
+
const args = []
|
|
84
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
85
|
+
const token = argv[i]
|
|
86
|
+
if (token === '--agent') {
|
|
87
|
+
command = argv[i + 1]
|
|
88
|
+
i += 1
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
if (token === '--agent-arg') {
|
|
92
|
+
args.push(argv[i + 1] ?? '')
|
|
93
|
+
i += 1
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
rustArgs.push(token)
|
|
97
|
+
}
|
|
98
|
+
if (command === undefined) {
|
|
99
|
+
return { agent: resolveStackedAgent(), rustArgs }
|
|
100
|
+
}
|
|
101
|
+
return { agent: { command, args }, rustArgs }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Extra argv for the native painter: user flags plus `--agent` so `/auth`
|
|
106
|
+
* can launch `{command} login` the same way Backchat launches Terminal Auth.
|
|
107
|
+
* @param {{ agent: { command: string, args?: string[] }, rustArgs: string[] }} parsed
|
|
108
|
+
* @returns {string[]}
|
|
109
|
+
*/
|
|
110
|
+
export function painterArgs(parsed) {
|
|
111
|
+
const flags = []
|
|
112
|
+
if (parsed.agent?.command) {
|
|
113
|
+
flags.push('--agent', parsed.agent.command)
|
|
114
|
+
for (const arg of parsed.agent.args ?? []) {
|
|
115
|
+
flags.push('--agent-arg', arg)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return [...parsed.rustArgs, ...flags]
|
|
119
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/** Independent TUI Client Cordis process launched by the Host runner. */
|
|
4
|
+
|
|
5
|
+
import { bootClient } from './boot.js'
|
|
6
|
+
|
|
7
|
+
await bootClient({
|
|
8
|
+
stream: { stdin: process.stdout, stdout: process.stdin },
|
|
9
|
+
extraArgs: process.argv.slice(2),
|
|
10
|
+
tty: { stdin: 3, stdout: 4 },
|
|
11
|
+
})
|