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