martty 0.2.30 → 0.2.31
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 +20 -1
- package/bin/martty.js +30 -1
- package/lib/acp-session-status.js +48 -5
- package/lib/agent.js +9 -1
- package/lib/boot.js +32 -3
- package/lib/harness-view.js +61 -0
- package/lib/harnesses.js +199 -0
- package/lib/inspect.js +3 -1
- package/package.json +3 -3
- 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
package/README.md
CHANGED
|
@@ -64,7 +64,26 @@ profiles remain compatible.
|
|
|
64
64
|
The profile Host mounts the ACP plugin on Base, then starts a separate TUI
|
|
65
65
|
Client process over standard ACP stdin/stdout. For standalone use, run
|
|
66
66
|
`martty` and use `--agent <cmd>` plus repeated `--agent-arg <arg>` for another
|
|
67
|
-
ACP server.
|
|
67
|
+
ACP server. Named standalone harnesses can also be discovered, saved, and selected:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
martty harness list
|
|
71
|
+
martty harness add local --command local-acp --arg --stdio
|
|
72
|
+
martty harness use local
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The same registry is available through three entry points: edit
|
|
76
|
+
`$MARTTY_HOME/settings.json`, use `martty harness`, or run `/harness` (or
|
|
77
|
+
`/harness <id>`) inside the TUI. A saved choice takes effect on the next
|
|
78
|
+
standalone launch, which starts a fresh ACP session. It does not replace a
|
|
79
|
+
running or profile-owned Host, and it never carries a session across Harnesses.
|
|
80
|
+
|
|
81
|
+
The selected entry is stored in `$MARTTY_HOME/settings.json` and takes effect
|
|
82
|
+
on the next standalone launch through a new `session/new`. `--agent` and
|
|
83
|
+
`DSH_TUI_AGENT` remain higher priority. This does not replace the Host-owned
|
|
84
|
+
runtime or session of `dsh --profile martty`.
|
|
85
|
+
|
|
86
|
+
The Node Client process owns a Cordis tree and starts the Rust painter. A sibling
|
|
68
87
|
`tui-cordis-client-runner` publishes TUI Client capabilities and evaluates
|
|
69
88
|
approved `code.client` packages from `dsh-tool-cordis` against that client tree.
|
|
70
89
|
|
package/bin/martty.js
CHANGED
|
@@ -4,7 +4,9 @@ import fs from 'node:fs'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { spawnSync } from 'node:child_process'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
|
-
import {
|
|
7
|
+
import { resolveDependencyStack } from '../lib/agent.js'
|
|
8
|
+
import { bootClient, parseClientArgv, painterArgs, uiSettingsPath } from '../lib/boot.js'
|
|
9
|
+
import { runHarnessCommand } from '../lib/harnesses.js'
|
|
8
10
|
|
|
9
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
12
|
const platformKey = process.platform + '-' + process.arch
|
|
@@ -24,6 +26,33 @@ const wantDemo = argv.includes('--demo')
|
|
|
24
26
|
const wantHelp = argv.includes('-h') || argv.includes('--help')
|
|
25
27
|
const wantVersion = argv.includes('-V') || argv.includes('--version')
|
|
26
28
|
|
|
29
|
+
if (argv[0] === 'harness') {
|
|
30
|
+
let defaults = []
|
|
31
|
+
try {
|
|
32
|
+
const agent = resolveDependencyStack()
|
|
33
|
+
defaults = [{
|
|
34
|
+
id: 'builtin-dsh',
|
|
35
|
+
label: 'Bundled DeepSeek Harness',
|
|
36
|
+
...agent,
|
|
37
|
+
source: 'builtin',
|
|
38
|
+
}]
|
|
39
|
+
} catch {
|
|
40
|
+
// Source checkouts without installed dependencies still list PATH entries.
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const result = runHarnessCommand(argv.slice(1), {
|
|
44
|
+
settingsPath: uiSettingsPath(),
|
|
45
|
+
defaults,
|
|
46
|
+
})
|
|
47
|
+
if (result.stdout) process.stdout.write(result.stdout)
|
|
48
|
+
if (result.stderr) process.stderr.write(result.stderr)
|
|
49
|
+
process.exit(result.code)
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`martty harness: ${error instanceof Error ? error.message : String(error)}`)
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
if (!fs.existsSync(binaryPath)) {
|
|
28
57
|
let available = []
|
|
29
58
|
try {
|
|
@@ -16,9 +16,15 @@ export const inject = ['acpClientEvents', 'acpSessionConfig']
|
|
|
16
16
|
const AUTH_REQUIRED_CODE = -32000
|
|
17
17
|
const SETUP_METHODS = new Set(['session/new', 'session/load'])
|
|
18
18
|
const RUNNING_UPDATE_TYPES = new Set([
|
|
19
|
+
'user_message_chunk',
|
|
19
20
|
'agent_message_chunk',
|
|
20
21
|
'agent_thought_chunk',
|
|
21
22
|
'tool_call',
|
|
23
|
+
'tool_call_update',
|
|
24
|
+
'plan',
|
|
25
|
+
'plan_update',
|
|
26
|
+
'plan_removed',
|
|
27
|
+
'usage_update',
|
|
22
28
|
])
|
|
23
29
|
|
|
24
30
|
class AcpSessionStatusService extends Service {
|
|
@@ -58,6 +64,7 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
58
64
|
let initializeId
|
|
59
65
|
let pendingAuthenticate = new Set()
|
|
60
66
|
const pendingSetup = new Map()
|
|
67
|
+
const pendingPrompts = new Map()
|
|
61
68
|
let permissionPreset
|
|
62
69
|
let sandboxMode
|
|
63
70
|
|
|
@@ -108,9 +115,15 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
108
115
|
)
|
|
109
116
|
return
|
|
110
117
|
}
|
|
111
|
-
if (message.method === 'session/prompt' &&
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
if (message.method === 'session/prompt' && message.id !== undefined) {
|
|
119
|
+
pendingPrompts.set(
|
|
120
|
+
message.id,
|
|
121
|
+
readString(message.params, 'sessionId', 'session_id'),
|
|
122
|
+
)
|
|
123
|
+
if (value.state === 'idle') {
|
|
124
|
+
value.state = 'starting'
|
|
125
|
+
publish()
|
|
126
|
+
}
|
|
114
127
|
}
|
|
115
128
|
}
|
|
116
129
|
|
|
@@ -155,6 +168,21 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
155
168
|
publish()
|
|
156
169
|
return
|
|
157
170
|
}
|
|
171
|
+
if (response(message) && pendingPrompts.has(message.id)) {
|
|
172
|
+
pendingPrompts.delete(message.id)
|
|
173
|
+
let changed = false
|
|
174
|
+
if (message.error !== undefined && isAuthRequired(message.error)
|
|
175
|
+
&& value.auth.status !== 'needs sign-in') {
|
|
176
|
+
value.auth.status = 'needs sign-in'
|
|
177
|
+
changed = true
|
|
178
|
+
}
|
|
179
|
+
if (pendingPrompts.size === 0 && value.state !== 'idle') {
|
|
180
|
+
value.state = 'idle'
|
|
181
|
+
changed = true
|
|
182
|
+
}
|
|
183
|
+
if (changed) publish()
|
|
184
|
+
return
|
|
185
|
+
}
|
|
158
186
|
if (message.error !== undefined && isAuthRequired(message.error)) {
|
|
159
187
|
value.auth.status = 'needs sign-in'
|
|
160
188
|
publish()
|
|
@@ -163,7 +191,7 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
163
191
|
|
|
164
192
|
if (message.method === 'session.status' && object(message.params)) {
|
|
165
193
|
const status = readString(message.params, 'status')
|
|
166
|
-
if (status === 'running' || status === 'idle') {
|
|
194
|
+
if (status === 'running' || (status === 'idle' && pendingPrompts.size === 0)) {
|
|
167
195
|
value.state = status
|
|
168
196
|
publish()
|
|
169
197
|
}
|
|
@@ -200,14 +228,24 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
200
228
|
return
|
|
201
229
|
}
|
|
202
230
|
if (message.method !== 'session/update' || !object(message.params)) return
|
|
231
|
+
const sessionId = readString(message.params, 'sessionId', 'session_id')
|
|
203
232
|
const update = object(message.params.update) ? message.params.update : undefined
|
|
204
233
|
const type = readString(update, 'sessionUpdate', 'session_update')
|
|
205
|
-
if (value.state
|
|
234
|
+
if (value.state !== 'running' && hasPendingPrompt(sessionId)
|
|
235
|
+
&& RUNNING_UPDATE_TYPES.has(type)) {
|
|
206
236
|
value.state = 'running'
|
|
207
237
|
publish()
|
|
208
238
|
}
|
|
209
239
|
}
|
|
210
240
|
|
|
241
|
+
function hasPendingPrompt(sessionId) {
|
|
242
|
+
if (sessionId === undefined) return false
|
|
243
|
+
for (const pendingSessionId of pendingPrompts.values()) {
|
|
244
|
+
if (pendingSessionId === sessionId) return true
|
|
245
|
+
}
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
|
|
211
249
|
function onConfigSnapshot(snapshot) {
|
|
212
250
|
if (snapshot === null || typeof snapshot !== 'object') return
|
|
213
251
|
if (typeof snapshot.sessionId === 'string' && snapshot.sessionId.length > 0
|
|
@@ -271,6 +309,11 @@ function object(value) {
|
|
|
271
309
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
272
310
|
}
|
|
273
311
|
|
|
312
|
+
function response(message) {
|
|
313
|
+
return !Object.hasOwn(message, 'method')
|
|
314
|
+
&& (Object.hasOwn(message, 'result') || Object.hasOwn(message, 'error'))
|
|
315
|
+
}
|
|
316
|
+
|
|
274
317
|
export function apply(ctx, options = {}) {
|
|
275
318
|
installAcpSessionStatus(ctx, options)
|
|
276
319
|
}
|
package/lib/agent.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createRequire } from 'node:module'
|
|
|
5
5
|
import { homedir } from 'node:os'
|
|
6
6
|
import { dirname, join, resolve } from 'node:path'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { selectedHarness } from './harnesses.js'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Resolve the ACP package and TUI's internal Creator overlay bundle.
|
|
@@ -31,14 +32,21 @@ export function resolveDependencyStack(anchor = import.meta.url) {
|
|
|
31
32
|
/**
|
|
32
33
|
* Resolve the standalone ACP command without affecting the profile path.
|
|
33
34
|
* @param {string | URL} [anchor]
|
|
35
|
+
* @param {{ settingsPath?: string }} [options]
|
|
34
36
|
* @returns {{ command: string, args: string[] }}
|
|
35
37
|
*/
|
|
36
|
-
export function resolveStackedAgent(anchor = import.meta.url) {
|
|
38
|
+
export function resolveStackedAgent(anchor = import.meta.url, options = {}) {
|
|
37
39
|
const envCmd = process.env.DSH_TUI_AGENT
|
|
38
40
|
if (typeof envCmd === 'string' && envCmd.trim().length > 0) {
|
|
39
41
|
const tokens = envCmd.trim().split(/\s+/)
|
|
40
42
|
return { command: tokens[0], args: tokens.slice(1) }
|
|
41
43
|
}
|
|
44
|
+
if (typeof options.settingsPath === 'string') {
|
|
45
|
+
const selected = selectedHarness(options.settingsPath)
|
|
46
|
+
if (selected !== undefined) {
|
|
47
|
+
return { command: selected.command, args: selected.args }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
42
50
|
try {
|
|
43
51
|
return resolveDependencyStack(anchor)
|
|
44
52
|
} catch {
|
package/lib/boot.js
CHANGED
|
@@ -33,7 +33,7 @@ import { homedir } from 'node:os'
|
|
|
33
33
|
import path from 'node:path'
|
|
34
34
|
import { apply as applyCordisClientRunner } from './inspect.js'
|
|
35
35
|
import { apply as applyShell } from './index.js'
|
|
36
|
-
import { resolveStackedAgent } from './agent.js'
|
|
36
|
+
import { resolveDependencyStack, resolveStackedAgent } from './agent.js'
|
|
37
37
|
import { apply as applySlots } from './tui-slots.js'
|
|
38
38
|
import { apply as applyTheme } from './tui-theme.js'
|
|
39
39
|
import { apply as applyAyu, inject as ayuInject } from './ayu.js'
|
|
@@ -56,6 +56,7 @@ import { apply as applyQueueView, inject as queueViewInject } from './queue-view
|
|
|
56
56
|
import { apply as applyStatsView, inject as statsViewInject } from './stats-view.js'
|
|
57
57
|
import { apply as applySessionStatus, inject as sessionStatusInject } from './acp-session-status.js'
|
|
58
58
|
import { apply as applyStatusView, inject as statusViewInject } from './status-view.js'
|
|
59
|
+
import { apply as applyHarnessView, inject as harnessViewInject } from './harness-view.js'
|
|
59
60
|
import { apply as applyDeepseekLogo, inject as deepseekLogoInject } from './deepseek-logo.js'
|
|
60
61
|
import { createTuiPluginStore, marttyHome } from './tui-plugin-store.js'
|
|
61
62
|
import { installTuiLocalPlugins } from './tui-local-plugins.js'
|
|
@@ -67,6 +68,8 @@ import { installTuiLocalPlugins } from './tui-local-plugins.js'
|
|
|
67
68
|
* @param {{ stdin: number | 'inherit', stdout: number | 'inherit' }} [options.tty]
|
|
68
69
|
* @param {string} [options.settingsPath]
|
|
69
70
|
* @param {string} [options.artifactRoot]
|
|
71
|
+
* @param {Array<{ id: string, label: string, command: string, args?: string[], source?: string }>} [options.harnessDefaults]
|
|
72
|
+
* @param {string} [options.harnessPathValue]
|
|
70
73
|
* @param {Array<{ id: string, kind: 'theme' | 'ui', entry: string }>} [options.packagePlugins]
|
|
71
74
|
*/
|
|
72
75
|
export async function bootClient(options = {}) {
|
|
@@ -80,6 +83,25 @@ export async function bootClient(options = {}) {
|
|
|
80
83
|
migrateLegacyUiSettings(settingsPath, legacyUiSettingsPaths(options.extraArgs ?? []))
|
|
81
84
|
}
|
|
82
85
|
const presetConfig = { settingsPath }
|
|
86
|
+
let harnessDefaults = options.harnessDefaults
|
|
87
|
+
if (harnessDefaults === undefined) {
|
|
88
|
+
try {
|
|
89
|
+
harnessDefaults = [{
|
|
90
|
+
id: 'builtin-dsh',
|
|
91
|
+
label: 'Bundled DeepSeek Harness',
|
|
92
|
+
...resolveDependencyStack(),
|
|
93
|
+
source: 'builtin',
|
|
94
|
+
}]
|
|
95
|
+
} catch {
|
|
96
|
+
harnessDefaults = []
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const harnessConfig = {
|
|
100
|
+
settingsPath,
|
|
101
|
+
defaults: harnessDefaults,
|
|
102
|
+
pathValue: options.harnessPathValue,
|
|
103
|
+
hostOwned: options.stream !== undefined,
|
|
104
|
+
}
|
|
83
105
|
if (typeof ctx.plugin === 'function') {
|
|
84
106
|
await ctx.plugin({ name: 'tui-theme', inject: [], apply: applyTheme }, presetConfig)
|
|
85
107
|
await ctx.plugin({ name: 'tui-theme-ayu', inject: ayuInject, apply: applyAyu })
|
|
@@ -105,6 +127,7 @@ export async function bootClient(options = {}) {
|
|
|
105
127
|
await ctx.plugin({ name: 'stats-view', inject: statsViewInject, apply: applyStatsView })
|
|
106
128
|
await ctx.plugin({ name: 'acp-session-status', inject: sessionStatusInject, apply: applySessionStatus })
|
|
107
129
|
await ctx.plugin({ name: 'status-view', inject: statusViewInject, apply: applyStatusView })
|
|
130
|
+
await ctx.plugin({ name: 'harness-view', inject: harnessViewInject, apply: applyHarnessView }, harnessConfig)
|
|
108
131
|
await ctx.plugin({ name: 'deepseek-logo', inject: deepseekLogoInject, apply: applyDeepseekLogo })
|
|
109
132
|
const localPlugins = installTuiLocalPlugins(ctx, {
|
|
110
133
|
store: createTuiPluginStore({ root: options.artifactRoot }),
|
|
@@ -165,6 +188,7 @@ export async function bootClient(options = {}) {
|
|
|
165
188
|
applyStatsView(ctx)
|
|
166
189
|
applySessionStatus(ctx)
|
|
167
190
|
applyStatusView(ctx)
|
|
191
|
+
applyHarnessView(ctx, harnessConfig)
|
|
168
192
|
applyDeepseekLogo(ctx)
|
|
169
193
|
const localPlugins = installTuiLocalPlugins(ctx, {
|
|
170
194
|
store: createTuiPluginStore({ root: options.artifactRoot }),
|
|
@@ -271,9 +295,10 @@ export function migrateLegacyUiSettings(settingsPath, legacyPaths) {
|
|
|
271
295
|
* `--agent` is also forwarded via {@link painterArgs} so Terminal Auth can
|
|
272
296
|
* re-exec the same command.
|
|
273
297
|
* @param {string[]} argv
|
|
298
|
+
* @param {{ settingsPath?: string }} [options]
|
|
274
299
|
* @returns {{ agent: { command: string, args: string[] }, rustArgs: string[] }}
|
|
275
300
|
*/
|
|
276
|
-
export function parseClientArgv(argv) {
|
|
301
|
+
export function parseClientArgv(argv, options = {}) {
|
|
277
302
|
const rustArgs = []
|
|
278
303
|
let command
|
|
279
304
|
const args = []
|
|
@@ -292,7 +317,11 @@ export function parseClientArgv(argv) {
|
|
|
292
317
|
rustArgs.push(token)
|
|
293
318
|
}
|
|
294
319
|
if (command === undefined) {
|
|
295
|
-
|
|
320
|
+
const settingsPath = options.settingsPath ?? uiSettingsPath(rustArgs)
|
|
321
|
+
return {
|
|
322
|
+
agent: resolveStackedAgent(import.meta.url, { settingsPath }),
|
|
323
|
+
rustArgs,
|
|
324
|
+
}
|
|
296
325
|
}
|
|
297
326
|
return { agent: { command, args }, rustArgs }
|
|
298
327
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Built-in Client Plugin: configure the next standalone ACP harness. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
activateHarness,
|
|
5
|
+
discoverHarnesses,
|
|
6
|
+
selectedHarness,
|
|
7
|
+
upsertHarness,
|
|
8
|
+
} from './harnesses.js'
|
|
9
|
+
|
|
10
|
+
export const name = 'harness-view'
|
|
11
|
+
export const inject = ['tuiCommands', 'tuiOverlay']
|
|
12
|
+
|
|
13
|
+
function entryOptions(settingsPath, options) {
|
|
14
|
+
return discoverHarnesses(settingsPath, options).map((entry) => ({
|
|
15
|
+
value: entry.id,
|
|
16
|
+
label: entry.label,
|
|
17
|
+
description: `${entry.source} · ${[entry.command, ...entry.args]
|
|
18
|
+
.map((part) => /\s/.test(part) ? JSON.stringify(part) : part)
|
|
19
|
+
.join(' ')}`,
|
|
20
|
+
}))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function apply(ctx, options = {}) {
|
|
24
|
+
const settingsPath = options.settingsPath
|
|
25
|
+
const choices = () => entryOptions(settingsPath, options)
|
|
26
|
+
const save = (id) => {
|
|
27
|
+
const entry = discoverHarnesses(settingsPath, options).find(({ id: candidate }) => candidate === id)
|
|
28
|
+
if (entry === undefined) throw new Error(`unknown harness ${JSON.stringify(id)}`)
|
|
29
|
+
upsertHarness(settingsPath, entry)
|
|
30
|
+
activateHarness(settingsPath, id)
|
|
31
|
+
ctx.tuiOverlay.openView({
|
|
32
|
+
id: 'harness-saved',
|
|
33
|
+
title: 'Harness saved',
|
|
34
|
+
nodes: [{
|
|
35
|
+
id: 'notice',
|
|
36
|
+
kind: 'notice',
|
|
37
|
+
level: 'info',
|
|
38
|
+
text: options.hostOwned
|
|
39
|
+
? `${entry.label} is saved for a new standalone session. The current dsh profile and session remain Host-owned.`
|
|
40
|
+
: `${entry.label} will start in a new session after restarting standalone Martty.`,
|
|
41
|
+
}],
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
const command = ctx.tuiCommands.register({
|
|
45
|
+
name: 'harness',
|
|
46
|
+
description: 'Switch harness in a new session on the next standalone launch',
|
|
47
|
+
input: { hint: '[id]', options: choices() },
|
|
48
|
+
}, async (args) => {
|
|
49
|
+
const requested = args.trim()
|
|
50
|
+
if (requested.length > 0) return save(requested)
|
|
51
|
+
const entries = choices()
|
|
52
|
+
const selected = selectedHarness(settingsPath)?.id
|
|
53
|
+
ctx.tuiOverlay.openSelect({
|
|
54
|
+
id: 'harness',
|
|
55
|
+
title: 'Harness · next standalone session',
|
|
56
|
+
value: entries.some(({ value }) => value === selected) ? selected : entries[0].value,
|
|
57
|
+
options: entries,
|
|
58
|
+
}, { onSubmit: save })
|
|
59
|
+
})
|
|
60
|
+
return () => command?.()
|
|
61
|
+
}
|
package/lib/harnesses.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
statSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
|
|
12
|
+
const HARNESS_ID = /^[a-z0-9][a-z0-9-]*$/
|
|
13
|
+
const HARNESS_HELP = `martty harness — configure standalone ACP harnesses
|
|
14
|
+
|
|
15
|
+
USAGE:
|
|
16
|
+
martty harness list
|
|
17
|
+
martty harness add <id> --command <cmd> [--label <label>] [--arg <arg>]...
|
|
18
|
+
martty harness use <id>
|
|
19
|
+
|
|
20
|
+
list discovers executable *-acp and *_acp commands on PATH. The active harness
|
|
21
|
+
is saved in $MARTTY_HOME/settings.json. The next standalone launch starts that
|
|
22
|
+
harness with a new ACP session; sessions are never carried across harnesses.
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
function readSettings(settingsPath) {
|
|
26
|
+
if (!existsSync(settingsPath)) return {}
|
|
27
|
+
let value
|
|
28
|
+
try {
|
|
29
|
+
value = JSON.parse(readFileSync(settingsPath, 'utf8'))
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new Error(`invalid Martty settings: ${error.message}`)
|
|
32
|
+
}
|
|
33
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
34
|
+
throw new Error('invalid Martty settings: root must be an object')
|
|
35
|
+
}
|
|
36
|
+
return value
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function writeSettings(settingsPath, value) {
|
|
40
|
+
mkdirSync(path.dirname(settingsPath), { recursive: true })
|
|
41
|
+
const temporary = `${settingsPath}.${process.pid}.${Date.now()}.tmp`
|
|
42
|
+
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`)
|
|
43
|
+
renameSync(temporary, settingsPath)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateHarness(value) {
|
|
47
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
48
|
+
throw new Error('harness must be an object')
|
|
49
|
+
}
|
|
50
|
+
if (typeof value.id !== 'string' || !HARNESS_ID.test(value.id)) {
|
|
51
|
+
throw new Error('harness id must use lowercase letters, numbers, and hyphens')
|
|
52
|
+
}
|
|
53
|
+
if (typeof value.command !== 'string' || value.command.trim().length === 0) {
|
|
54
|
+
throw new Error('harness command must be a non-empty string')
|
|
55
|
+
}
|
|
56
|
+
const args = value.args === undefined ? [] : value.args
|
|
57
|
+
if (!Array.isArray(args) || args.some((arg) => typeof arg !== 'string')) {
|
|
58
|
+
throw new Error('harness args must be an array of strings')
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
id: value.id,
|
|
62
|
+
label: typeof value.label === 'string' && value.label.trim().length > 0
|
|
63
|
+
? value.label
|
|
64
|
+
: value.id,
|
|
65
|
+
command: value.command,
|
|
66
|
+
args: [...args],
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function configuredHarnesses(settings) {
|
|
71
|
+
if (!Array.isArray(settings.harnesses)) return []
|
|
72
|
+
return settings.harnesses.map(validateHarness)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function upsertHarness(settingsPath, harness) {
|
|
76
|
+
const next = validateHarness(harness)
|
|
77
|
+
const settings = readSettings(settingsPath)
|
|
78
|
+
const harnesses = configuredHarnesses(settings)
|
|
79
|
+
const index = harnesses.findIndex(({ id }) => id === next.id)
|
|
80
|
+
if (index === -1) harnesses.push(next)
|
|
81
|
+
else harnesses[index] = next
|
|
82
|
+
writeSettings(settingsPath, { ...settings, harnesses })
|
|
83
|
+
return next
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function activateHarness(settingsPath, id) {
|
|
87
|
+
const settings = readSettings(settingsPath)
|
|
88
|
+
const harnesses = configuredHarnesses(settings)
|
|
89
|
+
if (!harnesses.some((harness) => harness.id === id)) {
|
|
90
|
+
throw new Error(`unknown harness ${JSON.stringify(id)}`)
|
|
91
|
+
}
|
|
92
|
+
writeSettings(settingsPath, { ...settings, harnesses, activeHarness: id })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function selectedHarness(settingsPath) {
|
|
96
|
+
const settings = readSettings(settingsPath)
|
|
97
|
+
if (typeof settings.activeHarness !== 'string') return undefined
|
|
98
|
+
return configuredHarnesses(settings).find(({ id }) => id === settings.activeHarness)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function discoverPathHarnesses(pathValue = process.env.PATH ?? '') {
|
|
102
|
+
const found = []
|
|
103
|
+
const names = new Set()
|
|
104
|
+
for (const directory of pathValue.split(path.delimiter).filter(Boolean)) {
|
|
105
|
+
let entries
|
|
106
|
+
try {
|
|
107
|
+
entries = readdirSync(directory, { withFileTypes: true })
|
|
108
|
+
} catch {
|
|
109
|
+
continue
|
|
110
|
+
}
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) continue
|
|
113
|
+
const name = entry.name.replace(/\.(?:cmd|exe|bat|com)$/i, '')
|
|
114
|
+
if (!/(?:^|[-_])acp$/i.test(name) || names.has(name)) continue
|
|
115
|
+
const command = path.resolve(directory, entry.name)
|
|
116
|
+
try {
|
|
117
|
+
const stat = statSync(command)
|
|
118
|
+
if (!stat.isFile()) continue
|
|
119
|
+
if (process.platform !== 'win32' && (stat.mode & 0o111) === 0) continue
|
|
120
|
+
} catch {
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
names.add(name)
|
|
124
|
+
found.push({
|
|
125
|
+
id: `path-${name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`,
|
|
126
|
+
label: name,
|
|
127
|
+
command,
|
|
128
|
+
args: [],
|
|
129
|
+
source: 'path',
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return found
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function discoverHarnesses(settingsPath, options = {}) {
|
|
137
|
+
const configured = configuredHarnesses(readSettings(settingsPath))
|
|
138
|
+
.map((harness) => ({ ...harness, source: 'configured' }))
|
|
139
|
+
const defaults = (options.defaults ?? []).map((entry) => ({
|
|
140
|
+
...validateHarness(entry),
|
|
141
|
+
source: typeof entry.source === 'string' ? entry.source : 'builtin',
|
|
142
|
+
}))
|
|
143
|
+
const pathEntries = discoverPathHarnesses(options.pathValue)
|
|
144
|
+
const seen = new Set()
|
|
145
|
+
return [...configured, ...defaults, ...pathEntries].filter((entry) => {
|
|
146
|
+
if (seen.has(entry.id)) return false
|
|
147
|
+
seen.add(entry.id)
|
|
148
|
+
return true
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function runHarnessCommand(argv, options) {
|
|
153
|
+
const settingsPath = options?.settingsPath
|
|
154
|
+
if (typeof settingsPath !== 'string' || settingsPath.length === 0) {
|
|
155
|
+
throw new Error('harness command needs a settings path')
|
|
156
|
+
}
|
|
157
|
+
const [action, id, ...tokens] = argv
|
|
158
|
+
if (action === 'help' || action === undefined) {
|
|
159
|
+
return { code: 0, stdout: HARNESS_HELP, stderr: '' }
|
|
160
|
+
}
|
|
161
|
+
if (action === 'list') {
|
|
162
|
+
const settings = readSettings(settingsPath)
|
|
163
|
+
const active = typeof settings.activeHarness === 'string' ? settings.activeHarness : undefined
|
|
164
|
+
const stdout = discoverHarnesses(settingsPath, options).map((entry) => {
|
|
165
|
+
const marker = entry.id === active ? '*' : ' '
|
|
166
|
+
const command = [entry.command, ...entry.args]
|
|
167
|
+
.map((part) => /\s/.test(part) ? JSON.stringify(part) : part)
|
|
168
|
+
.join(' ')
|
|
169
|
+
return `${marker} ${entry.id}\t${entry.source}\t${entry.label}\t${command}`
|
|
170
|
+
}).join('\n')
|
|
171
|
+
return { code: 0, stdout: stdout.length > 0 ? `${stdout}\n` : '', stderr: '' }
|
|
172
|
+
}
|
|
173
|
+
if (action === 'use') {
|
|
174
|
+
const harness = discoverHarnesses(settingsPath, options).find((entry) => entry.id === id)
|
|
175
|
+
if (harness === undefined) throw new Error(`unknown harness ${JSON.stringify(id ?? '')}`)
|
|
176
|
+
upsertHarness(settingsPath, harness)
|
|
177
|
+
activateHarness(settingsPath, id)
|
|
178
|
+
return {
|
|
179
|
+
code: 0,
|
|
180
|
+
stdout: `active harness ${id}; next standalone launch starts a new session\n`,
|
|
181
|
+
stderr: '',
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (action !== 'add') throw new Error(`unknown harness command ${JSON.stringify(action ?? '')}`)
|
|
185
|
+
let command
|
|
186
|
+
let label
|
|
187
|
+
const args = []
|
|
188
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
189
|
+
const token = tokens[index]
|
|
190
|
+
const value = tokens[index + 1]
|
|
191
|
+
if (token === '--command') command = value
|
|
192
|
+
else if (token === '--label') label = value
|
|
193
|
+
else if (token === '--arg') args.push(value ?? '')
|
|
194
|
+
else throw new Error(`unknown harness add option ${JSON.stringify(token)}`)
|
|
195
|
+
index += 1
|
|
196
|
+
}
|
|
197
|
+
upsertHarness(settingsPath, { id, label, command, args })
|
|
198
|
+
return { code: 0, stdout: `saved harness ${id}\n`, stderr: '' }
|
|
199
|
+
}
|
package/lib/inspect.js
CHANGED
|
@@ -683,7 +683,9 @@ export function statusInspectProvider(acpSessionStatus) {
|
|
|
683
683
|
call: 'ctx.acpSessionStatus.subscribe((snapshot) => { ... })',
|
|
684
684
|
returns: { type: 'function', role: 'dispose', idempotent: true },
|
|
685
685
|
},
|
|
686
|
-
transport: 'standard ACP initialize, authenticate, session
|
|
686
|
+
transport: 'standard ACP initialize, authenticate, session, session/prompt response, '
|
|
687
|
+
+ 'and session/update messages, plus optional Martty session.status/session.event '
|
|
688
|
+
+ 'extensions',
|
|
687
689
|
},
|
|
688
690
|
referencedTypes: ['SessionStatusSnapshot'],
|
|
689
691
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "martty",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.31",
|
|
4
4
|
"description": "Terminal-native ACP client UI; Cordis client tree, any ACP agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"main": "lib/index.js",
|
|
16
16
|
"scripts": {
|
|
17
17
|
"pretest": "node --test ../scripts/workflow-release.test.mjs ../scripts/package-alias.test.mjs",
|
|
18
|
-
"test": "node --test ../scripts/package-native.test.mjs ../scripts/check-release-tag.test.mjs ../scripts/check-static-elf.test.mjs ../scripts/smoke-old-linux.test.mjs ../scripts/cargo-guard.test.mjs ../scripts/build-npm.test.mjs ../scripts/client-profile.test.mjs ../scripts/plugin-runner.test.mjs ../scripts/profile-link-resolution.test.mjs ../scripts/packaged-acp-permission.test.mjs ../scripts/profile-smoke-tui.test.mjs ../scripts/release.test.mjs ../scripts/jsonrpc-line-transport.test.mjs ../scripts/tui-theme.test.mjs ../scripts/tui-presets.test.mjs ../scripts/tui-plugin-store.test.mjs ../scripts/tui-local-plugins.test.mjs ../scripts/tui-client-plugin-registry.test.mjs ../scripts/tui-slots.test.mjs ../scripts/tui-commands.test.mjs ../scripts/tui-overlay.test.mjs ../scripts/tui-agents.test.mjs ../scripts/tui-queue.test.mjs ../scripts/mux.test.mjs ../scripts/acp-client.test.mjs ../scripts/acp-client-events.test.mjs ../scripts/acp-session-config.test.mjs ../scripts/acp-session-plan.test.mjs ../scripts/acp-session-stats.test.mjs ../scripts/acp-session-status.test.mjs ../scripts/agents-view.test.mjs ../scripts/plan-view.test.mjs ../scripts/queue-view.test.mjs ../scripts/stats-view.test.mjs ../scripts/status-view.test.mjs ../scripts/deepseek-logo.test.mjs ../scripts/runner.test.mjs ../scripts/inspect.test.mjs ../scripts/creator-overlay.test.mjs ../scripts/real-agent-e2e.test.mjs",
|
|
18
|
+
"test": "node --test ../scripts/package-native.test.mjs ../scripts/check-release-tag.test.mjs ../scripts/check-static-elf.test.mjs ../scripts/smoke-old-linux.test.mjs ../scripts/cargo-guard.test.mjs ../scripts/build-npm.test.mjs ../scripts/client-profile.test.mjs ../scripts/plugin-runner.test.mjs ../scripts/profile-link-resolution.test.mjs ../scripts/packaged-acp-permission.test.mjs ../scripts/profile-smoke-tui.test.mjs ../scripts/release.test.mjs ../scripts/jsonrpc-line-transport.test.mjs ../scripts/tui-theme.test.mjs ../scripts/tui-presets.test.mjs ../scripts/tui-plugin-store.test.mjs ../scripts/tui-local-plugins.test.mjs ../scripts/tui-client-plugin-registry.test.mjs ../scripts/tui-slots.test.mjs ../scripts/tui-commands.test.mjs ../scripts/tui-overlay.test.mjs ../scripts/tui-agents.test.mjs ../scripts/tui-queue.test.mjs ../scripts/mux.test.mjs ../scripts/acp-client.test.mjs ../scripts/harnesses.test.mjs ../scripts/harness-view.test.mjs ../scripts/acp-client-events.test.mjs ../scripts/acp-session-config.test.mjs ../scripts/acp-session-plan.test.mjs ../scripts/acp-session-stats.test.mjs ../scripts/acp-session-status.test.mjs ../scripts/agents-view.test.mjs ../scripts/plan-view.test.mjs ../scripts/queue-view.test.mjs ../scripts/stats-view.test.mjs ../scripts/status-view.test.mjs ../scripts/deepseek-logo.test.mjs ../scripts/runner.test.mjs ../scripts/inspect.test.mjs ../scripts/creator-overlay.test.mjs ../scripts/real-agent-e2e.test.mjs",
|
|
19
19
|
"test:profile-install-matrix": "node --test ../scripts/profile-install-matrix.test.mjs"
|
|
20
20
|
},
|
|
21
21
|
"publishConfig": {
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
77
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
78
|
-
"@openma/deepseek-harness-acp": "0.4.
|
|
78
|
+
"@openma/deepseek-harness-acp": "0.4.27"
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@deepseek-ai/dsh": "0.1.1-rc.2"
|
|
Binary file
|
package/vendor/darwin-x64/martty
CHANGED
|
Binary file
|
|
Binary file
|
package/vendor/linux-x64/martty
CHANGED
|
Binary file
|
|
Binary file
|