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,376 @@
1
+ /**
2
+ * Live standard ACP Session configuration on the Client tree.
3
+ *
4
+ * The mux observes the existing ACP setup/update traffic. Mutations are sent
5
+ * to the native ACP client, which remains the sole owner of
6
+ * `session/set_config_option` and therefore also folds response-only state.
7
+ */
8
+
9
+ import { Service } from '@deepseek-ai/cordis'
10
+ import { CORDIS_METHODS, CORDIS_PROTOCOL } from './cordis-protocol.js'
11
+
12
+ export const name = 'acp-session-config'
13
+ export const inject = []
14
+
15
+ const SETUP_METHODS = new Set(['session/new', 'session/load'])
16
+
17
+ class AcpSessionConfigService extends Service {
18
+ constructor(ctx, core) {
19
+ super(ctx, 'acpSessionConfig')
20
+ this.core = core
21
+ }
22
+
23
+ list() {
24
+ return this.core.list()
25
+ }
26
+
27
+ current(id) {
28
+ return this.core.current(id)
29
+ }
30
+
31
+ byCategory(category) {
32
+ return this.core.byCategory(category)
33
+ }
34
+
35
+ transaction(selector) {
36
+ return this.core.transaction(selector)
37
+ }
38
+
39
+ set(id, value) {
40
+ return this.core.set(id, value)
41
+ }
42
+
43
+ subscribe(listener) {
44
+ return this.core.subscribe(this.ctx, listener)
45
+ }
46
+
47
+ bindTransport(requestTui) {
48
+ return this.core.bindTransport(requestTui)
49
+ }
50
+
51
+ observeClient(message) {
52
+ return this.core.observeClient(message)
53
+ }
54
+
55
+ observeAgent(message) {
56
+ return this.core.observeAgent(message)
57
+ }
58
+ }
59
+
60
+ /** Install the `ctx.acpSessionConfig` standard ACP state service. */
61
+ export function installAcpSessionConfig(ctx) {
62
+ let sessionId
63
+ let options = []
64
+ let requestTui
65
+ const pending = new Map()
66
+ const listeners = new Set()
67
+
68
+ function list() {
69
+ return cloneJson(options)
70
+ }
71
+
72
+ function current(id) {
73
+ if (typeof id !== 'string' || id.length === 0) {
74
+ throw new Error('acpSessionConfig.current: id must be a non-empty string')
75
+ }
76
+ const option = options.find((candidate) => candidate?.id === id)
77
+ return cloneJson(option?.currentValue ?? option?.current_value)
78
+ }
79
+
80
+ function byCategory(category) {
81
+ if (typeof category !== 'string' || category.length === 0) {
82
+ throw new Error('acpSessionConfig.byCategory: category must be a non-empty string')
83
+ }
84
+ return cloneJson(options.filter((candidate) => candidate?.category === category))
85
+ }
86
+
87
+ function snapshot() {
88
+ return { sessionId, options: list() }
89
+ }
90
+
91
+ function publish(nextSessionId, nextOptions) {
92
+ if (typeof nextSessionId === 'string' && nextSessionId.length > 0) {
93
+ sessionId = nextSessionId
94
+ }
95
+ if (!Array.isArray(nextOptions)) return
96
+ options = cloneJson(nextOptions)
97
+ const next = snapshot()
98
+ for (const listener of [...listeners]) listener(next)
99
+ }
100
+
101
+ function subscribe(effectCtx, listener) {
102
+ if (typeof listener !== 'function') {
103
+ throw new Error('acpSessionConfig.subscribe: listener must be a function')
104
+ }
105
+ const setup = () => {
106
+ listeners.add(listener)
107
+ return () => listeners.delete(listener)
108
+ }
109
+ const release = typeof effectCtx?.effect === 'function'
110
+ ? effectCtx.effect(setup, 'acpSessionConfig.subscribe')
111
+ : setup()
112
+ let disposed = false
113
+ return () => {
114
+ if (disposed) return
115
+ disposed = true
116
+ return release?.()
117
+ }
118
+ }
119
+
120
+ function observeClient(message) {
121
+ if (!isObject(message) || message.id === undefined || typeof message.method !== 'string') return
122
+ if (SETUP_METHODS.has(message.method)) {
123
+ const requested = message.method === 'session/load'
124
+ ? readString(message.params, 'sessionId', 'session_id')
125
+ : undefined
126
+ pending.set(message.id, { kind: 'setup', sessionId: requested })
127
+ return
128
+ }
129
+ if (message.method === 'session/set_config_option') {
130
+ pending.set(message.id, {
131
+ kind: 'config',
132
+ sessionId: readString(message.params, 'sessionId', 'session_id') ?? sessionId,
133
+ })
134
+ }
135
+ }
136
+
137
+ function observeAgent(message) {
138
+ if (!isObject(message)) return
139
+ if (message.id !== undefined && pending.has(message.id)) {
140
+ const tracked = pending.get(message.id)
141
+ pending.delete(message.id)
142
+ if (message.error !== undefined || !isObject(message.result)) return
143
+ const bound = readString(message.result, 'sessionId', 'session_id') ?? tracked.sessionId
144
+ const next = readOptions(message.result)
145
+ if (tracked.kind === 'setup') {
146
+ sessionId = bound
147
+ publish(bound, next ?? [])
148
+ } else if (bound === undefined || bound === sessionId) {
149
+ publish(bound, next)
150
+ }
151
+ return
152
+ }
153
+ if (message.method !== 'session/update' || !isObject(message.params)) return
154
+ const updatedSession = readString(message.params, 'sessionId', 'session_id')
155
+ if (sessionId !== undefined && updatedSession !== sessionId) return
156
+ const update = message.params.update
157
+ if (!isObject(update)) return
158
+ const type = readString(update, 'sessionUpdate', 'session_update')
159
+ if (type !== 'config_option_update') return
160
+ publish(updatedSession, readOptions(update))
161
+ }
162
+
163
+ function bindTransport(nextRequestTui) {
164
+ if (typeof nextRequestTui !== 'function') {
165
+ throw new Error('acpSessionConfig.bindTransport: requestTui must be a function')
166
+ }
167
+ requestTui = nextRequestTui
168
+ return () => {
169
+ if (requestTui === nextRequestTui) requestTui = undefined
170
+ }
171
+ }
172
+
173
+ async function set(id, value) {
174
+ if (typeof id !== 'string' || id.length === 0) {
175
+ throw new Error('acpSessionConfig.set: id must be a non-empty string')
176
+ }
177
+ if (sessionId === undefined) {
178
+ throw new Error('acpSessionConfig.set: no ACP Session is active')
179
+ }
180
+ if (typeof requestTui !== 'function') {
181
+ throw new Error('acpSessionConfig.set: native ACP transport is unavailable')
182
+ }
183
+ const option = options.find((candidate) => candidate?.id === id)
184
+ if (option === undefined) {
185
+ throw new Error(`acpSessionConfig.set: option "${id}" is not advertised by the current Session`)
186
+ }
187
+ validateValue(option, value)
188
+ const result = await requestTui(CORDIS_METHODS.sessionConfigSet, {
189
+ protocol: CORDIS_PROTOCOL,
190
+ sessionId,
191
+ configId: id,
192
+ value,
193
+ })
194
+ if (!isObject(result)) {
195
+ throw new Error('acpSessionConfig.set: native ACP client returned an invalid response')
196
+ }
197
+ const resultSession = readString(result, 'sessionId', 'session_id') ?? sessionId
198
+ publish(resultSession, readOptions(result))
199
+ return list()
200
+ }
201
+
202
+ function transaction(selector) {
203
+ const option = resolveTransactionOption(selector, options)
204
+ const original = cloneJson(option.currentValue ?? option.current_value)
205
+ if (original === undefined) {
206
+ throw new Error(
207
+ `acpSessionConfig.transaction: option "${option.id}" has no current value to restore`,
208
+ )
209
+ }
210
+
211
+ let desired = cloneJson(original)
212
+ let applied = cloneJson(original)
213
+ let chain = Promise.resolve(list())
214
+ let finalizing
215
+ let settled = false
216
+
217
+ const write = async (value) => {
218
+ const result = await set(option.id, value)
219
+ applied = cloneJson(value)
220
+ return result
221
+ }
222
+
223
+ const preview = (value) => {
224
+ if (settled) return Promise.resolve(list())
225
+ if (finalizing) return finalizing
226
+ if (Object.is(value, desired)) return chain
227
+ desired = cloneJson(value)
228
+ const next = cloneJson(value)
229
+ const task = () => write(next)
230
+ chain = chain.then(task, task)
231
+ return chain
232
+ }
233
+
234
+ const finish = (value) => {
235
+ if (settled) return finalizing ?? Promise.resolve(list())
236
+ if (finalizing) return finalizing
237
+ desired = cloneJson(value)
238
+ const settle = async () => {
239
+ if (!Object.is(applied, desired)) await write(desired)
240
+ settled = true
241
+ return list()
242
+ }
243
+ finalizing = chain.then(settle, settle).catch((error) => {
244
+ finalizing = undefined
245
+ throw error
246
+ })
247
+ return finalizing
248
+ }
249
+
250
+ return {
251
+ option: cloneJson(option),
252
+ original: cloneJson(original),
253
+ value() {
254
+ return cloneJson(desired)
255
+ },
256
+ preview,
257
+ commit(value = desired) {
258
+ return finish(value)
259
+ },
260
+ rollback() {
261
+ return finish(original)
262
+ },
263
+ }
264
+ }
265
+
266
+ const core = {
267
+ list,
268
+ current,
269
+ byCategory,
270
+ transaction,
271
+ set,
272
+ subscribe,
273
+ bindTransport,
274
+ observeClient,
275
+ observeAgent,
276
+ }
277
+ const service = typeof ctx.provide === 'function'
278
+ ? new AcpSessionConfigService(ctx, core)
279
+ : {
280
+ list,
281
+ current,
282
+ byCategory,
283
+ transaction,
284
+ set,
285
+ subscribe(listener) {
286
+ return subscribe(ctx, listener)
287
+ },
288
+ bindTransport,
289
+ observeClient,
290
+ observeAgent,
291
+ }
292
+ if (typeof ctx.provide !== 'function') ctx.acpSessionConfig = service
293
+ return service
294
+ }
295
+
296
+ function resolveTransactionOption(selector, options) {
297
+ if (!isObject(selector)) {
298
+ throw new Error('acpSessionConfig.transaction: selector must be an object')
299
+ }
300
+ const hasCategory = typeof selector.category === 'string' && selector.category.length > 0
301
+ const hasId = typeof selector.id === 'string' && selector.id.length > 0
302
+ if (hasCategory === hasId) {
303
+ throw new Error(
304
+ 'acpSessionConfig.transaction: selector must contain exactly one non-empty category or id',
305
+ )
306
+ }
307
+ const matches = hasCategory
308
+ ? options.filter((candidate) => candidate?.category === selector.category)
309
+ : options.filter((candidate) => candidate?.id === selector.id)
310
+ if (matches.length !== 1) {
311
+ const key = hasCategory ? `category "${selector.category}"` : `id "${selector.id}"`
312
+ throw new Error(
313
+ `acpSessionConfig.transaction: expected exactly one option for ${key}, found ${matches.length}`,
314
+ )
315
+ }
316
+ return cloneJson(matches[0])
317
+ }
318
+
319
+ function validateValue(option, value) {
320
+ const kind = option.type
321
+ const current = option.currentValue ?? option.current_value
322
+ if (kind === 'boolean' || typeof current === 'boolean') {
323
+ if (typeof value !== 'boolean') {
324
+ throw new Error(`acpSessionConfig.set: option "${option.id}" requires a boolean value`)
325
+ }
326
+ return
327
+ }
328
+ if (typeof value !== 'string') {
329
+ throw new Error(`acpSessionConfig.set: option "${option.id}" requires a string value id`)
330
+ }
331
+ if (kind === 'select' && Array.isArray(option.options)) {
332
+ const values = selectValues(option.options)
333
+ if (!values.has(value)) {
334
+ throw new Error(
335
+ `acpSessionConfig.set: value "${value}" is not advertised for option "${option.id}"`,
336
+ )
337
+ }
338
+ }
339
+ }
340
+
341
+ function selectValues(entries, out = new Set()) {
342
+ for (const entry of entries) {
343
+ if (!isObject(entry)) continue
344
+ const value = entry.value ?? entry.id
345
+ if (typeof value === 'string') out.add(value)
346
+ if (Array.isArray(entry.options)) selectValues(entry.options, out)
347
+ }
348
+ return out
349
+ }
350
+
351
+ function readOptions(value) {
352
+ if (!isObject(value)) return undefined
353
+ const found = value.configOptions ?? value.config_options
354
+ return Array.isArray(found) ? found : undefined
355
+ }
356
+
357
+ function readString(value, ...keys) {
358
+ if (!isObject(value)) return undefined
359
+ for (const key of keys) {
360
+ if (typeof value[key] === 'string' && value[key].length > 0) return value[key]
361
+ }
362
+ return undefined
363
+ }
364
+
365
+ function isObject(value) {
366
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
367
+ }
368
+
369
+ function cloneJson(value) {
370
+ if (value === undefined) return undefined
371
+ return structuredClone(value)
372
+ }
373
+
374
+ export function apply(ctx) {
375
+ installAcpSessionConfig(ctx)
376
+ }
@@ -0,0 +1,196 @@
1
+ /** Live structured Plan state folded from standard ACP Session updates. */
2
+
3
+ import { Service } from '@deepseek-ai/cordis'
4
+
5
+ export const name = 'acp-session-plan'
6
+ export const inject = []
7
+
8
+ const SETUP_METHODS = new Set(['session/new', 'session/load'])
9
+
10
+ class AcpSessionPlanService extends Service {
11
+ constructor(ctx, core) {
12
+ super(ctx, 'acpSessionPlan')
13
+ this.core = core
14
+ }
15
+
16
+ list() {
17
+ return this.core.list()
18
+ }
19
+
20
+ current() {
21
+ return this.core.current()
22
+ }
23
+
24
+ subscribe(listener) {
25
+ return this.core.subscribe(this.ctx, listener)
26
+ }
27
+
28
+ observeClient(message) {
29
+ return this.core.observeClient(message)
30
+ }
31
+
32
+ observeAgent(message) {
33
+ return this.core.observeAgent(message)
34
+ }
35
+ }
36
+
37
+ /** Install `ctx.acpSessionPlan`, backed only by standard ACP traffic. */
38
+ export function installAcpSessionPlan(ctx) {
39
+ let sessionId
40
+ const plans = new Map()
41
+ const pending = new Map()
42
+ const listeners = new Set()
43
+
44
+ function list() {
45
+ return cloneJson([...plans.values()])
46
+ }
47
+
48
+ function current() {
49
+ return cloneJson([...plans.values()].at(-1) ?? null)
50
+ }
51
+
52
+ function snapshot() {
53
+ return { sessionId, plans: list() }
54
+ }
55
+
56
+ function publish() {
57
+ const next = snapshot()
58
+ for (const listener of [...listeners]) listener(next)
59
+ }
60
+
61
+ function reset(nextSessionId) {
62
+ sessionId = nextSessionId
63
+ plans.clear()
64
+ publish()
65
+ }
66
+
67
+ function subscribe(effectCtx, listener) {
68
+ if (typeof listener !== 'function') {
69
+ throw new Error('acpSessionPlan.subscribe: listener must be a function')
70
+ }
71
+ const setup = () => {
72
+ listeners.add(listener)
73
+ return () => listeners.delete(listener)
74
+ }
75
+ const release = typeof effectCtx?.effect === 'function'
76
+ ? effectCtx.effect(setup, 'acpSessionPlan.subscribe')
77
+ : setup()
78
+ let disposed = false
79
+ return () => {
80
+ if (disposed) return
81
+ disposed = true
82
+ return release?.()
83
+ }
84
+ }
85
+
86
+ function observeClient(message) {
87
+ if (!isObject(message) || message.id === undefined || !SETUP_METHODS.has(message.method)) return
88
+ pending.set(message.id, {
89
+ sessionId: message.method === 'session/load'
90
+ ? readString(message.params, 'sessionId', 'session_id')
91
+ : undefined,
92
+ })
93
+ }
94
+
95
+ function observeAgent(message) {
96
+ if (!isObject(message)) return
97
+ if (message.id !== undefined && pending.has(message.id)) {
98
+ const tracked = pending.get(message.id)
99
+ pending.delete(message.id)
100
+ if (message.error !== undefined || !isObject(message.result)) return
101
+ reset(readString(message.result, 'sessionId', 'session_id') ?? tracked.sessionId)
102
+ return
103
+ }
104
+ if (message.method !== 'session/update' || !isObject(message.params)) return
105
+ const updatedSession = readString(message.params, 'sessionId', 'session_id')
106
+ if (sessionId !== undefined && updatedSession !== sessionId) return
107
+ if (sessionId === undefined && updatedSession !== undefined) sessionId = updatedSession
108
+ const update = message.params.update
109
+ if (!isObject(update)) return
110
+ const type = readString(update, 'sessionUpdate', 'session_update')
111
+ if (type === 'plan_removed') {
112
+ const id = readString(update, 'planId', 'plan_id')
113
+ if (id === undefined) plans.clear()
114
+ else plans.delete(id)
115
+ publish()
116
+ return
117
+ }
118
+ if (type !== 'plan' && type !== 'plan_update') return
119
+ const plan = normalizePlan(update)
120
+ if (plan === null) return
121
+ if (plan.kind === 'items' && plan.entries.length === 0) {
122
+ plans.delete(plan.id)
123
+ } else {
124
+ plans.delete(plan.id)
125
+ plans.set(plan.id, plan)
126
+ }
127
+ publish()
128
+ }
129
+
130
+ const core = { list, current, subscribe, observeClient, observeAgent }
131
+ const service = typeof ctx.provide === 'function'
132
+ ? new AcpSessionPlanService(ctx, core)
133
+ : {
134
+ list,
135
+ current,
136
+ subscribe(listener) {
137
+ return subscribe(ctx, listener)
138
+ },
139
+ observeClient,
140
+ observeAgent,
141
+ }
142
+ if (typeof ctx.provide !== 'function') ctx.acpSessionPlan = service
143
+ return service
144
+ }
145
+
146
+ function normalizePlan(update) {
147
+ const nested = isObject(update.plan) ? update.plan : update
148
+ const id = readString(nested, 'planId', 'plan_id')
149
+ ?? readString(update, 'planId', 'plan_id')
150
+ ?? 'default'
151
+ const declaredKind = readString(nested, 'type', 'kind')
152
+ if (declaredKind === 'items' || Array.isArray(nested.entries)) {
153
+ const entries = Array.isArray(nested.entries)
154
+ ? nested.entries.map(normalizeEntry).filter((entry) => entry !== null)
155
+ : []
156
+ return { id, kind: 'items', entries }
157
+ }
158
+ if (declaredKind === 'markdown' || typeof nested.content === 'string') {
159
+ const content = typeof nested.content === 'string' ? nested.content : ''
160
+ return { id, kind: 'markdown', content }
161
+ }
162
+ if (declaredKind === 'file' || typeof nested.uri === 'string') {
163
+ const uri = typeof nested.uri === 'string' ? nested.uri : ''
164
+ return { id, kind: 'file', uri }
165
+ }
166
+ return null
167
+ }
168
+
169
+ function normalizeEntry(value) {
170
+ if (!isObject(value) || typeof value.content !== 'string') return null
171
+ return {
172
+ content: value.content,
173
+ ...(typeof value.priority === 'string' ? { priority: value.priority } : {}),
174
+ ...(typeof value.status === 'string' ? { status: value.status } : {}),
175
+ }
176
+ }
177
+
178
+ function readString(value, ...keys) {
179
+ if (!isObject(value)) return undefined
180
+ for (const key of keys) {
181
+ if (typeof value[key] === 'string' && value[key].length > 0) return value[key]
182
+ }
183
+ return undefined
184
+ }
185
+
186
+ function isObject(value) {
187
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
188
+ }
189
+
190
+ function cloneJson(value) {
191
+ return value === undefined ? undefined : structuredClone(value)
192
+ }
193
+
194
+ export function apply(ctx) {
195
+ installAcpSessionPlan(ctx)
196
+ }