martty 0.3.2-beta.0 → 0.3.2-beta.3
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 +42 -8
- package/bin/martty.js +5 -0
- package/lib/acp-agent-pool.js +3 -1
- package/lib/acp-client.js +63 -4
- package/lib/acp-session-status.js +45 -3
- package/lib/agent.js +5 -10
- package/lib/creator-overlay.js +66 -9
- package/lib/harness-registry.js +7 -1
- package/lib/harness-view.js +10 -1
- package/lib/harnesses.js +9 -3
- package/lib/index.js +4 -1
- package/lib/install-migration.js +456 -0
- package/lib/queue-view.js +7 -1
- package/lib/status-view.js +15 -1
- package/lib/tui-queue.js +4 -0
- package/package.json +12 -11
package/README.md
CHANGED
|
@@ -15,9 +15,10 @@ x64. Requires Node.js 22.19+.
|
|
|
15
15
|
|
|
16
16
|
> **Rename note:** the project and recommended npm package have moved from
|
|
17
17
|
> DeepSeek Harness TUI / `martty` to **Martty** / `martty`.
|
|
18
|
-
>
|
|
19
|
-
>
|
|
20
|
-
>
|
|
18
|
+
> Configuration and session data remain compatible. The production profile is
|
|
19
|
+
> now `martty`; legacy `tui` profiles continue to work. The `dsh-tui` command
|
|
20
|
+
> is no longer installed by the `martty` package — see
|
|
21
|
+
> [Migrating from DeepSeek Harness TUI / the scoped package](#migrating-from-deepseek-harness-tui--the-scoped-package).
|
|
21
22
|
|
|
22
23
|
## Global Agent TUI
|
|
23
24
|
|
|
@@ -49,8 +50,35 @@ missing profile and installs the TUI plus its ACP dependency; no global
|
|
|
49
50
|
|
|
50
51
|
`martty` is the recommended package name. Starting with `0.2.13`, it and the
|
|
51
52
|
legacy `martty` package are published by the same CI run
|
|
52
|
-
with identical versions and artifacts.
|
|
53
|
-
|
|
53
|
+
with identical versions and artifacts. The scoped name is deprecated; switch
|
|
54
|
+
package names with:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npm install --global martty
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
No removal is needed first, whatever state the legacy package is in —
|
|
61
|
+
published versions of `martty` register `dsh-tui`
|
|
62
|
+
(every version) and `martty` (0.2.14/0.2.20) global commands, and `npm link`ed
|
|
63
|
+
legacy checkouts own the same names (a checkout of this repository links as
|
|
64
|
+
`martty`, so it replaces — or is replaced by — an installed `martty` with no
|
|
65
|
+
collision). The `martty` package declares only `martty-cli` so npm never
|
|
66
|
+
refuses the install over a bin another package owns (`EEXIST`); a
|
|
67
|
+
postinstall step then retires the stale `martty`/`dsh-tui`/`dsb` links that
|
|
68
|
+
verifiably belonged to the legacy package (a third-party `dsh-tui` stays
|
|
69
|
+
untouched), creates the `martty` command itself, and detaches the legacy
|
|
70
|
+
package's bin declarations so a later `npm rm -g` can't take `martty` down
|
|
71
|
+
with it. Removing `martty` afterwards is optional
|
|
72
|
+
cleanup; `martty` warns at launch while it remains — on an interactive
|
|
73
|
+
terminal the notice prints when the session exits, so the TUI cannot hide
|
|
74
|
+
it. With `--ignore-scripts`
|
|
75
|
+
the postinstall never runs — invoke `martty-cli` once (or `npx martty`) and
|
|
76
|
+
the first launch finishes the migration itself. And after `npm rm -g
|
|
77
|
+
martty`, the `martty` command left behind is a self-cleaning shim: invoking
|
|
78
|
+
it once prints the reinstall hint and removes it — nothing dead is left
|
|
79
|
+
in `<prefix>/bin`.
|
|
80
|
+
|
|
81
|
+
Inside a `dsh` profile the same switch is:
|
|
54
82
|
|
|
55
83
|
```sh
|
|
56
84
|
dsh plugin --profile tui remove martty
|
|
@@ -58,8 +86,13 @@ dsh plugin --profile martty add martty@latest
|
|
|
58
86
|
```
|
|
59
87
|
|
|
60
88
|
The recommended profile changes from `tui` to `martty`; runtime behavior,
|
|
61
|
-
config, and session data remain unchanged. `
|
|
62
|
-
|
|
89
|
+
config, and session data remain unchanged. Legacy `tui` profiles remain
|
|
90
|
+
compatible.
|
|
91
|
+
|
|
92
|
+
The `martty` package no longer installs a `dsh-tui` command: the name
|
|
93
|
+
collided with both the legacy package and an unrelated `dsh-tui` package on
|
|
94
|
+
npm, which blocked `npm install --global martty` outright. If your fingers
|
|
95
|
+
still type `dsh-tui`, alias it: `alias dsh-tui=martty`.
|
|
63
96
|
|
|
64
97
|
The profile Host mounts the ACP plugin on Base, then starts a separate TUI
|
|
65
98
|
Client process over standard ACP stdin/stdout. For standalone use, run
|
|
@@ -229,7 +262,8 @@ npx --yes martty --demo
|
|
|
229
262
|
|
|
230
263
|
The profile path remains recommended for an existing DSH installation that
|
|
231
264
|
wants standard plugin management and a durable `martty` profile. `martty` is the
|
|
232
|
-
primary command; `dsh-tui`
|
|
265
|
+
primary command; the legacy `dsh-tui` command is no longer installed
|
|
266
|
+
(`alias dsh-tui=martty` restores it).
|
|
233
267
|
|
|
234
268
|
## Highlights
|
|
235
269
|
|
package/bin/martty.js
CHANGED
|
@@ -9,10 +9,15 @@ import { resolveDependencyStack } from '../lib/agent.js'
|
|
|
9
9
|
import { bootClient, parseClientArgv, painterArgs, uiSettingsPath } from '../lib/boot.js'
|
|
10
10
|
import { runHarnessCommandAsync } from '../lib/harnesses.js'
|
|
11
11
|
import { nativeBinary } from '../lib/spawn-tui.js'
|
|
12
|
+
import { reportLaunchNotices, warnAboutLegacyPackage } from '../lib/install-migration.js'
|
|
12
13
|
|
|
13
14
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
14
15
|
const configuredBin = process.env.MARTTY_BIN || process.env.DSH_TUI_BIN
|
|
15
16
|
|
|
17
|
+
const migrationNotices = []
|
|
18
|
+
warnAboutLegacyPackage({ argv1: process.argv[1], write: (line) => migrationNotices.push(line) })
|
|
19
|
+
reportLaunchNotices(migrationNotices)
|
|
20
|
+
|
|
16
21
|
const argv = process.argv.slice(2)
|
|
17
22
|
const wantDemoSkin = argv.includes('--demo-skin')
|
|
18
23
|
const wantDemo = argv.includes('--demo')
|
package/lib/acp-agent-pool.js
CHANGED
|
@@ -123,7 +123,9 @@ export function createAgentPool(initial, { spawnAgent, resolveAgent, diagnosticE
|
|
|
123
123
|
owner.failed = diagnosticError(error, owner.handle)
|
|
124
124
|
for (const pending of owner.pending.values()) {
|
|
125
125
|
if (pending.reject) pending.reject(owner.failed)
|
|
126
|
-
|
|
126
|
+
// `agentExited` marks the reply as pool-synthesized so the client can
|
|
127
|
+
// tell a dead child apart from a refusal the agent itself authored.
|
|
128
|
+
else write(response(owner, { error: { code: -32603, message: owner.failed.message, data: { agentExited: true } } }, pending))
|
|
127
129
|
}
|
|
128
130
|
owner.pending.clear()
|
|
129
131
|
}
|
package/lib/acp-client.js
CHANGED
|
@@ -105,7 +105,12 @@ export function apply(ctx, config = {}) {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
liveAgent = null
|
|
108
|
-
const
|
|
108
|
+
const noteAgentDiagnostics = installAgentWarnings(ctx)
|
|
109
|
+
const service = createAgentPool(agent, {
|
|
110
|
+
spawnAgent: (spec) => spawnAgent(spec, noteAgentDiagnostics),
|
|
111
|
+
resolveAgent,
|
|
112
|
+
diagnosticError,
|
|
113
|
+
})
|
|
109
114
|
if (liveAgentExitHook !== null) process.removeListener('exit', liveAgentExitHook)
|
|
110
115
|
liveAgentExitHook = () => service.close()
|
|
111
116
|
process.once('exit', liveAgentExitHook)
|
|
@@ -113,14 +118,14 @@ export function apply(ctx, config = {}) {
|
|
|
113
118
|
provide(ctx, service)
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
function spawnAgent(agent) {
|
|
121
|
+
function spawnAgent(agent, onDiagnosticLine) {
|
|
117
122
|
const child = spawn(agent.command, agent.args ?? [], {
|
|
118
123
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
119
124
|
env: harnessEnvironment(agent.env),
|
|
120
125
|
})
|
|
121
126
|
child.stdin.on('error', () => {})
|
|
122
127
|
child.stdout.on('error', () => {})
|
|
123
|
-
const diagnostics = drainDiagnostics(child.stderr)
|
|
128
|
+
const diagnostics = drainDiagnostics(child.stderr, onDiagnosticLine)
|
|
124
129
|
return {
|
|
125
130
|
kind: 'spawn',
|
|
126
131
|
command: agent.command,
|
|
@@ -133,14 +138,67 @@ function spawnAgent(agent) {
|
|
|
133
138
|
}
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Agent stderr lines worth surfacing in the input dock: warnings and
|
|
143
|
+
* failures the agent reports but survives (e.g. a bundle entry that did
|
|
144
|
+
* not activate) would otherwise be invisible until something exits.
|
|
145
|
+
*/
|
|
146
|
+
const DIAGNOSTIC_NOTICE = /warn|error|fail|did not activate|refus|incompat/i
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Feed warning-shaped agent stderr lines to a `conversation.input.dock`
|
|
150
|
+
* notice when the client tree carries `tuiSlots`. Returns undefined when the
|
|
151
|
+
* slot service is absent — stderr stays diagnostics-only as before.
|
|
152
|
+
*/
|
|
153
|
+
function installAgentWarnings(ctx) {
|
|
154
|
+
const slots = ctx.get?.('tuiSlots') ?? ctx.tuiSlots
|
|
155
|
+
if (typeof slots?.register !== 'function') return undefined
|
|
156
|
+
let controller
|
|
157
|
+
try {
|
|
158
|
+
controller = slots.register(
|
|
159
|
+
{ name: 'conversation.input.dock', id: 'acp-agent-warnings' },
|
|
160
|
+
[],
|
|
161
|
+
)
|
|
162
|
+
} catch {
|
|
163
|
+
return undefined
|
|
164
|
+
}
|
|
165
|
+
const shown = []
|
|
166
|
+
return (line) => {
|
|
167
|
+
if (!DIAGNOSTIC_NOTICE.test(line)) return
|
|
168
|
+
shown.push(line.trimEnd())
|
|
169
|
+
if (shown.length > 4) shown.shift()
|
|
170
|
+
try {
|
|
171
|
+
controller.update([{ id: 'latest', kind: 'notice', level: 'warn', text: shown.join('\n') }])
|
|
172
|
+
} catch {
|
|
173
|
+
// The contribution is gone (disposed or re-mounted); stderr keeps
|
|
174
|
+
// flowing to the diagnostics tail regardless.
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function drainDiagnostics(stream, onLine) {
|
|
137
180
|
const limit = 8192
|
|
138
181
|
let tail = Buffer.alloc(0)
|
|
182
|
+
let pending = ''
|
|
139
183
|
let state = 'text'
|
|
184
|
+
const emitLines = (text, flush = false) => {
|
|
185
|
+
if (onLine === undefined) return
|
|
186
|
+
pending += text
|
|
187
|
+
for (let index = pending.indexOf('\n'); index !== -1; index = pending.indexOf('\n')) {
|
|
188
|
+
const line = pending.slice(0, index)
|
|
189
|
+
pending = pending.slice(index + 1)
|
|
190
|
+
if (line.trim() !== '') onLine(line)
|
|
191
|
+
}
|
|
192
|
+
if (flush && pending.trim() !== '') {
|
|
193
|
+
onLine(pending)
|
|
194
|
+
pending = ''
|
|
195
|
+
}
|
|
196
|
+
}
|
|
140
197
|
// stderr is not a UI surface. Drain it even without a diagnostics consumer,
|
|
141
198
|
// strip control strings incrementally, and never retain their payloads.
|
|
142
199
|
stream.setEncoding('utf8')
|
|
143
200
|
stream.on('error', () => {})
|
|
201
|
+
stream.on('close', () => emitLines('', true))
|
|
144
202
|
stream.on('data', (chunk) => {
|
|
145
203
|
let text = ''
|
|
146
204
|
for (const char of chunk) {
|
|
@@ -174,6 +232,7 @@ function drainDiagnostics(stream) {
|
|
|
174
232
|
text += char
|
|
175
233
|
}
|
|
176
234
|
if (text.length === 0) return
|
|
235
|
+
emitLines(text)
|
|
177
236
|
const combined = Buffer.concat([tail, Buffer.from(text)])
|
|
178
237
|
let start = Math.max(0, combined.length - limit)
|
|
179
238
|
// Never split a UTF-8 code point or retain the large incoming allocation.
|
|
@@ -68,6 +68,7 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
68
68
|
let selectionKnown = false
|
|
69
69
|
let connection = 'connecting'
|
|
70
70
|
let server
|
|
71
|
+
let dshRuntime
|
|
71
72
|
const auth = { status: undefined, method: undefined }
|
|
72
73
|
let initializeId
|
|
73
74
|
let authMethods = []
|
|
@@ -94,7 +95,7 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
94
95
|
const value = stateFor(sessionId)
|
|
95
96
|
const { permissionPreset: _permissionPreset, sandboxMode: _sandboxMode,
|
|
96
97
|
connectionFacts, ...visible } = value
|
|
97
|
-
return structuredClone({ connection, server, auth, ...connectionFacts, ...visible })
|
|
98
|
+
return structuredClone({ connection, server, dshRuntime, auth, ...connectionFacts, ...visible })
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
function publish() {
|
|
@@ -146,6 +147,7 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
146
147
|
selectionKnown = false
|
|
147
148
|
connection = 'connecting'
|
|
148
149
|
server = undefined
|
|
150
|
+
dshRuntime = undefined
|
|
149
151
|
auth.status = undefined
|
|
150
152
|
auth.method = undefined
|
|
151
153
|
delete auth.message
|
|
@@ -197,13 +199,44 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
197
199
|
if (!object(message)) return
|
|
198
200
|
if (message.id !== undefined && message.id === initializeId) {
|
|
199
201
|
initializeId = undefined
|
|
200
|
-
connection = message.error === undefined ? 'attached' : 'failed'
|
|
201
202
|
fallback.error = message.error?.message
|
|
202
203
|
const result = object(message.result) ? message.result : undefined
|
|
203
204
|
server = readString(result?.agentInfo, 'name')
|
|
205
|
+
// The runtime the ACP agent actually booted (dsh-acp advertises it in
|
|
206
|
+
// agentCapabilities._meta.dsh.runtime); shown in /status so users can
|
|
207
|
+
// tell bundled from host installs (issue #157).
|
|
208
|
+
dshRuntime = readDshRuntime(result?.agentCapabilities)
|
|
204
209
|
const methods = Array.isArray(result?.authMethods) ? result.authMethods : []
|
|
205
210
|
authMethods = methods
|
|
206
211
|
// initialize advertises choices, not the credential used by this process.
|
|
212
|
+
if (message.error === undefined) {
|
|
213
|
+
connection = 'attached'
|
|
214
|
+
} else {
|
|
215
|
+
// An agent that answered `initialize` with a refusal is still
|
|
216
|
+
// attached — only a locally synthesized failure (the pool's
|
|
217
|
+
// agentExited marker for a dead child) counts as 'failed'. A refused
|
|
218
|
+
// handshake may advertise auth methods in its error data (the -32000
|
|
219
|
+
// convention): surface sign-in the same way the refused
|
|
220
|
+
// session/new path does. A refusal demanding auth while declaring
|
|
221
|
+
// no methods can never be remedied, so it stays 'failed'.
|
|
222
|
+
const offered = Array.isArray(message.error.data?.authMethods)
|
|
223
|
+
? message.error.data.authMethods : []
|
|
224
|
+
if (message.error.data?.agentExited === true
|
|
225
|
+
|| (isAuthRequired(message.error) && offered.length === 0)) {
|
|
226
|
+
connection = 'failed'
|
|
227
|
+
} else {
|
|
228
|
+
connection = 'refused'
|
|
229
|
+
if (isAuthRequired(message.error)) {
|
|
230
|
+
authMethods = offered
|
|
231
|
+
auth.status = 'needs sign-in'
|
|
232
|
+
}
|
|
233
|
+
// The refusal's own words are the diagnosis — our transport's
|
|
234
|
+
// appended "Agent stderr:" tail is unrelated launch noise.
|
|
235
|
+
if (typeof fallback.error === 'string') {
|
|
236
|
+
fallback.error = fallback.error.split('\nAgent stderr:')[0].trimEnd()
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
207
240
|
publish()
|
|
208
241
|
return
|
|
209
242
|
}
|
|
@@ -230,7 +263,7 @@ export function installAcpSessionStatus(ctx, options = {}) {
|
|
|
230
263
|
if (message.error !== undefined) {
|
|
231
264
|
if (isAuthRequired(message.error)) auth.status = 'needs sign-in'
|
|
232
265
|
else {
|
|
233
|
-
connection = 'failed'
|
|
266
|
+
connection = message.error.data?.agentExited === true ? 'failed' : 'refused'
|
|
234
267
|
const value = stateFor(setup.sessionId, false)
|
|
235
268
|
value.error = readString(message.error, 'message') ?? 'Session setup failed'
|
|
236
269
|
value.state = 'idle'
|
|
@@ -416,6 +449,15 @@ function isAuthRequired(error) {
|
|
|
416
449
|
|| error.code === 'auth_required' || error.code === 'AuthRequired')
|
|
417
450
|
}
|
|
418
451
|
|
|
452
|
+
function readDshRuntime(capabilities) {
|
|
453
|
+
const runtime = capabilities?._meta?.dsh?.runtime
|
|
454
|
+
if (!object(runtime)) return undefined
|
|
455
|
+
const version = readString(runtime, 'version')
|
|
456
|
+
const source = readString(runtime, 'source')
|
|
457
|
+
if (version === undefined && source === undefined) return undefined
|
|
458
|
+
return { version, source, base: readString(runtime, 'base') }
|
|
459
|
+
}
|
|
460
|
+
|
|
419
461
|
function readString(value, ...keys) {
|
|
420
462
|
if (!object(value)) return undefined
|
|
421
463
|
for (const key of keys) {
|
package/lib/agent.js
CHANGED
|
@@ -75,16 +75,11 @@ export function resolveStackedAgent(anchor = import.meta.url, options = {}) {
|
|
|
75
75
|
const home = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
76
76
|
for (const profile of ['tui-test', 'martty', 'tui']) {
|
|
77
77
|
const bin = join(home, 'profiles', profile, 'node_modules', '.bin', 'dsh-acp')
|
|
78
|
-
const creator =
|
|
79
|
-
home,
|
|
80
|
-
'profiles',
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
'@openma',
|
|
84
|
-
'deepseek-harness-tui',
|
|
85
|
-
'creator',
|
|
86
|
-
)
|
|
87
|
-
if (existsSync(bin) && existsSync(join(creator, 'package.json'))) {
|
|
78
|
+
const creator = [
|
|
79
|
+
join(home, 'profiles', profile, 'node_modules', '@openma', 'deepseek-harness-tui', 'creator'),
|
|
80
|
+
join(home, 'profiles', profile, 'node_modules', 'martty', 'creator'),
|
|
81
|
+
].find((dir) => existsSync(join(dir, 'package.json')))
|
|
82
|
+
if (existsSync(bin) && creator) {
|
|
88
83
|
return { command: bin, args: ['--bundle', creator] }
|
|
89
84
|
}
|
|
90
85
|
}
|
package/lib/creator-overlay.js
CHANGED
|
@@ -25,6 +25,38 @@ const jsonOutput = {
|
|
|
25
25
|
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Whether an acquireScope rejection says the preset is not registered yet.
|
|
30
|
+
* Preset declarations register asynchronously while the Host activates, so
|
|
31
|
+
* the overlay can run while the registry is still empty — this is the one
|
|
32
|
+
* failure worth retrying. Everything else (broken compositions, transport
|
|
33
|
+
* failures) fails fast.
|
|
34
|
+
*/
|
|
35
|
+
function presetNotFound(error) {
|
|
36
|
+
return error?.code === 'agent-preset/not-found'
|
|
37
|
+
|| /^Unknown agent preset: /.test(error?.message ?? '')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Acquire one preset scope lease, retrying `agent-preset/not-found` until
|
|
42
|
+
* `waitMs` elapses — the registry may still be collecting declarations when
|
|
43
|
+
* this plugin mounts. A registry without `acquireScope` resolves undefined
|
|
44
|
+
* so the caller can reject it with the usual lease check.
|
|
45
|
+
*/
|
|
46
|
+
async function acquirePresetScope(agentPresets, preset, waitMs, pollMs) {
|
|
47
|
+
const acquire = agentPresets?.acquireScope
|
|
48
|
+
if (typeof acquire !== 'function') return undefined
|
|
49
|
+
const deadline = Date.now() + waitMs
|
|
50
|
+
for (;;) {
|
|
51
|
+
try {
|
|
52
|
+
return await acquire.call(agentPresets, preset)
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (!presetNotFound(error) || Date.now() >= deadline) throw error
|
|
55
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
28
60
|
function requireAgent(exec) {
|
|
29
61
|
if (exec?.agent === undefined) {
|
|
30
62
|
throw new Error('TUI artifact tools require an Agent-backed Creator session')
|
|
@@ -190,7 +222,7 @@ const skillContent = skillDocument.slice(skillFrontmatter[0].length)
|
|
|
190
222
|
|
|
191
223
|
/**
|
|
192
224
|
* @param {object} ctx
|
|
193
|
-
* @param {{ preset?: string }} [config]
|
|
225
|
+
* @param {{ preset?: string, scopeWaitMs?: number, scopePollMs?: number }} [config]
|
|
194
226
|
*/
|
|
195
227
|
export async function apply(ctx, config = {}) {
|
|
196
228
|
// Standalone launches mount this overlay directly into the ACP Host tree,
|
|
@@ -202,13 +234,38 @@ export async function apply(ctx, config = {}) {
|
|
|
202
234
|
if (typeof preset !== 'string' || preset.length === 0) {
|
|
203
235
|
throw new Error('tui-creator-overlay: preset must be a non-empty string')
|
|
204
236
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
237
|
+
// The preset registry hands out scope keys through acquireScope (a lease
|
|
238
|
+
// carrying generation.key); standingKeyFor never existed on the shipped
|
|
239
|
+
// registry. Holding the lease for the overlay's lifetime keeps the scope
|
|
240
|
+
// generation alive until the overlay is disposed. Declarations land after
|
|
241
|
+
// dependent rows activate, so a `not-found` result is retried briefly.
|
|
242
|
+
const scopeWaitMs = Number.isFinite(config.scopeWaitMs) ? Math.max(0, config.scopeWaitMs) : 10_000
|
|
243
|
+
const scopePollMs = Number.isFinite(config.scopePollMs) ? Math.max(1, config.scopePollMs) : 50
|
|
244
|
+
const lease = await acquirePresetScope(ctx.agentPresets, preset, scopeWaitMs, scopePollMs)
|
|
245
|
+
if (lease === null || typeof lease !== 'object' || !('key' in lease)) {
|
|
246
|
+
throw new Error(`tui-creator-overlay: preset scope is unavailable for ${JSON.stringify(preset)}`)
|
|
247
|
+
}
|
|
248
|
+
const release = typeof lease[Symbol.asyncDispose] === 'function'
|
|
249
|
+
? () => lease[Symbol.asyncDispose]()
|
|
250
|
+
: async () => {}
|
|
251
|
+
let overlay
|
|
252
|
+
try {
|
|
253
|
+
const scopeModule = await ctx.loader.import('@deepseek-ai/dsh-scope')
|
|
254
|
+
if (typeof scopeModule?.createScope !== 'function') {
|
|
255
|
+
throw new Error('tui-creator-overlay: profile dsh-scope module is unavailable')
|
|
256
|
+
}
|
|
257
|
+
overlay = scopeModule.createScope(ctx, lease.key)
|
|
258
|
+
} catch (error) {
|
|
259
|
+
await release()
|
|
260
|
+
throw error
|
|
261
|
+
}
|
|
262
|
+
const dispose = async () => {
|
|
263
|
+
try {
|
|
264
|
+
await overlay.dispose()
|
|
265
|
+
} finally {
|
|
266
|
+
await release()
|
|
267
|
+
}
|
|
209
268
|
}
|
|
210
|
-
const { createScope } = scopeModule
|
|
211
|
-
const overlay = createScope(ctx, key)
|
|
212
269
|
try {
|
|
213
270
|
const toolsModule = await ctx.loader.import('@deepseek-ai/dsh-tools')
|
|
214
271
|
if (typeof toolsModule?.defineTool !== 'function') {
|
|
@@ -240,9 +297,9 @@ export async function apply(ctx, config = {}) {
|
|
|
240
297
|
toolsModule.defineTool,
|
|
241
298
|
createTuiPluginStore({ root: config.artifactRoot }),
|
|
242
299
|
)
|
|
243
|
-
ctx.effect(() =>
|
|
300
|
+
ctx.effect(() => dispose, `tui-creator-overlay(${JSON.stringify(preset)})`)
|
|
244
301
|
} catch (error) {
|
|
245
|
-
await
|
|
302
|
+
await dispose()
|
|
246
303
|
throw error
|
|
247
304
|
}
|
|
248
305
|
}
|
package/lib/harness-registry.js
CHANGED
|
@@ -20,12 +20,17 @@ import { downloadFile } from './download.js'
|
|
|
20
20
|
export const ACP_REGISTRY_URL = 'https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json'
|
|
21
21
|
|
|
22
22
|
export const OPENMA_PI_ACP = '@openma/pi-acp@latest'
|
|
23
|
+
export const OPENMA_PI_ACP_REPOSITORY = 'https://github.com/openma-ai/pi-acp'
|
|
23
24
|
|
|
24
25
|
function preferMarttyPiAcp(records) {
|
|
25
26
|
return records.map((record) => record.id === 'pi-acp' ? {
|
|
26
27
|
...record,
|
|
27
|
-
label: '
|
|
28
|
+
label: 'pi-acp',
|
|
28
29
|
version: 'latest',
|
|
30
|
+
// The official catalog also lists an unrelated unprefixed pi-acp package;
|
|
31
|
+
// Martty only ever launches @openma/pi-acp, and the repository URL is how
|
|
32
|
+
// users tell the two entries apart.
|
|
33
|
+
repository: OPENMA_PI_ACP_REPOSITORY,
|
|
29
34
|
distributions: [{
|
|
30
35
|
type: 'npx', command: 'npx', args: [OPENMA_PI_ACP], env: {},
|
|
31
36
|
}],
|
|
@@ -144,6 +149,7 @@ export function normalizeAcpRegistry(value, options = {}) {
|
|
|
144
149
|
...(typeof agent.icon === 'string' ? { icon: agent.icon } : {}),
|
|
145
150
|
version: agent.version,
|
|
146
151
|
description: typeof agent.description === 'string' ? agent.description : '',
|
|
152
|
+
...(typeof agent.repository === 'string' ? { repository: agent.repository } : {}),
|
|
147
153
|
distributions,
|
|
148
154
|
}]
|
|
149
155
|
})
|
package/lib/harness-view.js
CHANGED
|
@@ -41,6 +41,15 @@ function findDescription(entry) {
|
|
|
41
41
|
return `found locally · configure · ${commandText(entry)}`
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Registry entries carry their repository like the catalog's name /
|
|
45
|
+
// description / repository triple; the URL distinguishes same-named
|
|
46
|
+
// harnesses (e.g. @openma/pi-acp from the unrelated registry pi-acp).
|
|
47
|
+
function describeCandidate(entry) {
|
|
48
|
+
const detail = findDescription(entry)
|
|
49
|
+
const repository = entry.registryRecord?.repository
|
|
50
|
+
return repository === undefined ? detail : `${detail} · ${repository}`
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
export function apply(ctx, options = {}) {
|
|
45
54
|
const settingsPath = options.settingsPath
|
|
46
55
|
let registryPromise
|
|
@@ -477,7 +486,7 @@ export function apply(ctx, options = {}) {
|
|
|
477
486
|
openSelect({ id: 'harness-find', title: failure ? 'Add Harness · Registry offline' : pending ? 'Add Harness · checking Registry…' : 'Add Harness', searchable: true,
|
|
478
487
|
value: entries[0]?.id ?? REFRESH,
|
|
479
488
|
options: [
|
|
480
|
-
...entries.map((entry) => ({ value: entry.id, label: entry.label, description:
|
|
489
|
+
...entries.map((entry) => ({ value: entry.id, label: entry.label, description: describeCandidate(entry),
|
|
481
490
|
...currentOption(entry),
|
|
482
491
|
group: locallyAvailable(entry) ? 'Installed / configured' : entry.status === 'Checking locally' ? 'Catalog' : 'Not downloaded' })),
|
|
483
492
|
{ value: REFRESH, label: failure ? 'Retry Registry' : 'Refresh Registry', description: failure ? errorText(failure) : 'Recheck the catalog and local programs' },
|
package/lib/harnesses.js
CHANGED
|
@@ -25,6 +25,8 @@ export { fetchAcpRegistry } from './harness-registry.js'
|
|
|
25
25
|
const HARNESS_ID = /^[a-z0-9][a-z0-9-]*$/
|
|
26
26
|
const PI_ACP_LATEST = OPENMA_PI_ACP
|
|
27
27
|
const PI_ACP_SPEC = /^(?:@openma\/)?pi-acp(?:@.+)?$/
|
|
28
|
+
const PI_ACP_ID = 'pi-acp'
|
|
29
|
+
const PI_ACP_LEGACY_LABEL = 'Pi (OpenMA)'
|
|
28
30
|
const ANSI = Object.freeze({ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', cyan: '\x1b[36m', green: '\x1b[32m' })
|
|
29
31
|
|
|
30
32
|
// Kept as a source-compatible empty export. The source of truth is the
|
|
@@ -373,11 +375,14 @@ function validateHarness(value) {
|
|
|
373
375
|
// through npm, never through a Martty release.
|
|
374
376
|
.map((arg) => PI_ACP_SPEC.test(arg) ? PI_ACP_LATEST : arg)
|
|
375
377
|
: [...args]
|
|
378
|
+
const label = typeof value.label === 'string' && value.label.trim().length > 0
|
|
379
|
+
? value.label
|
|
380
|
+
: value.id
|
|
376
381
|
return {
|
|
377
382
|
id: value.id,
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
383
|
+
// Configs saved when the pi harness was branded "Pi (OpenMA)" keep
|
|
384
|
+
// working; only the stale default display label migrates to the id.
|
|
385
|
+
label: value.id === PI_ACP_ID && label === PI_ACP_LEGACY_LABEL ? PI_ACP_ID : label,
|
|
381
386
|
command: value.command,
|
|
382
387
|
args: normalizedArgs,
|
|
383
388
|
...(env !== undefined ? { env: { ...env } } : {}),
|
|
@@ -529,6 +534,7 @@ function registryRecords(options = {}) {
|
|
|
529
534
|
: record.id,
|
|
530
535
|
version: typeof record.version === 'string' ? record.version : 'unversioned',
|
|
531
536
|
description: typeof record.description === 'string' ? record.description : '',
|
|
537
|
+
...(typeof record.repository === 'string' ? { repository: record.repository } : {}),
|
|
532
538
|
distributions,
|
|
533
539
|
}]
|
|
534
540
|
}
|
package/lib/index.js
CHANGED
|
@@ -72,8 +72,11 @@ export function restoreHostTty() {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
if (process.stdout.isTTY) {
|
|
75
|
+
// Leave the alt screen with ?1047l, not ?1049l: ?1049l also restores the
|
|
76
|
+
// cursor saved at ?1049h, which a second emit on the main screen would
|
|
77
|
+
// move back over the `session <id>` line the painter just printed.
|
|
75
78
|
process.stdout.write(
|
|
76
|
-
'\x1b[?2004l\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?25h\x1b[?
|
|
79
|
+
'\x1b[?2004l\x1b[?1006l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?25h\x1b[?1047l',
|
|
77
80
|
)
|
|
78
81
|
}
|
|
79
82
|
}
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
// Migration aid for the package rename `martty` ->
|
|
2
|
+
// `martty`.
|
|
3
|
+
//
|
|
4
|
+
// npm refuses to link a global bin owned by another package, and it links
|
|
5
|
+
// bins before running any lifecycle script — so no script inside this
|
|
6
|
+
// package can stop an EEXIST on a name the legacy package may already own
|
|
7
|
+
// (`martty`, `dsh-tui`, `dsb`). The published `martty` package therefore
|
|
8
|
+
// declares only `martty-cli` — a name nothing else owns — and this
|
|
9
|
+
// postinstall step creates the user-facing `martty` command itself after
|
|
10
|
+
// retiring any legacy-owned links. It also strips `bin` from the legacy
|
|
11
|
+
// package's installed manifest, because `npm rm -g` deletes bin links by
|
|
12
|
+
// name without checking what they point at — left in place, removing the
|
|
13
|
+
// old package would take the fresh `martty` command down with it.
|
|
14
|
+
//
|
|
15
|
+
// `martty` is a small shim file, not a symlink: `npm rm -g martty` removes
|
|
16
|
+
// the package but not this command, and a shim can degrade gracefully —
|
|
17
|
+
// when its package is gone it prints a one-line hint and deletes itself,
|
|
18
|
+
// instead of leaving a dead symlink behind.
|
|
19
|
+
//
|
|
20
|
+
// npm hides postinstall output on success, so the binary re-checks its
|
|
21
|
+
// install root at launch: invoked as a real `martty`/`martty-cli` command
|
|
22
|
+
// it converges the prefix the same way the postinstall does (retire legacy
|
|
23
|
+
// links, recreate `martty`, sanitize) and warns while the legacy package is
|
|
24
|
+
// still present — which is also what recovers a `--ignore-scripts` install.
|
|
25
|
+
//
|
|
26
|
+
// The legacy name is intentionally assembled: `scripts/package-alias.mjs`
|
|
27
|
+
// rewrites the literal `martty` to `martty` in every
|
|
28
|
+
// shipped .js file, which would silently break detection inside the `martty`
|
|
29
|
+
// package itself.
|
|
30
|
+
|
|
31
|
+
import fs from 'node:fs'
|
|
32
|
+
import path from 'node:path'
|
|
33
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
34
|
+
|
|
35
|
+
export const LEGACY_PACKAGE = ['@openma', 'deepseek-harness-tui'].join('/')
|
|
36
|
+
const scopedParts = LEGACY_PACKAGE.split('/')
|
|
37
|
+
const LEGACY_PACKAGE_DIR = scopedParts[scopedParts.length - 1]
|
|
38
|
+
|
|
39
|
+
const COMMAND_NAME = 'martty'
|
|
40
|
+
const DECLARED_BIN_NAME = 'martty-cli'
|
|
41
|
+
const BIN_FILE = path.join('bin', 'martty.js')
|
|
42
|
+
|
|
43
|
+
// Bin names the legacy package may own globally: every published version
|
|
44
|
+
// ships `dsh-tui`, 0.2.14/0.2.19-era releases add `dsb`, and 0.2.14/0.2.20
|
|
45
|
+
// also claim `martty` itself. Links are only removed when they verifiably
|
|
46
|
+
// belong to the legacy package — an installed `martty` package keeps its
|
|
47
|
+
// own `martty` command and a third-party `dsh-tui` stays untouched.
|
|
48
|
+
const LEGACY_BIN_NAMES = [COMMAND_NAME, 'dsh-tui', 'dsb']
|
|
49
|
+
|
|
50
|
+
// Marks the `martty` shims this package writes so ownership checks can tell
|
|
51
|
+
// them from a foreign file that happens to mention a martty path.
|
|
52
|
+
const SHIM_MARKER = 'martty-command-shim'
|
|
53
|
+
const NOT_INSTALLED_LINE =
|
|
54
|
+
'martty: the martty package is not installed - reinstall with: npm i -g martty'
|
|
55
|
+
|
|
56
|
+
const stderrLine = (line) => process.stderr.write(`${line}\n`)
|
|
57
|
+
|
|
58
|
+
// Global installs land in <prefix>/lib/node_modules on POSIX and in
|
|
59
|
+
// <prefix>/node_modules on Windows; npm link uses the same layout.
|
|
60
|
+
export const legacyInstallDirs = (prefix) => [
|
|
61
|
+
path.join(prefix, 'lib', 'node_modules', ...scopedParts),
|
|
62
|
+
path.join(prefix, 'node_modules', ...scopedParts),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
// POSIX global bins live in <prefix>/bin as symlinks; Windows creates a
|
|
66
|
+
// msys shim, a .cmd shim, and a .ps1 shim directly under the prefix.
|
|
67
|
+
export const binCandidates = (prefix, name) => [
|
|
68
|
+
path.join(prefix, 'bin', name),
|
|
69
|
+
path.join(prefix, name),
|
|
70
|
+
path.join(prefix, `${name}.cmd`),
|
|
71
|
+
path.join(prefix, `${name}.ps1`),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
// A bin entry belongs to the legacy package when its symlink target (POSIX)
|
|
75
|
+
// or its shim contents (Windows) point into the legacy install directory.
|
|
76
|
+
// Anything else — a third-party `dsh-tui` package, a hand-written script —
|
|
77
|
+
// is left untouched.
|
|
78
|
+
export function isLegacyOwned(file) {
|
|
79
|
+
let stat
|
|
80
|
+
try {
|
|
81
|
+
stat = fs.lstatSync(file)
|
|
82
|
+
} catch {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
if (stat.isSymbolicLink()) {
|
|
87
|
+
return fs.readlinkSync(file).includes(LEGACY_PACKAGE_DIR)
|
|
88
|
+
}
|
|
89
|
+
if (!stat.isFile()) return false
|
|
90
|
+
return fs.readFileSync(file, 'utf8').includes(LEGACY_PACKAGE_DIR)
|
|
91
|
+
} catch {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Classify the entry at `file` for the `martty` command slot:
|
|
97
|
+
// 'none' — nothing there
|
|
98
|
+
// 'ours' — resolves to (or points at) this package's bin file
|
|
99
|
+
// 'legacy' — belongs to the legacy package (isLegacyOwned)
|
|
100
|
+
// 'foreign' — something else; never overwritten
|
|
101
|
+
function commandOwner(file, binFile) {
|
|
102
|
+
let stat
|
|
103
|
+
try {
|
|
104
|
+
stat = fs.lstatSync(file)
|
|
105
|
+
} catch {
|
|
106
|
+
return 'none'
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
if (stat.isSymbolicLink()) {
|
|
110
|
+
const target = fs.readlinkSync(file)
|
|
111
|
+
if (path.resolve(path.dirname(file), target) === binFile) return 'ours'
|
|
112
|
+
if (target.includes(LEGACY_PACKAGE_DIR)) return 'legacy'
|
|
113
|
+
// A live link whose target names a `martty/` directory points at some
|
|
114
|
+
// martty installation and is left alone; a dangling one is broken
|
|
115
|
+
// anyway and safe to replace. Anything else belongs to another tool.
|
|
116
|
+
if (/martty[/\\]/.test(target)) {
|
|
117
|
+
return fs.existsSync(path.resolve(path.dirname(file), target)) ? 'ours' : 'none'
|
|
118
|
+
}
|
|
119
|
+
return 'foreign'
|
|
120
|
+
}
|
|
121
|
+
if (!stat.isFile()) return 'foreign'
|
|
122
|
+
const content = fs.readFileSync(file, 'utf8')
|
|
123
|
+
if (content.includes(LEGACY_PACKAGE_DIR)) return 'legacy'
|
|
124
|
+
if (content.includes(SHIM_MARKER) || /node_modules[/\\]martty[/\\]/.test(content)) {
|
|
125
|
+
return 'ours'
|
|
126
|
+
}
|
|
127
|
+
return 'foreign'
|
|
128
|
+
} catch {
|
|
129
|
+
return 'foreign'
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The POSIX `martty` command is a shim file, not a symlink: `npm rm -g
|
|
134
|
+
// martty` removes the package but not this command, and a shim can notice
|
|
135
|
+
// its target is gone — it prints a one-line hint and deletes itself instead
|
|
136
|
+
// of leaving a dead symlink behind.
|
|
137
|
+
function posixShim(binFile) {
|
|
138
|
+
const quoted = `'${binFile.replaceAll("'", "'\\''")}'`
|
|
139
|
+
return `#!/bin/sh
|
|
140
|
+
# ${SHIM_MARKER} — written by the martty package; not managed by npm.
|
|
141
|
+
target=${quoted}
|
|
142
|
+
if [ -f "$target" ]; then
|
|
143
|
+
exec node "$target" "$@"
|
|
144
|
+
fi
|
|
145
|
+
echo "${NOT_INSTALLED_LINE}" >&2
|
|
146
|
+
rm -f "$0" 2>/dev/null
|
|
147
|
+
exit 127
|
|
148
|
+
`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Windows receives the same shim triplet npm generates (msys shell wrapper,
|
|
152
|
+
// .cmd, .ps1), with the same degrade-and-remove behavior. `rel` is the bin
|
|
153
|
+
// file's path relative to the shim directory with forward slashes;
|
|
154
|
+
// `backRel` is the backslash variant for cmd.exe.
|
|
155
|
+
function windowsShims(dir, binFile) {
|
|
156
|
+
const rel = path.relative(dir, binFile).split(path.sep).join('/')
|
|
157
|
+
const backRel = rel.split('/').join('\\')
|
|
158
|
+
return {
|
|
159
|
+
[COMMAND_NAME]: `#!/bin/sh
|
|
160
|
+
# ${SHIM_MARKER} — written by the martty package; not managed by npm.
|
|
161
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')")
|
|
162
|
+
|
|
163
|
+
case \`uname\` in
|
|
164
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
165
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
166
|
+
basedir=\`cygpath -w "$basedir"\`
|
|
167
|
+
fi
|
|
168
|
+
;;
|
|
169
|
+
esac
|
|
170
|
+
|
|
171
|
+
target="$basedir/${rel}"
|
|
172
|
+
if [ -f "$target" ]; then
|
|
173
|
+
exec node "$target" "$@"
|
|
174
|
+
fi
|
|
175
|
+
echo "${NOT_INSTALLED_LINE}" >&2
|
|
176
|
+
rm -f "$0" 2>/dev/null
|
|
177
|
+
exit 127
|
|
178
|
+
`,
|
|
179
|
+
[`${COMMAND_NAME}.cmd`]: `@echo off
|
|
180
|
+
rem ${SHIM_MARKER} - written by the martty package; not managed by npm.
|
|
181
|
+
set "target=%~dp0${backRel}"
|
|
182
|
+
if not exist "%target%" goto missing
|
|
183
|
+
node "%target%" %*
|
|
184
|
+
exit /b %ERRORLEVEL%
|
|
185
|
+
:missing
|
|
186
|
+
>&2 echo ${NOT_INSTALLED_LINE}
|
|
187
|
+
del "%~f0" 2>nul
|
|
188
|
+
exit /b 127
|
|
189
|
+
`,
|
|
190
|
+
[`${COMMAND_NAME}.ps1`]: `#!/usr/bin/env pwsh
|
|
191
|
+
# ${SHIM_MARKER} - written by the martty package; not managed by npm.
|
|
192
|
+
$target = Join-Path (Split-Path $MyInvocation.MyCommand.Definition -Parent) '${rel}'
|
|
193
|
+
if (Test-Path -LiteralPath $target) {
|
|
194
|
+
& node $target @args
|
|
195
|
+
exit $LASTEXITCODE
|
|
196
|
+
}
|
|
197
|
+
[Console]::Error.WriteLine('${NOT_INSTALLED_LINE}')
|
|
198
|
+
Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue
|
|
199
|
+
exit 127
|
|
200
|
+
`,
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Create the `martty` command next to wherever `martty-cli` was invoked
|
|
205
|
+
// from (`argv[1]`) or inside `binDir` — which keeps it correct for global
|
|
206
|
+
// prefixes, `npm link`, and a project's node_modules/.bin alike. Existing
|
|
207
|
+
// entries are only replaced when they are ours or verifiably legacy-owned;
|
|
208
|
+
// a foreign `martty` is left untouched with a note about `martty-cli`.
|
|
209
|
+
export function ensureMarttyLink({ argv1, binDir, packageDir, platform = process.platform, write = stderrLine } = {}) {
|
|
210
|
+
const selfPackageDir = packageDir ?? path.dirname(path.dirname(fileURLToPath(import.meta.url)))
|
|
211
|
+
const binFile = path.join(selfPackageDir, BIN_FILE)
|
|
212
|
+
let dir = binDir ?? null
|
|
213
|
+
if (!dir) {
|
|
214
|
+
// Only an invocation under the declared `martty-cli` name earns a
|
|
215
|
+
// `martty` sibling — a plain `node bin/martty.js` must not scribble a
|
|
216
|
+
// link next to whatever script the caller pointed node at.
|
|
217
|
+
const invokedAs = argv1 ? path.basename(argv1).replace(/\.(cmd|ps1)$/i, '') : null
|
|
218
|
+
if (invokedAs !== DECLARED_BIN_NAME) return false
|
|
219
|
+
dir = path.dirname(argv1)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const target = path.join(dir, COMMAND_NAME)
|
|
223
|
+
const owner = commandOwner(target, binFile)
|
|
224
|
+
if (owner === 'ours') return true
|
|
225
|
+
if (owner === 'foreign') {
|
|
226
|
+
write(`martty: \`martty\` in ${dir} belongs to another tool — leaving it untouched.`)
|
|
227
|
+
write('martty: this package\'s command is `martty-cli`.')
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
233
|
+
if (platform === 'win32') {
|
|
234
|
+
for (const [name, content] of Object.entries(windowsShims(dir, binFile))) {
|
|
235
|
+
fs.writeFileSync(path.join(dir, name), content)
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
fs.rmSync(target, { force: true })
|
|
239
|
+
fs.writeFileSync(target, posixShim(binFile))
|
|
240
|
+
fs.chmodSync(target, 0o755)
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
write(`martty: could not create the \`martty\` command in ${dir} — run \`martty-cli\` instead.`)
|
|
244
|
+
return false
|
|
245
|
+
}
|
|
246
|
+
return true
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function legacyNotice(write) {
|
|
250
|
+
write(`${LEGACY_PACKAGE} is the previous name of this package — it now ships as \`martty\`.`)
|
|
251
|
+
write(`To migrate: npm rm -g ${LEGACY_PACKAGE} && npm install --global martty`)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function marttyNotice(write, hint) {
|
|
255
|
+
write(`martty: ${LEGACY_PACKAGE} is still installed. Remove it to finish the rename:`)
|
|
256
|
+
write(`martty: ${hint}`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Remove the bin entries the legacy package owns in `prefix` (POSIX links
|
|
260
|
+
// and Windows shims alike). Foreign entries never qualify; undeletable
|
|
261
|
+
// ones are skipped — they must never fail an install or a launch.
|
|
262
|
+
function retireLegacyBins(prefix, write) {
|
|
263
|
+
const removed = new Set()
|
|
264
|
+
for (const name of LEGACY_BIN_NAMES) {
|
|
265
|
+
for (const file of binCandidates(prefix, name)) {
|
|
266
|
+
if (!isLegacyOwned(file)) continue
|
|
267
|
+
try {
|
|
268
|
+
fs.rmSync(file, { force: true })
|
|
269
|
+
removed.add(name)
|
|
270
|
+
} catch {
|
|
271
|
+
// An undeletable link must not fail the install; the nudge below
|
|
272
|
+
// still names the blocking package.
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (removed.size > 0) {
|
|
277
|
+
const list = [...removed].map((name) => `\`${name}\``).join(', ')
|
|
278
|
+
write(`martty: removed the stale ${list} command${removed.size > 1 ? 's' : ''} left by ${LEGACY_PACKAGE} — the TUI now runs as \`martty\`.`)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// postinstall entry point (`npm_package_name`/`npm_config_*` env): retire
|
|
283
|
+
// the legacy package's bin links, create the `martty` command, and nudge
|
|
284
|
+
// the user to remove the old package. Never fails the install.
|
|
285
|
+
export function migrate(env, write = stderrLine, platform = process.platform) {
|
|
286
|
+
if (env.npm_config_global !== 'true') return
|
|
287
|
+
const prefix = env.npm_config_prefix
|
|
288
|
+
if (!prefix) return
|
|
289
|
+
const packageName = env.npm_package_name ?? ''
|
|
290
|
+
|
|
291
|
+
if (packageName === LEGACY_PACKAGE) {
|
|
292
|
+
legacyNotice(write)
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const legacyInstalled = legacyInstallDirs(prefix).some((dir) => fs.existsSync(dir))
|
|
297
|
+
|
|
298
|
+
retireLegacyBins(prefix, write)
|
|
299
|
+
|
|
300
|
+
const packageDir = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
|
|
301
|
+
const binDir = platform === 'win32' ? prefix : path.join(prefix, 'bin')
|
|
302
|
+
ensureMarttyLink({ binDir, packageDir, platform, write })
|
|
303
|
+
|
|
304
|
+
if (legacyInstalled) {
|
|
305
|
+
sanitizeLegacyInstall(prefix, write)
|
|
306
|
+
marttyNotice(write, `npm rm -g ${LEGACY_PACKAGE}`)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Neutralize the legacy package's `bin` declarations so a later
|
|
311
|
+
// `npm rm -g martty` — which deletes bin links by
|
|
312
|
+
// name without checking what they point at — cannot take the new `martty`
|
|
313
|
+
// command down with it. A real install gets the `bin` field stripped from
|
|
314
|
+
// its installed manifest; an `npm link`ed one is a symlink into the user's
|
|
315
|
+
// checkout — editing that file would dirty their worktree, so the symlink
|
|
316
|
+
// is replaced with a stub package (name + version only) which `npm rm -g`
|
|
317
|
+
// still removes cleanly. Never fails the install.
|
|
318
|
+
export function sanitizeLegacyInstall(prefix, write = stderrLine) {
|
|
319
|
+
for (const legacyDir of legacyInstallDirs(prefix)) {
|
|
320
|
+
let stat
|
|
321
|
+
try {
|
|
322
|
+
stat = fs.lstatSync(legacyDir)
|
|
323
|
+
} catch {
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (stat.isSymbolicLink()) {
|
|
328
|
+
let version
|
|
329
|
+
try {
|
|
330
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(legacyDir, 'package.json'), 'utf8'))
|
|
331
|
+
if (manifest && typeof manifest.version === 'string') version = manifest.version
|
|
332
|
+
} catch {
|
|
333
|
+
// Unreadable linked manifest — the stub below omits the version.
|
|
334
|
+
}
|
|
335
|
+
try {
|
|
336
|
+
fs.rmSync(legacyDir, { force: true })
|
|
337
|
+
fs.mkdirSync(legacyDir, { recursive: true })
|
|
338
|
+
const stub = { name: LEGACY_PACKAGE, ...(version === undefined ? {} : { version }) }
|
|
339
|
+
fs.writeFileSync(path.join(legacyDir, 'package.json'), `${JSON.stringify(stub, null, 2)}\n`)
|
|
340
|
+
} catch {
|
|
341
|
+
write(`martty: could not detach the linked ${LEGACY_PACKAGE} — remove it with \`npm rm -g ${LEGACY_PACKAGE}\`.`)
|
|
342
|
+
}
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!stat.isDirectory()) continue
|
|
347
|
+
const manifestPath = path.join(legacyDir, 'package.json')
|
|
348
|
+
let manifest
|
|
349
|
+
try {
|
|
350
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
|
351
|
+
} catch {
|
|
352
|
+
// A missing or unreadable manifest has no readable bin table for a
|
|
353
|
+
// later `npm rm -g` to act on either — nothing to strip.
|
|
354
|
+
continue
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
if (manifest && typeof manifest === 'object' && 'bin' in manifest) {
|
|
358
|
+
delete manifest.bin
|
|
359
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
write(`martty: could not edit the installed ${LEGACY_PACKAGE} manifest — remove it with \`npm rm -g ${LEGACY_PACKAGE}\`.`)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Launch-time entry point: npm hides postinstall output unless it fails, so
|
|
368
|
+
// the binary re-checks its install root on every run. Invoked through a
|
|
369
|
+
// real command name (`martty`, `martty-cli`, `npx`) it converges the global
|
|
370
|
+
// prefix the same way the postinstall does — retire the legacy links,
|
|
371
|
+
// (re)create `martty`, sanitize the legacy install — which is also what
|
|
372
|
+
// recovers an install whose scripts were skipped. A bare `node
|
|
373
|
+
// bin/martty.js` or a dsh profile layout only gets the warning, never the
|
|
374
|
+
// filesystem changes.
|
|
375
|
+
export function warnAboutLegacyPackage({ argv1, libDir, write = stderrLine, platform = process.platform } = {}) {
|
|
376
|
+
const selfLibDir = libDir ?? path.dirname(fileURLToPath(import.meta.url))
|
|
377
|
+
const packageDir = path.dirname(selfLibDir)
|
|
378
|
+
if (path.basename(packageDir) === LEGACY_PACKAGE_DIR) {
|
|
379
|
+
legacyNotice(write)
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
ensureMarttyLink({ argv1, packageDir, platform, write })
|
|
385
|
+
} catch {
|
|
386
|
+
// A read-only prefix or a missing bin dir must not break the launch.
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Candidate prefixes to converge: the global root this package sits in
|
|
390
|
+
// (lib/node_modules layout), plus the root behind the bin that launched
|
|
391
|
+
// us (covers `npm link` where the package resolves to a checkout).
|
|
392
|
+
const prefixes = []
|
|
393
|
+
const nodeModules = path.dirname(packageDir)
|
|
394
|
+
if (path.basename(nodeModules) === 'node_modules') {
|
|
395
|
+
const layoutDir = path.dirname(nodeModules)
|
|
396
|
+
prefixes.push(path.basename(layoutDir) === 'lib' ? path.dirname(layoutDir) : layoutDir)
|
|
397
|
+
}
|
|
398
|
+
if (argv1) {
|
|
399
|
+
const invokedAs = path.basename(argv1).replace(/\.(cmd|ps1)$/i, '')
|
|
400
|
+
const binDir = path.dirname(argv1)
|
|
401
|
+
if (
|
|
402
|
+
(invokedAs === COMMAND_NAME || invokedAs === DECLARED_BIN_NAME)
|
|
403
|
+
&& (path.basename(binDir) === 'bin' || platform === 'win32')
|
|
404
|
+
) {
|
|
405
|
+
prefixes.push(platform === 'win32' && path.basename(binDir) !== 'bin' ? binDir : path.dirname(binDir))
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let hint = null
|
|
410
|
+
for (const prefix of new Set(prefixes)) {
|
|
411
|
+
if (!legacyInstallDirs(prefix).some((dir) => fs.existsSync(dir))) continue
|
|
412
|
+
const segments = prefix.split(path.sep)
|
|
413
|
+
const profileAt = segments.lastIndexOf('profiles')
|
|
414
|
+
if (profileAt >= 0) {
|
|
415
|
+
// A dsh profile: warn with the plugin-removal hint, touch nothing —
|
|
416
|
+
// profile layouts are managed by dsh, not npm.
|
|
417
|
+
hint ??= `dsh plugin --profile ${segments[profileAt + 1]} remove ${LEGACY_PACKAGE}`
|
|
418
|
+
continue
|
|
419
|
+
}
|
|
420
|
+
hint ??= `npm rm -g ${LEGACY_PACKAGE}`
|
|
421
|
+
try {
|
|
422
|
+
retireLegacyBins(prefix, write)
|
|
423
|
+
const binDir = platform === 'win32' ? prefix : path.join(prefix, 'bin')
|
|
424
|
+
ensureMarttyLink({ binDir, packageDir, platform, write })
|
|
425
|
+
sanitizeLegacyInstall(prefix, write)
|
|
426
|
+
} catch {
|
|
427
|
+
// Best effort: nothing here may break the launch.
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (hint) marttyNotice(write, hint)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Deliver the launch notices collected by warnAboutLegacyPackage. On an
|
|
435
|
+
// interactive POSIX terminal the TUI repaints the whole screen for the
|
|
436
|
+
// session — printing now would bury the notice behind the first frame — so
|
|
437
|
+
// it is deferred to process exit, where it lands right above the next
|
|
438
|
+
// prompt. Piped stderr and Windows consoles get it immediately: an
|
|
439
|
+
// exit-handler write only flushes synchronously to a POSIX TTY or a file.
|
|
440
|
+
export function reportLaunchNotices(
|
|
441
|
+
lines,
|
|
442
|
+
{ tty = process.stderr.isTTY, platform = process.platform, write = stderrLine } = {},
|
|
443
|
+
) {
|
|
444
|
+
if (lines.length === 0) return
|
|
445
|
+
if (tty && platform !== 'win32') {
|
|
446
|
+
process.once('exit', () => {
|
|
447
|
+
for (const line of lines) write(line)
|
|
448
|
+
})
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
for (const line of lines) write(line)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
455
|
+
migrate(process.env)
|
|
456
|
+
}
|
package/lib/queue-view.js
CHANGED
|
@@ -29,11 +29,17 @@ function dockNodes(queue) {
|
|
|
29
29
|
if (!Array.isArray(queue?.items) || queue.items.length === 0) return []
|
|
30
30
|
const expanded = queue.selectedId !== null || queue.editingId !== null
|
|
31
31
|
const count = Number.isInteger(queue.count) ? queue.count : queue.items.length
|
|
32
|
+
// A finished-but-failed attach never binds: the head cannot be sent, so
|
|
33
|
+
// the collapsed title names the failure instead of promising it.
|
|
32
34
|
const title = expanded
|
|
33
35
|
? queue.editingId !== null
|
|
34
36
|
? `Queue · ${count} · enter save · ctrl+d delete · esc cancel`
|
|
35
37
|
: `Queue · ${count} · ↑/↓ choose · enter edit · esc close`
|
|
36
|
-
:
|
|
38
|
+
: queue.attachFailed === true
|
|
39
|
+
? `Queue · ${count} · attach failed · prompt stays queued`
|
|
40
|
+
: queue.authPending === true
|
|
41
|
+
? `Queue · ${count} · sign-in needed · prompt stays queued`
|
|
42
|
+
: `Queue · ${count} · enter send first · ⌥↑ edit`
|
|
37
43
|
const nodes = [{
|
|
38
44
|
id: 'summary',
|
|
39
45
|
kind: 'generic',
|
package/lib/status-view.js
CHANGED
|
@@ -41,13 +41,27 @@ function statusMarkdown(status, stats) {
|
|
|
41
41
|
const lines = []
|
|
42
42
|
lines.push(`- state · ${status.state ?? 'idle'}`)
|
|
43
43
|
lines.push(`- acp · ${status.connection ?? 'not attached'}`)
|
|
44
|
-
if (status.error)
|
|
44
|
+
if (status.error) {
|
|
45
|
+
// A failure carries its diagnostics tail as extra lines (e.g.
|
|
46
|
+
// "Agent stderr: …"); keep the bullet on the headline and give the rest
|
|
47
|
+
// a fenced block so long refusal text stays readable verbatim.
|
|
48
|
+
const [headline, ...rest] = status.error.split('\n')
|
|
49
|
+
lines.push(`- connection error · ${headline}`)
|
|
50
|
+
if (rest.length > 0) {
|
|
51
|
+
lines.push('', '```text', rest.join('\n'), '```')
|
|
52
|
+
}
|
|
53
|
+
}
|
|
45
54
|
if (status.auth?.status !== undefined) {
|
|
46
55
|
lines.push(`- auth · ${status.auth.status}${status.auth.method ? ` · ${status.auth.method}` : ''}`)
|
|
47
56
|
if (status.auth.message) lines.push(`- auth error · ${status.auth.message}`)
|
|
48
57
|
}
|
|
49
58
|
lines.push(`- session · ${status.session?.bound ? status.session.sessionId : 'unbound'}`)
|
|
50
59
|
if (status.server !== undefined) lines.push(`- server · ${status.server}`)
|
|
60
|
+
if (status.dshRuntime !== undefined) {
|
|
61
|
+
const runtime = [status.dshRuntime.version, status.dshRuntime.source && `via ${status.dshRuntime.source}`]
|
|
62
|
+
.filter(Boolean).join(' ')
|
|
63
|
+
lines.push(`- dsh · ${runtime}`)
|
|
64
|
+
}
|
|
51
65
|
if (status.model !== undefined) lines.push(`- model · ${status.model}`)
|
|
52
66
|
if (status.agent !== undefined) lines.push(`- agent · ${status.agent}`)
|
|
53
67
|
if (status.permission !== undefined) lines.push(`- permission · ${status.permission}`)
|
package/lib/tui-queue.js
CHANGED
|
@@ -22,6 +22,8 @@ const EMPTY = Object.freeze({
|
|
|
22
22
|
selectedId: null,
|
|
23
23
|
editingId: null,
|
|
24
24
|
deleteConfirm: false,
|
|
25
|
+
attachFailed: false,
|
|
26
|
+
authPending: false,
|
|
25
27
|
})
|
|
26
28
|
|
|
27
29
|
export function installTuiQueue(ctx) {
|
|
@@ -62,6 +64,8 @@ export function installTuiQueue(ctx) {
|
|
|
62
64
|
selectedId: idOrNull(snapshot.selectedId),
|
|
63
65
|
editingId: idOrNull(snapshot.editingId),
|
|
64
66
|
deleteConfirm: snapshot.deleteConfirm === true,
|
|
67
|
+
attachFailed: snapshot.attachFailed === true,
|
|
68
|
+
authPending: snapshot.authPending === true,
|
|
65
69
|
}
|
|
66
70
|
const next = current()
|
|
67
71
|
for (const listener of [...listeners]) listener(next)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "martty",
|
|
3
|
-
"version": "0.3.2-beta.
|
|
3
|
+
"version": "0.3.2-beta.3",
|
|
4
4
|
"description": "Terminal-native ACP client UI; Cordis client tree, any ACP agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"main": "lib/index.js",
|
|
16
16
|
"scripts": {
|
|
17
|
+
"postinstall": "node lib/install-migration.js",
|
|
17
18
|
"test:harness-ui": "node --test ../scripts/harness-management-tui.test.mjs ../scripts/harness-session-tui.test.mjs",
|
|
18
19
|
"pretest": "node --test ../scripts/workflow-release.test.mjs ../scripts/package-alias.test.mjs ../scripts/harness-removal.test.mjs",
|
|
19
|
-
"test": "node --test ../scripts/download.test.mjs ../scripts/package-native.test.mjs ../scripts/check-release-tag.test.mjs ../scripts/release-check.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-agent-pool.test.mjs ../scripts/harnesses.test.mjs ../scripts/harness-discovery.test.mjs ../scripts/harness-discovery-scenario.test.mjs ../scripts/harness-view.test.mjs ../scripts/harness-onboarding.test.mjs ../scripts/harness-registry.test.mjs ../scripts/harness-badge.test.mjs ../scripts/harness-package.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",
|
|
20
|
+
"test": "node --test ../scripts/download.test.mjs ../scripts/package-native.test.mjs ../scripts/check-release-tag.test.mjs ../scripts/release-check.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-agent-pool.test.mjs ../scripts/harnesses.test.mjs ../scripts/harness-discovery.test.mjs ../scripts/harness-discovery-scenario.test.mjs ../scripts/harness-view.test.mjs ../scripts/harness-onboarding.test.mjs ../scripts/harness-registry.test.mjs ../scripts/harness-badge.test.mjs ../scripts/harness-package.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/install-migration.test.mjs ../scripts/global-install-collision.test.mjs ../scripts/real-agent-e2e.test.mjs",
|
|
20
21
|
"test:profile-install-matrix": "node --test ../scripts/profile-install-matrix.test.mjs"
|
|
21
22
|
},
|
|
22
23
|
"publishConfig": {
|
|
@@ -56,8 +57,7 @@
|
|
|
56
57
|
"./package.json": "./package.json"
|
|
57
58
|
},
|
|
58
59
|
"bin": {
|
|
59
|
-
"martty": "bin/martty.js"
|
|
60
|
-
"dsh-tui": "bin/martty.js"
|
|
60
|
+
"martty-cli": "bin/martty.js"
|
|
61
61
|
},
|
|
62
62
|
"files": [
|
|
63
63
|
"bin",
|
|
@@ -71,12 +71,13 @@
|
|
|
71
71
|
"dsh": {
|
|
72
72
|
"bundle": {
|
|
73
73
|
"patch": "./cordis.patch.yml"
|
|
74
|
-
}
|
|
74
|
+
},
|
|
75
|
+
"supported": "0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2"
|
|
75
76
|
},
|
|
76
77
|
"dependencies": {
|
|
77
78
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
78
79
|
"@lobehub/icons-static-svg": "1.95.0",
|
|
79
|
-
"@openma/deepseek-harness-acp": "0.4.
|
|
80
|
+
"@openma/deepseek-harness-acp": "0.4.38-beta.1",
|
|
80
81
|
"@resvg/resvg-js": "2.6.2",
|
|
81
82
|
"cross-spawn": "^7.0.6",
|
|
82
83
|
"node-downloader-helper": "2.1.11"
|
|
@@ -96,10 +97,10 @@
|
|
|
96
97
|
"agent-client-protocol"
|
|
97
98
|
],
|
|
98
99
|
"optionalDependencies": {
|
|
99
|
-
"@openma/martty-darwin-arm64": "0.3.2-beta.
|
|
100
|
-
"@openma/martty-darwin-x64": "0.3.2-beta.
|
|
101
|
-
"@openma/martty-linux-x64": "0.3.2-beta.
|
|
102
|
-
"@openma/martty-linux-arm64": "0.3.2-beta.
|
|
103
|
-
"@openma/martty-win32-x64": "0.3.2-beta.
|
|
100
|
+
"@openma/martty-darwin-arm64": "0.3.2-beta.3",
|
|
101
|
+
"@openma/martty-darwin-x64": "0.3.2-beta.3",
|
|
102
|
+
"@openma/martty-linux-x64": "0.3.2-beta.3",
|
|
103
|
+
"@openma/martty-linux-arm64": "0.3.2-beta.3",
|
|
104
|
+
"@openma/martty-win32-x64": "0.3.2-beta.3"
|
|
104
105
|
}
|
|
105
106
|
}
|