martty 0.2.30 → 0.2.32

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 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. The Node Client process owns a Cordis tree and starts the Rust painter. A sibling
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 { bootClient, parseClientArgv, painterArgs } from '../lib/boot.js'
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' && value.state === 'idle') {
112
- value.state = 'starting'
113
- publish()
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 === 'starting' && RUNNING_UPDATE_TYPES.has(type)) {
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
- return { agent: resolveStackedAgent(), rustArgs }
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
+ }
@@ -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 and session/update messages',
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.30",
3
+ "version": "0.2.32",
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.26"
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
Binary file
Binary file
Binary file
Binary file