martty 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/bin/dsh-tui.js +68 -0
- package/cordis.patch.yml +30 -0
- package/creator/cordis.patch.yml +6 -0
- package/creator/package.json +10 -0
- package/lib/acp-client-events.js +65 -0
- package/lib/acp-client.js +114 -0
- package/lib/acp-host.js +24 -0
- package/lib/acp-session-config.js +376 -0
- package/lib/acp-session-plan.js +196 -0
- package/lib/acp-session-stats.js +239 -0
- package/lib/agent.js +64 -0
- package/lib/boot.js +119 -0
- package/lib/client-process.js +11 -0
- package/lib/client-run.js +379 -0
- package/lib/cordis-protocol.js +51 -0
- package/lib/creator-overlay.js +77 -0
- package/lib/demo-skin.js +79 -0
- package/lib/ember.js +20 -0
- package/lib/index.js +226 -0
- package/lib/inspect.js +971 -0
- package/lib/jsonrpc-line-transport.js +155 -0
- package/lib/mux.js +281 -0
- package/lib/palettes/default.json +44 -0
- package/lib/palettes/ember.json +44 -0
- package/lib/plan-view.js +92 -0
- package/lib/profile-acp-client.js +11 -0
- package/lib/right-demo.js +55 -0
- package/lib/runner.js +94 -0
- package/lib/spawn-tui.js +179 -0
- package/lib/stats-view.js +90 -0
- package/lib/tui-commands.js +144 -0
- package/lib/tui-overlay.js +252 -0
- package/lib/tui-slots.js +351 -0
- package/lib/tui-theme.js +463 -0
- package/package.json +83 -0
- package/skills/tui-plugin-development/SKILL.md +172 -0
- package/vendor/darwin-arm64/dsh-tui +0 -0
- package/vendor/darwin-x64/dsh-tui +0 -0
- package/vendor/linux-arm64/dsh-tui +0 -0
- package/vendor/linux-x64/dsh-tui +0 -0
- package/vendor/win32-x64/dsh-tui.exe +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenMA contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/openma-ai/Martty/main/assets/tui-whale.svg" width="424" alt="DeepSeek Harness TUI whale" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">DeepSeek Harness TUI</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
Terminal-native ACP client UI with a Cordis client plugin tree.
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Native binaries are packaged for macOS arm64, macOS x64, Linux x64, and Windows
|
|
14
|
+
x64. Requires Node.js 18+.
|
|
15
|
+
|
|
16
|
+
## Recommended
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install --global @deepseek-ai/dsh
|
|
20
|
+
dsh plugin --profile tui add martty@latest
|
|
21
|
+
dsh --profile tui
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The profile command is the recommended install and upgrade path. It creates a
|
|
25
|
+
missing profile and installs the TUI plus its ACP dependency; no global
|
|
26
|
+
`dsh-tui` installation is required.
|
|
27
|
+
|
|
28
|
+
The profile Host mounts the ACP plugin on Base, then starts a separate TUI
|
|
29
|
+
Client process over standard ACP stdin/stdout. For standalone use, run
|
|
30
|
+
`dsh-tui` and use `--agent <cmd>` plus repeated `--agent-arg <arg>` for another
|
|
31
|
+
ACP server. The Node Client process owns a Cordis tree and starts the Rust painter. A sibling
|
|
32
|
+
`tui-cordis-client-runner` publishes TUI Client capabilities and evaluates
|
|
33
|
+
approved `code.client` packages from `dsh-tool-cordis` against that client tree.
|
|
34
|
+
|
|
35
|
+
ACP is a runtime dependency of this package. On a profile that already carries
|
|
36
|
+
a different version through the standard ACP bundle, the package manager may
|
|
37
|
+
retain both copies, but the TUI bundle replaces that surface's rows and mounts
|
|
38
|
+
only the plugin resolved from TUI's own dependency graph. The supported profile
|
|
39
|
+
shape never runs two ACP surfaces for one TUI.
|
|
40
|
+
|
|
41
|
+
The package carries ACP as a runtime dependency and exports its Creator Host
|
|
42
|
+
overlay internally. The profile bundle mounts both on the Host Base tree;
|
|
43
|
+
neither enters the Client tree. Creator adds TUI plugin guidance to the
|
|
44
|
+
existing `cordis` preset and does not use ACP for skill registration.
|
|
45
|
+
|
|
46
|
+
Node and Rust use inherited pipes on Unix and an authenticated loopback TCP
|
|
47
|
+
socket on Windows. This compositor channel carries theme/render data only;
|
|
48
|
+
agent traffic remains ACP.
|
|
49
|
+
|
|
50
|
+
## Demo
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
dsh-tui --demo
|
|
54
|
+
dsh-tui --demo-skin
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`dsh-tui` is the primary command. `dsb` is a compatibility alias.
|
|
58
|
+
|
|
59
|
+
## Highlights
|
|
60
|
+
|
|
61
|
+
- Streamed reasoning, replies, tool activity, subagents, run state, and
|
|
62
|
+
token/cache metrics in one terminal timeline.
|
|
63
|
+
- Agent-advertised models, compositions, permissions, authentication methods,
|
|
64
|
+
and invocable skills in one searchable slash menu.
|
|
65
|
+
- Up to eight editable image chips per prompt, with clipboard/file staging and
|
|
66
|
+
metadata previews on kitty-capable terminals.
|
|
67
|
+
- Terminal-aware Markdown for headings, lists, quotes, code, emphasis, links,
|
|
68
|
+
and mixed CJK/Latin text.
|
|
69
|
+
- Durable sessions, queued follow-ups, immediate steering, readline editing,
|
|
70
|
+
platform-native modifier bindings, mouse selection, and inline-expanded tools.
|
|
71
|
+
- Dark/light themes, clipboard routing for local, tmux, and SSH sessions, plus
|
|
72
|
+
the optional `/liang` pixel companion.
|
|
73
|
+
- A root `chrome.right` plugin rail for validated TuiNode trees, with live
|
|
74
|
+
update/unload and Client inspect support for Creator-authored plugins.
|
|
75
|
+
- Lifecycle-owned local commands and native slider overlays, plus transactions
|
|
76
|
+
over the current Session's standard ACP `configOptions`.
|
|
77
|
+
- Dynamic `code.host` + `code.client` Packages with inspect/run lifecycle and
|
|
78
|
+
package-private Host/Client RPC over a negotiated `_dsh/cordis/*` ACP
|
|
79
|
+
extension. Ordinary ACP agents remain usable when they do not advertise it.
|
|
80
|
+
|
|
81
|
+
## Plugin surface
|
|
82
|
+
|
|
83
|
+
TUI extensions are ordinary Cordis plugins on the Node Client tree. A single
|
|
84
|
+
plugin may register a theme, `chrome.right` nodes, slash commands, overlays,
|
|
85
|
+
timers, Session config transactions, and Host/Client RPC; all contributions
|
|
86
|
+
leave together when its fiber stops or `/theme` replaces the selected Theme
|
|
87
|
+
Plugin. Plugins submit semantic data and never receive the TTY, Ratatui,
|
|
88
|
+
absolute coordinates, compositor fds, or private transport methods.
|
|
89
|
+
|
|
90
|
+
The versioned contract and examples live in the repository's
|
|
91
|
+
[plugin API](https://github.com/openma-ai/Martty/blob/main/docs/plugins.en.md).
|
|
92
|
+
|
|
93
|
+
The demo needs no API key or agent. Run `dsh-tui --help` for agent, model,
|
|
94
|
+
credential, session, theme, and demo options.
|
|
95
|
+
|
|
96
|
+
## Supported platforms
|
|
97
|
+
|
|
98
|
+
| Node platform key | Binary |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `darwin-arm64` | `vendor/darwin-arm64/dsh-tui` |
|
|
101
|
+
| `darwin-x64` | `vendor/darwin-x64/dsh-tui` |
|
|
102
|
+
| `linux-x64` | `vendor/linux-x64/dsh-tui` |
|
|
103
|
+
| `linux-arm64` | `vendor/linux-arm64/dsh-tui` |
|
|
104
|
+
| `win32-x64` | `vendor/win32-x64/dsh-tui.exe` |
|
|
105
|
+
|
|
106
|
+
If installation succeeds but launch reports `no native binary for ...`, confirm
|
|
107
|
+
that you installed the latest version and that your platform appears above.
|
|
108
|
+
|
|
109
|
+
## Uninstall
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
npm uninstall --global martty
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Source, screenshots, development commands, and architecture notes live in the
|
|
116
|
+
[GitHub repository](https://github.com/openma-ai/Martty).
|
|
117
|
+
|
|
118
|
+
[MIT](https://github.com/openma-ai/Martty/blob/main/LICENSE).
|
package/bin/dsh-tui.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { bootClient, parseClientArgv, painterArgs } from '../lib/boot.js'
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
const platformKey = process.platform + '-' + process.arch
|
|
11
|
+
const vendorDir = path.join(__dirname, '..', 'vendor')
|
|
12
|
+
const binaryName = process.platform === 'win32' ? 'dsh-tui.exe' : 'dsh-tui'
|
|
13
|
+
const packaged = path.join(vendorDir, platformKey, binaryName)
|
|
14
|
+
const envBin = process.env.DSH_TUI_BIN
|
|
15
|
+
const binaryPath = typeof envBin === 'string' && envBin.length > 0 && fs.existsSync(envBin)
|
|
16
|
+
? envBin
|
|
17
|
+
: packaged
|
|
18
|
+
|
|
19
|
+
const argv = process.argv.slice(2)
|
|
20
|
+
const wantDemoSkin = argv.includes('--demo-skin')
|
|
21
|
+
const wantDemo = argv.includes('--demo')
|
|
22
|
+
const wantHelp = argv.includes('-h') || argv.includes('--help')
|
|
23
|
+
const wantVersion = argv.includes('-V') || argv.includes('--version')
|
|
24
|
+
|
|
25
|
+
if (!fs.existsSync(binaryPath)) {
|
|
26
|
+
let available = []
|
|
27
|
+
try {
|
|
28
|
+
available = fs
|
|
29
|
+
.readdirSync(vendorDir, { withFileTypes: true })
|
|
30
|
+
.filter((e) => e.isDirectory())
|
|
31
|
+
.map((e) => e.name)
|
|
32
|
+
} catch {
|
|
33
|
+
// vendor dir missing entirely
|
|
34
|
+
}
|
|
35
|
+
console.error('dsh-tui: no bundled binary for this platform (' + platformKey + ').')
|
|
36
|
+
if (available.length > 0) {
|
|
37
|
+
console.error('Bundled platforms: ' + available.join(', '))
|
|
38
|
+
} else {
|
|
39
|
+
console.error('No platform binaries are bundled in this install.')
|
|
40
|
+
}
|
|
41
|
+
console.error('Hint: run scripts/build-npm.sh on this machine to build and package a native binary.')
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function exitFromSpawn(result) {
|
|
46
|
+
process.exit(
|
|
47
|
+
result.status ??
|
|
48
|
+
(result.signal === 'SIGINT' ? 130 : result.signal === 'SIGTERM' ? 143 : 1),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (wantHelp || wantVersion || wantDemo) {
|
|
53
|
+
exitFromSpawn(spawnSync(binaryPath, argv, { stdio: 'inherit' }))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (wantDemoSkin) {
|
|
57
|
+
process.env.DSH_TUI_BIN ??= binaryPath
|
|
58
|
+
const demoSkinPath = path.join(__dirname, '..', 'lib', 'demo-skin.js')
|
|
59
|
+
const args = argv.filter((arg) => arg !== '--demo-skin')
|
|
60
|
+
exitFromSpawn(spawnSync(process.execPath, [demoSkinPath, ...args], {
|
|
61
|
+
stdio: 'inherit',
|
|
62
|
+
env: process.env,
|
|
63
|
+
}))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
process.env.DSH_TUI_BIN ??= binaryPath
|
|
67
|
+
const parsed = parseClientArgv(argv)
|
|
68
|
+
await bootClient({ agent: parsed.agent, extraArgs: painterArgs(parsed) })
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Host-side entries for the TUI profile surface. The ACP plugin mounts on the
|
|
2
|
+
# existing Base Cordis tree. The runner starts a separate TUI Client process;
|
|
3
|
+
# standard stdin/stdout carries ACP between them. Client plugins never mount
|
|
4
|
+
# beside the Harness.
|
|
5
|
+
|
|
6
|
+
# If this profile already carries an ACP surface bundle, TUI replaces its
|
|
7
|
+
# provider/stdio rows with the same ACP plugin resolved through TUI's declared
|
|
8
|
+
# dependency. The profile transport is replaced because its stdio belongs to
|
|
9
|
+
# the TUI launcher, not an external ACP client. Missing ids are harmless.
|
|
10
|
+
- id: acp-plugin
|
|
11
|
+
disabled: true
|
|
12
|
+
|
|
13
|
+
- id: acp-server
|
|
14
|
+
disabled: true
|
|
15
|
+
|
|
16
|
+
- id: acp-bridge
|
|
17
|
+
disabled: true
|
|
18
|
+
|
|
19
|
+
- id: tui-creator-overlay
|
|
20
|
+
disabled: true
|
|
21
|
+
|
|
22
|
+
- insert:
|
|
23
|
+
- id: tui-acp-host
|
|
24
|
+
name: 'martty/acp-host'
|
|
25
|
+
|
|
26
|
+
- id: tui-host-creator
|
|
27
|
+
name: 'martty/creator-overlay'
|
|
28
|
+
|
|
29
|
+
- id: tui-runner
|
|
30
|
+
name: 'martty/runner'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/** Effect-scoped fan-out for standard ACP messages observed by Client plugins. */
|
|
2
|
+
|
|
3
|
+
import { Service } from '@deepseek-ai/cordis'
|
|
4
|
+
|
|
5
|
+
export const name = 'acp-client-events'
|
|
6
|
+
export const inject = []
|
|
7
|
+
|
|
8
|
+
class AcpClientEventsService extends Service {
|
|
9
|
+
constructor(ctx, core) {
|
|
10
|
+
super(ctx, 'acpClientEvents')
|
|
11
|
+
this.core = core
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
register(observer) { return this.core.register(this.ctx, observer) }
|
|
15
|
+
observeClient(message) { return this.core.observeClient(message) }
|
|
16
|
+
observeAgent(message) { return this.core.observeAgent(message) }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function installAcpClientEvents(ctx) {
|
|
20
|
+
const observers = new Set()
|
|
21
|
+
|
|
22
|
+
function register(effectCtx, observer) {
|
|
23
|
+
if (observer === null || typeof observer !== 'object'
|
|
24
|
+
|| (typeof observer.observeClient !== 'function'
|
|
25
|
+
&& typeof observer.observeAgent !== 'function')) {
|
|
26
|
+
throw new Error('acpClientEvents.register: observeClient or observeAgent is required')
|
|
27
|
+
}
|
|
28
|
+
const setup = () => {
|
|
29
|
+
observers.add(observer)
|
|
30
|
+
return () => observers.delete(observer)
|
|
31
|
+
}
|
|
32
|
+
const release = typeof effectCtx?.effect === 'function'
|
|
33
|
+
? effectCtx.effect(setup, 'acpClientEvents.register')
|
|
34
|
+
: setup()
|
|
35
|
+
let disposed = false
|
|
36
|
+
return () => {
|
|
37
|
+
if (disposed) return
|
|
38
|
+
disposed = true
|
|
39
|
+
return release?.()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function observeClient(message) {
|
|
44
|
+
for (const observer of [...observers]) observer.observeClient?.(message)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function observeAgent(message) {
|
|
48
|
+
for (const observer of [...observers]) observer.observeAgent?.(message)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const core = { register, observeClient, observeAgent }
|
|
52
|
+
const service = typeof ctx.provide === 'function'
|
|
53
|
+
? new AcpClientEventsService(ctx, core)
|
|
54
|
+
: {
|
|
55
|
+
register(observer) { return register(ctx, observer) },
|
|
56
|
+
observeClient,
|
|
57
|
+
observeAgent,
|
|
58
|
+
}
|
|
59
|
+
if (typeof ctx.provide !== 'function') ctx.acpClientEvents = service
|
|
60
|
+
return service
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function apply(ctx) {
|
|
64
|
+
installAcpClientEvents(ctx)
|
|
65
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cordis plugin: attach to any ACP agent by spawn or an existing stream.
|
|
3
|
+
*
|
|
4
|
+
* Provides `ctx.acpClient` plus the standard-ACP-backed
|
|
5
|
+
* `ctx.acpSessionConfig`. Does not import a harness, dsh, or dsh-acp.
|
|
6
|
+
* Switching agents is `{ command, args }` (or `config.stream`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from 'node:child_process'
|
|
10
|
+
import { installAcpClientEvents } from './acp-client-events.js'
|
|
11
|
+
import { installAcpSessionConfig } from './acp-session-config.js'
|
|
12
|
+
import { installAcpSessionPlan } from './acp-session-plan.js'
|
|
13
|
+
import { installAcpSessionStats } from './acp-session-stats.js'
|
|
14
|
+
|
|
15
|
+
export { installAcpSessionConfig } from './acp-session-config.js'
|
|
16
|
+
export { installAcpSessionPlan } from './acp-session-plan.js'
|
|
17
|
+
export { installAcpSessionStats } from './acp-session-stats.js'
|
|
18
|
+
export { installAcpClientEvents } from './acp-client-events.js'
|
|
19
|
+
|
|
20
|
+
export const name = 'acp-client'
|
|
21
|
+
export const inject = []
|
|
22
|
+
|
|
23
|
+
/** @type {null | { command: string, args: string[], child: import('node:child_process').ChildProcess, stdin: import('node:stream').Writable, stdout: import('node:stream').Readable, kind: 'spawn' }} */
|
|
24
|
+
let liveAgent = null
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {{ command: string, args?: string[], env?: Record<string, string> }} AgentSpec
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {unknown} config
|
|
32
|
+
* @returns {AgentSpec}
|
|
33
|
+
*/
|
|
34
|
+
export function resolveAgent(config = {}) {
|
|
35
|
+
const fromConfig = config && typeof config === 'object' ? config.agent : undefined
|
|
36
|
+
if (fromConfig && typeof fromConfig === 'object' && typeof fromConfig.command === 'string') {
|
|
37
|
+
return {
|
|
38
|
+
command: fromConfig.command,
|
|
39
|
+
args: Array.isArray(fromConfig.args) ? fromConfig.args.map(String) : [],
|
|
40
|
+
...(fromConfig.env !== undefined ? { env: fromConfig.env } : {}),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const envCmd = process.env.DSH_TUI_AGENT
|
|
44
|
+
if (typeof envCmd === 'string' && envCmd.trim().length > 0) {
|
|
45
|
+
const tokens = envCmd.trim().split(/\s+/)
|
|
46
|
+
return { command: tokens[0], args: tokens.slice(1) }
|
|
47
|
+
}
|
|
48
|
+
return { command: 'dsh-acp', args: [] }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {object} ctx
|
|
53
|
+
* @param {object} [config]
|
|
54
|
+
*/
|
|
55
|
+
export function apply(ctx, config = {}) {
|
|
56
|
+
const events = installAcpClientEvents(ctx)
|
|
57
|
+
const sessionConfig = installAcpSessionConfig(ctx)
|
|
58
|
+
const sessionPlan = installAcpSessionPlan(ctx)
|
|
59
|
+
const sessionStats = installAcpSessionStats(ctx)
|
|
60
|
+
events.register(sessionConfig)
|
|
61
|
+
events.register(sessionPlan)
|
|
62
|
+
events.register(sessionStats)
|
|
63
|
+
if (config.stream !== undefined && config.stream !== null) {
|
|
64
|
+
const service = {
|
|
65
|
+
kind: 'stream',
|
|
66
|
+
stdin: config.stream.stdin,
|
|
67
|
+
stdout: config.stream.stdout,
|
|
68
|
+
child: config.stream.child,
|
|
69
|
+
}
|
|
70
|
+
provide(ctx, service)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
const agent = resolveAgent(config)
|
|
74
|
+
if (
|
|
75
|
+
liveAgent !== null
|
|
76
|
+
&& liveAgent.child.exitCode === null
|
|
77
|
+
&& liveAgent.command === agent.command
|
|
78
|
+
&& JSON.stringify(liveAgent.args) === JSON.stringify(agent.args ?? [])
|
|
79
|
+
) {
|
|
80
|
+
provide(ctx, liveAgent)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
const child = spawn(agent.command, agent.args ?? [], {
|
|
84
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
85
|
+
env: { ...process.env, ...(agent.env ?? {}) },
|
|
86
|
+
})
|
|
87
|
+
child.stdin.on('error', () => {})
|
|
88
|
+
child.stdout.on('error', () => {})
|
|
89
|
+
const service = {
|
|
90
|
+
kind: 'spawn',
|
|
91
|
+
command: agent.command,
|
|
92
|
+
args: agent.args ?? [],
|
|
93
|
+
stdin: child.stdin,
|
|
94
|
+
stdout: child.stdout,
|
|
95
|
+
child,
|
|
96
|
+
}
|
|
97
|
+
liveAgent = service
|
|
98
|
+
provide(ctx, service)
|
|
99
|
+
process.once('exit', () => {
|
|
100
|
+
try {
|
|
101
|
+
child.kill('SIGTERM')
|
|
102
|
+
} catch {
|
|
103
|
+
// already gone
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function provide(ctx, service) {
|
|
109
|
+
if (typeof ctx.provide === 'function') {
|
|
110
|
+
ctx.provide('acpClient', service)
|
|
111
|
+
} else {
|
|
112
|
+
ctx.acpClient = service
|
|
113
|
+
}
|
|
114
|
+
}
|
package/lib/acp-host.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Resolve the embeddable ACP plugin from TUI's own dependency graph. */
|
|
2
|
+
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
|
|
5
|
+
export const name = 'dsh-tui-acp-host'
|
|
6
|
+
export const inject = ['loader']
|
|
7
|
+
|
|
8
|
+
const requireFromTui = createRequire(import.meta.url)
|
|
9
|
+
|
|
10
|
+
function ownAcpPlugin() {
|
|
11
|
+
const specifier = '@openma/deepseek-harness-acp/plugin'
|
|
12
|
+
try {
|
|
13
|
+
return requireFromTui.resolve(specifier)
|
|
14
|
+
} catch {
|
|
15
|
+
// A source-linked package may rely on the active profile's installation.
|
|
16
|
+
return specifier
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function apply(ctx, config) {
|
|
21
|
+
const exports = await ctx.loader.import(ownAcpPlugin())
|
|
22
|
+
const plugin = ctx.loader.unwrapExports(exports)
|
|
23
|
+
await ctx.plugin(plugin, config)
|
|
24
|
+
}
|