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
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluate a Cordis `code.client` function body on the TUI client tree.
|
|
3
|
+
*
|
|
4
|
+
* Same closure convention as the web runner: the source is an async function
|
|
5
|
+
* body that returns a plugin. Open services include `tuiTheme`, `tuiSlots`,
|
|
6
|
+
* `acpSessionConfig`, `acpSessionPlan`, `acpSessionStats`, and lifecycle-owned `timer`; `host.call` reaches this
|
|
7
|
+
* Package's Host half.
|
|
8
|
+
* No React, browser slots, TTY, or raw Host service names.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const ALLOWED_INJECT = new Set([
|
|
12
|
+
'tuiTheme',
|
|
13
|
+
'tuiSlots',
|
|
14
|
+
'tuiCommands',
|
|
15
|
+
'tuiOverlay',
|
|
16
|
+
'acpSessionConfig',
|
|
17
|
+
'acpSessionPlan',
|
|
18
|
+
'acpSessionStats',
|
|
19
|
+
'timer',
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
const THEME_TEACHING =
|
|
23
|
+
'TUI Client Theme is ctx.tuiTheme, not ctx.theme. '
|
|
24
|
+
+ 'register a complete 18-token palette with tuiTheme.register({ id, label, dark, light }) '
|
|
25
|
+
+ 'inside one Theme Plugin; /theme loads or replaces that whole Plugin. '
|
|
26
|
+
+ 'Token names are closed #RRGGBB keys, not CSS variables.'
|
|
27
|
+
|
|
28
|
+
const SLOT_TEACHING =
|
|
29
|
+
'TUI shell slots are ctx.tuiSlots, not browser ctx.slots. '
|
|
30
|
+
+ 'Inspect tuiSlots.list(), then use tuiSlots.inject(name, () => tuiSlots.register('
|
|
31
|
+
+ '{ name, id }, nodes)).'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} clientCode
|
|
35
|
+
* @param {{ pluginId?: string, tuiTheme?: object, tuiSlots?: object, tuiCommands?: object, tuiOverlay?: object, acpSessionConfig?: object, acpSessionPlan?: object, acpSessionStats?: object, timer?: object, invoke?: Function }} env
|
|
36
|
+
* @returns {Promise<{ waitingFor: string[], dispose: () => void }>}
|
|
37
|
+
*/
|
|
38
|
+
export async function applyClientHalf(clientCode, env) {
|
|
39
|
+
if (typeof clientCode !== 'string' || clientCode.trim().length === 0) {
|
|
40
|
+
throw new Error('TUI Client half is empty')
|
|
41
|
+
}
|
|
42
|
+
let plugin
|
|
43
|
+
try {
|
|
44
|
+
const factory = new Function(
|
|
45
|
+
'React',
|
|
46
|
+
'styles',
|
|
47
|
+
'host',
|
|
48
|
+
'harness',
|
|
49
|
+
'require',
|
|
50
|
+
'process',
|
|
51
|
+
'Buffer',
|
|
52
|
+
'setTimeout',
|
|
53
|
+
'setInterval',
|
|
54
|
+
`return (async () => {\n${clientCode}\n})()`,
|
|
55
|
+
)
|
|
56
|
+
plugin = await factory(
|
|
57
|
+
reactTrap,
|
|
58
|
+
stylesTrap,
|
|
59
|
+
hostFor(env),
|
|
60
|
+
harnessTrap,
|
|
61
|
+
requireTrap,
|
|
62
|
+
processTrap,
|
|
63
|
+
bufferTrap,
|
|
64
|
+
timerGlobalTrap,
|
|
65
|
+
timerGlobalTrap,
|
|
66
|
+
)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error instanceof SyntaxError) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`TUI Client half failed to parse: ${error.message}\n`
|
|
71
|
+
+ 'The Client half is plain JavaScript (no JSX, no TypeScript).',
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
throw error
|
|
75
|
+
}
|
|
76
|
+
const evaluated = normalizePlugin(plugin)
|
|
77
|
+
const inject = evaluated.inject ?? []
|
|
78
|
+
for (const name of inject) {
|
|
79
|
+
if (name === 'theme') {
|
|
80
|
+
throw new Error(THEME_TEACHING)
|
|
81
|
+
}
|
|
82
|
+
if (name === 'slots') {
|
|
83
|
+
throw new Error(SLOT_TEACHING)
|
|
84
|
+
}
|
|
85
|
+
if (!ALLOWED_INJECT.has(name)) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`TUI Client inject "${name}" is not open. Open services: tuiTheme, tuiSlots, tuiCommands, tuiOverlay, acpSessionConfig, acpSessionPlan, acpSessionStats, timer.`,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const waitingFor = inject.filter((name) => env[name] === undefined)
|
|
92
|
+
if (waitingFor.length > 0) return { waitingFor, dispose() {} }
|
|
93
|
+
|
|
94
|
+
const owned = new Set()
|
|
95
|
+
const own = (dispose) => {
|
|
96
|
+
if (typeof dispose !== 'function') return dispose
|
|
97
|
+
let active = true
|
|
98
|
+
const release = () => {
|
|
99
|
+
if (!active) return
|
|
100
|
+
active = false
|
|
101
|
+
owned.delete(release)
|
|
102
|
+
return dispose()
|
|
103
|
+
}
|
|
104
|
+
owned.add(release)
|
|
105
|
+
return release
|
|
106
|
+
}
|
|
107
|
+
const ctx = restrictedCtx(env, own, new Set(inject))
|
|
108
|
+
const result = await evaluated.apply(ctx)
|
|
109
|
+
if (typeof result === 'function' && !owned.has(result)) own(result)
|
|
110
|
+
return {
|
|
111
|
+
waitingFor: [],
|
|
112
|
+
dispose() {
|
|
113
|
+
for (const release of [...owned].reverse()) release()
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizePlugin(value) {
|
|
119
|
+
if (typeof value === 'function') {
|
|
120
|
+
return { inject: [], apply: value }
|
|
121
|
+
}
|
|
122
|
+
if (value !== null && typeof value === 'object' && typeof value.apply === 'function') {
|
|
123
|
+
const inject = Array.isArray(value.inject) ? value.inject.map(String) : []
|
|
124
|
+
return { inject, apply: value.apply.bind(value) }
|
|
125
|
+
}
|
|
126
|
+
throw new Error(
|
|
127
|
+
'TUI Client half must return a plugin object `{ inject, apply }` or an apply function. '
|
|
128
|
+
+ THEME_TEACHING,
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function restrictedCtx(env, own, inject) {
|
|
133
|
+
const sourceTheme = env.tuiTheme
|
|
134
|
+
const sourceSlots = env.tuiSlots
|
|
135
|
+
const sourceCommands = env.tuiCommands
|
|
136
|
+
const sourceOverlay = env.tuiOverlay
|
|
137
|
+
const sourceSessionConfig = env.acpSessionConfig
|
|
138
|
+
const sourceSessionPlan = env.acpSessionPlan
|
|
139
|
+
const sourceSessionStats = env.acpSessionStats
|
|
140
|
+
const sourceTimer = env.timer
|
|
141
|
+
const tuiTheme = sourceTheme === undefined
|
|
142
|
+
? undefined
|
|
143
|
+
: {
|
|
144
|
+
register(palette, options) {
|
|
145
|
+
const register = typeof env.pluginId === 'string'
|
|
146
|
+
&& typeof sourceTheme.registerOwned === 'function'
|
|
147
|
+
? sourceTheme.registerOwned.bind(sourceTheme, env.pluginId)
|
|
148
|
+
: sourceTheme.register.bind(sourceTheme)
|
|
149
|
+
const registration = register(palette, options)
|
|
150
|
+
const dispose = own(
|
|
151
|
+
typeof registration?.dispose === 'function'
|
|
152
|
+
? registration.dispose.bind(registration)
|
|
153
|
+
: registration,
|
|
154
|
+
)
|
|
155
|
+
if (typeof dispose !== 'function') return dispose
|
|
156
|
+
dispose.dispose = dispose
|
|
157
|
+
if (typeof registration?.update === 'function') {
|
|
158
|
+
dispose.update = (patch) => registration.update(patch)
|
|
159
|
+
}
|
|
160
|
+
return dispose
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
const tuiSlots = sourceSlots === undefined
|
|
164
|
+
? undefined
|
|
165
|
+
: {
|
|
166
|
+
inject(name, callback) {
|
|
167
|
+
return own(sourceSlots.inject(name, callback))
|
|
168
|
+
},
|
|
169
|
+
register(options, nodes) {
|
|
170
|
+
const panel = sourceSlots.register(options, nodes)
|
|
171
|
+
const dispose = own(() => panel.dispose())
|
|
172
|
+
return {
|
|
173
|
+
update(nextNodes) {
|
|
174
|
+
return panel.update(nextNodes)
|
|
175
|
+
},
|
|
176
|
+
dispose,
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
list() {
|
|
180
|
+
return sourceSlots.list()
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
const tuiCommands = sourceCommands === undefined
|
|
184
|
+
? undefined
|
|
185
|
+
: {
|
|
186
|
+
register(options, handler) {
|
|
187
|
+
return own(sourceCommands.register(options, handler))
|
|
188
|
+
},
|
|
189
|
+
list() {
|
|
190
|
+
return sourceCommands.list()
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
const tuiOverlay = sourceOverlay === undefined
|
|
194
|
+
? undefined
|
|
195
|
+
: {
|
|
196
|
+
openSlider(options, handlers) {
|
|
197
|
+
const controller = sourceOverlay.openSlider(options, handlers)
|
|
198
|
+
const close = own(() => controller.close())
|
|
199
|
+
return { close }
|
|
200
|
+
},
|
|
201
|
+
openView(options, handlers) {
|
|
202
|
+
const controller = sourceOverlay.openView(options, handlers)
|
|
203
|
+
const close = own(() => controller.close())
|
|
204
|
+
return { close }
|
|
205
|
+
},
|
|
206
|
+
active() {
|
|
207
|
+
return sourceOverlay.active()
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
const acpSessionConfig = sourceSessionConfig === undefined
|
|
211
|
+
? undefined
|
|
212
|
+
: {
|
|
213
|
+
list() {
|
|
214
|
+
return sourceSessionConfig.list()
|
|
215
|
+
},
|
|
216
|
+
current(id) {
|
|
217
|
+
return sourceSessionConfig.current(id)
|
|
218
|
+
},
|
|
219
|
+
byCategory(category) {
|
|
220
|
+
return sourceSessionConfig.byCategory(category)
|
|
221
|
+
},
|
|
222
|
+
transaction(selector) {
|
|
223
|
+
const transaction = sourceSessionConfig.transaction(selector)
|
|
224
|
+
own(() => transaction.rollback())
|
|
225
|
+
return transaction
|
|
226
|
+
},
|
|
227
|
+
set(id, value) {
|
|
228
|
+
return sourceSessionConfig.set(id, value)
|
|
229
|
+
},
|
|
230
|
+
subscribe(listener) {
|
|
231
|
+
return own(sourceSessionConfig.subscribe(listener))
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
const acpSessionPlan = sourceSessionPlan === undefined
|
|
235
|
+
? undefined
|
|
236
|
+
: {
|
|
237
|
+
list() {
|
|
238
|
+
return sourceSessionPlan.list()
|
|
239
|
+
},
|
|
240
|
+
current() {
|
|
241
|
+
return sourceSessionPlan.current()
|
|
242
|
+
},
|
|
243
|
+
subscribe(listener) {
|
|
244
|
+
return own(sourceSessionPlan.subscribe(listener))
|
|
245
|
+
},
|
|
246
|
+
}
|
|
247
|
+
const acpSessionStats = sourceSessionStats === undefined
|
|
248
|
+
? undefined
|
|
249
|
+
: {
|
|
250
|
+
current() {
|
|
251
|
+
return sourceSessionStats.current()
|
|
252
|
+
},
|
|
253
|
+
subscribe(listener) {
|
|
254
|
+
return own(sourceSessionStats.subscribe(listener))
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
const timer = sourceTimer === undefined || !inject.has('timer')
|
|
258
|
+
? undefined
|
|
259
|
+
: {
|
|
260
|
+
interval(callback, delay) {
|
|
261
|
+
return own(sourceTimer.interval(callback, delay))
|
|
262
|
+
},
|
|
263
|
+
timeout(callback, delay) {
|
|
264
|
+
return own(sourceTimer.timeout(callback, delay))
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
tuiTheme,
|
|
269
|
+
tuiSlots,
|
|
270
|
+
tuiCommands,
|
|
271
|
+
tuiOverlay,
|
|
272
|
+
acpSessionConfig,
|
|
273
|
+
acpSessionPlan,
|
|
274
|
+
acpSessionStats,
|
|
275
|
+
timer,
|
|
276
|
+
interval: timer?.interval,
|
|
277
|
+
timeout: timer?.timeout,
|
|
278
|
+
get(name) {
|
|
279
|
+
if (name === 'tuiTheme') return tuiTheme
|
|
280
|
+
if (name === 'tuiSlots') return tuiSlots
|
|
281
|
+
if (name === 'tuiCommands') return tuiCommands
|
|
282
|
+
if (name === 'tuiOverlay') return tuiOverlay
|
|
283
|
+
if (name === 'acpSessionConfig') return acpSessionConfig
|
|
284
|
+
if (name === 'acpSessionPlan') return acpSessionPlan
|
|
285
|
+
if (name === 'acpSessionStats') return acpSessionStats
|
|
286
|
+
if (name === 'timer') return timer
|
|
287
|
+
return undefined
|
|
288
|
+
},
|
|
289
|
+
effect(fn) {
|
|
290
|
+
return typeof fn === 'function' ? own(fn()) : undefined
|
|
291
|
+
},
|
|
292
|
+
on() {
|
|
293
|
+
throw new Error('TUI Client events are not open yet')
|
|
294
|
+
},
|
|
295
|
+
provide() {
|
|
296
|
+
throw new Error('TUI Client halves cannot provide services')
|
|
297
|
+
},
|
|
298
|
+
plugin() {
|
|
299
|
+
throw new Error('mount TUI contributions with tuiTheme/tuiSlots, not ctx.plugin')
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function reactTrap() {
|
|
305
|
+
throw new Error(
|
|
306
|
+
'React is not available on the TUI Client. Use tuiTheme palettes or tuiSlots TuiNode trees.',
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const stylesTrap = {
|
|
311
|
+
insert() {
|
|
312
|
+
throw new Error('styles.insert is a browser Client symbol. TUI palettes use tuiTheme.register.')
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function hostFor(env) {
|
|
317
|
+
return {
|
|
318
|
+
call(method, args = null) {
|
|
319
|
+
if (typeof env.invoke !== 'function') {
|
|
320
|
+
return Promise.reject(new Error('host.call is unavailable without an active TUI Cordis Host run'))
|
|
321
|
+
}
|
|
322
|
+
return Promise.resolve().then(() => env.invoke(method, args))
|
|
323
|
+
},
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function timerGlobalTrap() {
|
|
328
|
+
throw new Error(
|
|
329
|
+
"timer globals are unavailable in dynamic TUI plugins; declare inject: ['timer'] and use ctx.interval/timeout",
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Create lifecycle-neutral timer operations; the dynamic Context owns each returned disposer. */
|
|
334
|
+
export function createClientTimer() {
|
|
335
|
+
return {
|
|
336
|
+
interval(callback, delay) {
|
|
337
|
+
assertTimerArgs('interval', callback, delay)
|
|
338
|
+
const handle = globalThis.setInterval(callback, delay)
|
|
339
|
+
return () => globalThis.clearInterval(handle)
|
|
340
|
+
},
|
|
341
|
+
timeout(callback, delay) {
|
|
342
|
+
assertTimerArgs('timeout', callback, delay)
|
|
343
|
+
const handle = globalThis.setTimeout(callback, delay)
|
|
344
|
+
return () => globalThis.clearTimeout(handle)
|
|
345
|
+
},
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function assertTimerArgs(name, callback, delay) {
|
|
350
|
+
if (typeof callback !== 'function') throw new Error(`ctx.${name}: callback must be a function`)
|
|
351
|
+
if (typeof delay !== 'number' || !Number.isFinite(delay) || delay < 0) {
|
|
352
|
+
throw new Error(`ctx.${name}: delay must be a non-negative finite number`)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const harnessTrap = new Proxy({}, {
|
|
357
|
+
get(_target, prop) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`harness.${String(prop)} belongs to the HOST half (code.host). `
|
|
360
|
+
+ 'Register Package-private methods there with harness.handle; call them here with host.call.',
|
|
361
|
+
)
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
function requireTrap() {
|
|
366
|
+
throw new Error('modules cannot be imported in a dynamic Client half')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const processTrap = new Proxy({}, {
|
|
370
|
+
get() {
|
|
371
|
+
throw new Error('process is not available in a TUI Client half')
|
|
372
|
+
},
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
const bufferTrap = new Proxy({}, {
|
|
376
|
+
get() {
|
|
377
|
+
throw new Error('Buffer is not available in a TUI Client half')
|
|
378
|
+
},
|
|
379
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** ACP extension contract for DSH Cordis client capabilities. */
|
|
2
|
+
|
|
3
|
+
export const CORDIS_PROTOCOL = 0
|
|
4
|
+
|
|
5
|
+
export const CORDIS_CAPABILITY = Object.freeze({ protocol: CORDIS_PROTOCOL })
|
|
6
|
+
|
|
7
|
+
export const CORDIS_METHODS = Object.freeze({
|
|
8
|
+
inspectSync: '_dsh/cordis/inspect/sync',
|
|
9
|
+
inspectResolve: '_dsh/cordis/inspect/resolve',
|
|
10
|
+
inspectQuery: '_dsh/cordis/inspect/query',
|
|
11
|
+
inspectQueryResolved: '_dsh/cordis/inspect/query-resolved',
|
|
12
|
+
runHost: '_dsh/cordis/run/host',
|
|
13
|
+
getClientCode: '_dsh/cordis/run/client-code',
|
|
14
|
+
resolveRequestRun: '_dsh/cordis/run/resolve',
|
|
15
|
+
requestRun: '_dsh/cordis/run/request',
|
|
16
|
+
requestRunResolved: '_dsh/cordis/run/request-resolved',
|
|
17
|
+
userRun: '_dsh/cordis/run/user',
|
|
18
|
+
settleUserRun: '_dsh/cordis/run/settle',
|
|
19
|
+
pluginInvoke: '_dsh/cordis/plugin/invoke',
|
|
20
|
+
pluginsList: '_dsh/cordis/plugins/list',
|
|
21
|
+
pluginStart: '_dsh/cordis/plugins/start',
|
|
22
|
+
pluginStop: '_dsh/cordis/plugins/stop',
|
|
23
|
+
pluginRetract: '_dsh/cordis/plugins/retract',
|
|
24
|
+
themeUpdate: '_dsh/cordis/tui/theme/update',
|
|
25
|
+
themeRemove: '_dsh/cordis/tui/theme/remove',
|
|
26
|
+
themeSelected: '_dsh/cordis/tui/theme/selected',
|
|
27
|
+
slotsUpdate: '_dsh/cordis/tui/slots/update',
|
|
28
|
+
commandsUpdate: '_dsh/cordis/tui/commands/update',
|
|
29
|
+
commandInvoke: '_dsh/cordis/tui/commands/invoke',
|
|
30
|
+
overlayUpdate: '_dsh/cordis/tui/overlay/update',
|
|
31
|
+
overlayEvent: '_dsh/cordis/tui/overlay/event',
|
|
32
|
+
sessionConfigSet: '_dsh/cordis/tui/session-config/set',
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read the negotiated DSH Cordis capability from an ACP initialize result.
|
|
37
|
+
* @param {unknown} result
|
|
38
|
+
* @returns {{ protocol: number } | null}
|
|
39
|
+
*/
|
|
40
|
+
export function readCordisCapability(result) {
|
|
41
|
+
if (result === null || typeof result !== 'object' || Array.isArray(result)) return null
|
|
42
|
+
const capabilities = result.agentCapabilities
|
|
43
|
+
if (capabilities === null || typeof capabilities !== 'object' || Array.isArray(capabilities)) return null
|
|
44
|
+
const meta = capabilities._meta
|
|
45
|
+
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) return null
|
|
46
|
+
const dsh = meta.dsh
|
|
47
|
+
if (dsh === null || typeof dsh !== 'object' || Array.isArray(dsh)) return null
|
|
48
|
+
const cordis = dsh.cordis
|
|
49
|
+
if (cordis === null || typeof cordis !== 'object' || Array.isArray(cordis)) return null
|
|
50
|
+
return cordis.protocol === CORDIS_PROTOCOL ? { protocol: CORDIS_PROTOCOL } : null
|
|
51
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add TUI plugin authoring guidance to the shipped Creator preset without
|
|
3
|
+
* copying or modifying that preset's composition.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
|
|
8
|
+
export const name = 'tui-creator-overlay'
|
|
9
|
+
export const inject = ['agentPresets', 'skills', 'systemPrompt', 'loader']
|
|
10
|
+
|
|
11
|
+
const SKILL_NAME = 'tui-plugin-development'
|
|
12
|
+
const ROUTING_PROMPT = `# Dynamic TUI Plugin routing
|
|
13
|
+
|
|
14
|
+
Load cordis-plugin-development for every dynamic Plugin; it owns the common Plugin, Package, lifecycle, approval, Host, RPC, and repair model.
|
|
15
|
+
|
|
16
|
+
- When any requested behavior belongs to the TUI — terminal themes or backgrounds, native TUI slots, local TUI commands, native overlays, or current ACP Session options — also load tui-plugin-development. Its TUI Provider guidance overrides generic browser UI assumptions only for that half.
|
|
17
|
+
- Browser/Web-only work needs no TUI companion. A mixed Plugin uses the generic skill for common, Host, and Web behavior and the TUI skill for its terminal behavior. Do not translate a TUI request into Web Slots or add Host code unless the requested behavior owns Host data.`
|
|
18
|
+
|
|
19
|
+
const skillDocument = readFileSync(
|
|
20
|
+
new URL('../skills/tui-plugin-development/SKILL.md', import.meta.url),
|
|
21
|
+
'utf8',
|
|
22
|
+
)
|
|
23
|
+
const skillFrontmatter = skillDocument.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/)
|
|
24
|
+
if (!skillFrontmatter) {
|
|
25
|
+
throw new Error('tui-creator-overlay: TUI skill frontmatter is unavailable')
|
|
26
|
+
}
|
|
27
|
+
const skillDescription = skillFrontmatter[1]
|
|
28
|
+
.match(/^description:\s*(.+)$/m)?.[1]
|
|
29
|
+
?.trim()
|
|
30
|
+
if (!skillDescription) {
|
|
31
|
+
throw new Error('tui-creator-overlay: TUI skill description is unavailable')
|
|
32
|
+
}
|
|
33
|
+
const skillContent = skillDocument.slice(skillFrontmatter[0].length)
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {object} ctx
|
|
37
|
+
* @param {{ preset?: string }} [config]
|
|
38
|
+
*/
|
|
39
|
+
export async function apply(ctx, config = {}) {
|
|
40
|
+
const preset = config.preset ?? 'cordis'
|
|
41
|
+
if (typeof preset !== 'string' || preset.length === 0) {
|
|
42
|
+
throw new Error('tui-creator-overlay: preset must be a non-empty string')
|
|
43
|
+
}
|
|
44
|
+
const key = await ctx.agentPresets.standingKeyFor(preset)
|
|
45
|
+
const scopeModule = await ctx.loader.import('@deepseek-ai/dsh-scope')
|
|
46
|
+
if (typeof scopeModule?.createScope !== 'function') {
|
|
47
|
+
throw new Error('tui-creator-overlay: profile dsh-scope module is unavailable')
|
|
48
|
+
}
|
|
49
|
+
const { createScope } = scopeModule
|
|
50
|
+
const overlay = createScope(ctx, key)
|
|
51
|
+
try {
|
|
52
|
+
const skills = overlay.ctx.get('skills')
|
|
53
|
+
if (skills === undefined || typeof skills.register !== 'function') {
|
|
54
|
+
throw new Error('tui-creator-overlay: skills service is unavailable in the preset scope')
|
|
55
|
+
}
|
|
56
|
+
skills.register({
|
|
57
|
+
name: SKILL_NAME,
|
|
58
|
+
description: skillDescription,
|
|
59
|
+
source: 'martty/creator-overlay',
|
|
60
|
+
content: skillContent,
|
|
61
|
+
invocation: { modelInvocable: true, userInvocable: true },
|
|
62
|
+
})
|
|
63
|
+
const systemPrompt = overlay.ctx.get('systemPrompt')
|
|
64
|
+
if (systemPrompt === undefined || typeof systemPrompt.section !== 'function') {
|
|
65
|
+
throw new Error('tui-creator-overlay: systemPrompt service is unavailable in the preset scope')
|
|
66
|
+
}
|
|
67
|
+
systemPrompt.section({
|
|
68
|
+
name: 'tool:tui-cordis-routing',
|
|
69
|
+
order: 116,
|
|
70
|
+
text: ROUTING_PROMPT,
|
|
71
|
+
})
|
|
72
|
+
ctx.effect(() => () => overlay.dispose(), `tui-creator-overlay(${JSON.stringify(preset)})`)
|
|
73
|
+
} catch (error) {
|
|
74
|
+
await overlay.dispose()
|
|
75
|
+
throw error
|
|
76
|
+
}
|
|
77
|
+
}
|
package/lib/demo-skin.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--demo-skin` entry: mount gallery pack `ember` through `tuiTheme.register`,
|
|
3
|
+
* spawn the native TUI in `--demo` attach mode, and flush the Cordis TUI
|
|
4
|
+
* theme update with
|
|
5
|
+
* `activate: true`. Runnable as `node npm/lib/demo-skin.js`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from 'node:fs'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
import * as emberPlugin from './ember.js'
|
|
12
|
+
import { JsonRpcLineTransport } from './jsonrpc-line-transport.js'
|
|
13
|
+
import { nativeBinary, spawnPluginTui } from './spawn-tui.js'
|
|
14
|
+
import * as themePlugin from './tui-theme.js'
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
17
|
+
const { version } = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'))
|
|
18
|
+
|
|
19
|
+
function extraBinaryArgs(argv) {
|
|
20
|
+
const extra = ['--demo']
|
|
21
|
+
for (const arg of argv) {
|
|
22
|
+
if (arg === '--demo-skin' || arg === '--demo') continue
|
|
23
|
+
extra.push(arg)
|
|
24
|
+
}
|
|
25
|
+
return extra
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Register ember, activate it, spawn the TUI, and serve the attach handshake.
|
|
30
|
+
* @param {string[]} [argv]
|
|
31
|
+
*/
|
|
32
|
+
export async function runDemoSkin(argv = process.argv.slice(2)) {
|
|
33
|
+
const { Context } = await import('@deepseek-ai/cordis')
|
|
34
|
+
const ctx = new Context()
|
|
35
|
+
await ctx.plugin(themePlugin)
|
|
36
|
+
await ctx.plugin(emberPlugin)
|
|
37
|
+
const theme = ctx.tuiTheme
|
|
38
|
+
ctx.tuiTheme.activate('ember')
|
|
39
|
+
|
|
40
|
+
const bin = process.env.DSH_TUI_BIN || nativeBinary()
|
|
41
|
+
const connection = await spawnPluginTui(bin, extraBinaryArgs(argv))
|
|
42
|
+
const { child } = connection
|
|
43
|
+
const transport = new JsonRpcLineTransport(connection.input, connection.output)
|
|
44
|
+
|
|
45
|
+
function notify(method, params) {
|
|
46
|
+
transport.notify(method, params)
|
|
47
|
+
}
|
|
48
|
+
theme.bindNotify(notify)
|
|
49
|
+
|
|
50
|
+
transport.onRequest(async (method) => {
|
|
51
|
+
switch (method) {
|
|
52
|
+
case 'initialize':
|
|
53
|
+
return { serverInfo: { name: 'dsh-tui-demo-skin', version } }
|
|
54
|
+
case 'session/prompt':
|
|
55
|
+
return { messageId: 'demo-skin' }
|
|
56
|
+
case 'shutdown':
|
|
57
|
+
return {}
|
|
58
|
+
default:
|
|
59
|
+
throw new Error(`unknown method: ${method}`)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
transport.start()
|
|
64
|
+
connection.resume?.()
|
|
65
|
+
|
|
66
|
+
child.on('exit', (code) => {
|
|
67
|
+
process.exit(code === null ? 0 : code)
|
|
68
|
+
})
|
|
69
|
+
child.on('error', (err) => {
|
|
70
|
+
console.error(`dsh-tui demo-skin failed: ${err.message}`)
|
|
71
|
+
process.exit(1)
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const isMain = process.argv[1] !== undefined
|
|
76
|
+
&& path.resolve(fileURLToPath(import.meta.url)) === path.resolve(process.argv[1])
|
|
77
|
+
if (isMain) {
|
|
78
|
+
await runDemoSkin(process.argv.slice(2))
|
|
79
|
+
}
|
package/lib/ember.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gallery palette pack `ember`. Registers complete dark/light token maps.
|
|
3
|
+
* Does not activate: `--demo-skin` calls `tuiTheme.activate('ember')`.
|
|
4
|
+
* `inject = ['tuiTheme']`: sibling profile row, not `ctx.plugin` inside the runner.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from 'node:fs'
|
|
8
|
+
|
|
9
|
+
const emberPalette = JSON.parse(
|
|
10
|
+
readFileSync(new URL('./palettes/ember.json', import.meta.url), 'utf8'),
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
export const name = 'tui-theme-ember'
|
|
14
|
+
export const inject = ['tuiTheme']
|
|
15
|
+
|
|
16
|
+
export function apply(ctx) {
|
|
17
|
+
ctx.effect(() => ctx.tuiTheme.register(emberPalette, { activate: false }))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { emberPalette }
|