dsh-creator-mode-plus 0.3.8
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/AGENTS.md +40 -0
- package/CHANGELOG.md +104 -0
- package/LICENSE +21 -0
- package/README.en.md +83 -0
- package/README.md +82 -0
- package/docs/bridge-contract.md +431 -0
- package/docs/dshx-v0.7-alignment.md +108 -0
- package/docs/screenshots/already-installed.png +0 -0
- package/docs/screenshots/install.png +0 -0
- package/docs/screenshots/mode-picker.gif +0 -0
- package/docs/screenshots/mode-picker.png +0 -0
- package/docs/screenshots/mode-selected.png +0 -0
- package/dshx.yml +16 -0
- package/package.json +67 -0
- package/preset/preset.yml +2 -0
- package/preset/skills/creator-mode-plus/SKILL.md +101 -0
- package/preset/skills/creator-mode-plus/existing-plugin-trials.md +60 -0
- package/preset/skills/creator-mode-plus/maintenance.md +48 -0
- package/preset/skills/creator-mode-plus/verification-recovery.md +54 -0
- package/scripts/install.mjs +292 -0
- package/scripts/verify-dshx.mjs +77 -0
- package/scripts/verify-harness-install.mjs +101 -0
- package/src/auth.js +26 -0
- package/src/client.js +112 -0
- package/src/compatibility.js +261 -0
- package/src/delivery.js +331 -0
- package/src/index.js +377 -0
- package/src/preset-015.js +28 -0
- package/src/runner.js +464 -0
- package/src/safety.js +79 -0
- package/src/takeover.js +157 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { installTakeoverFence, requestTakeover } from './takeover.js'
|
|
2
|
+
/** Creator Mode+ model tools backed by fixed dshx operations. */
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
currentWebPort,
|
|
6
|
+
installCreatorRecovery,
|
|
7
|
+
runClaimedDshx,
|
|
8
|
+
runClientFailureDshx,
|
|
9
|
+
runDshx,
|
|
10
|
+
} from './runner.js'
|
|
11
|
+
import { healCreatorPlusPresets } from './preset-015.js'
|
|
12
|
+
import {
|
|
13
|
+
forgetCreatorClaim,
|
|
14
|
+
installCreatorSafetyGuard,
|
|
15
|
+
rememberCreatorClaim,
|
|
16
|
+
} from './safety.js'
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
CREATOR_BRIDGE_VERSION,
|
|
20
|
+
CREATOR_MODEL_TOOLS,
|
|
21
|
+
DSHX_CONTRACT,
|
|
22
|
+
} from './compatibility.js'
|
|
23
|
+
|
|
24
|
+
export const name = 'dsh-creator-mode-plus'
|
|
25
|
+
export const inject = ['tools', 'webServer', 'connection', 'agents', 'sessions', 'userQuestions']
|
|
26
|
+
|
|
27
|
+
const CLIENT_FAILURE_PATH = '/dsh-creator-mode-plus/client-failure'
|
|
28
|
+
const MAX_CLIENT_FAILURE_BYTES = 16 * 1024
|
|
29
|
+
const CLIENT_FAILURE_ROUTE_REGISTRY = Symbol.for('dsh-creator-mode-plus.client-failure-routes.v1')
|
|
30
|
+
|
|
31
|
+
const PLUGIN_ID = /^[a-z][a-z0-9-]*$/
|
|
32
|
+
const KINDS = new Set(['function', 'tool', 'client', 'object', 'class'])
|
|
33
|
+
const CHANGES = new Set(['patch', 'manifest', 'preset', 'client', 'new-client', 'server', 'artifact'])
|
|
34
|
+
const HOT_RELOAD_INFRASTRUCTURE = new Set(['dsh-creator-mode-plus', 'dsh-external-plugin-devkit'])
|
|
35
|
+
|
|
36
|
+
function pluginId(value) {
|
|
37
|
+
if (!PLUGIN_ID.test(value)) throw new Error('plugin name must be lower-case kebab-case')
|
|
38
|
+
return value
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function choice(value, allowed, label) {
|
|
42
|
+
if (!allowed.has(value)) throw new Error(`${label} is not supported: ${value}`)
|
|
43
|
+
return value
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const output = {
|
|
47
|
+
schema: { type: 'object', additionalProperties: true },
|
|
48
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isSameOrigin(req) {
|
|
52
|
+
const origin = req.headers.origin
|
|
53
|
+
const authority = req.headers.host
|
|
54
|
+
if (typeof origin !== 'string' || typeof authority !== 'string') return false
|
|
55
|
+
try {
|
|
56
|
+
const parsed = new URL(origin)
|
|
57
|
+
return (parsed.protocol === 'http:' || parsed.protocol === 'https:') && parsed.host === authority
|
|
58
|
+
} catch {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readBoundedJson(req) {
|
|
64
|
+
const chunks = []
|
|
65
|
+
let bytes = 0
|
|
66
|
+
for await (const chunk of req) {
|
|
67
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
68
|
+
bytes += buffer.byteLength
|
|
69
|
+
if (bytes > MAX_CLIENT_FAILURE_BYTES) {
|
|
70
|
+
const error = new Error('client failure report is too large')
|
|
71
|
+
error.statusCode = 413
|
|
72
|
+
throw error
|
|
73
|
+
}
|
|
74
|
+
chunks.push(buffer)
|
|
75
|
+
}
|
|
76
|
+
const value = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
77
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('invalid client failure report')
|
|
78
|
+
return value
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function clientFailureRouteRegistry() {
|
|
82
|
+
if (globalThis[CLIENT_FAILURE_ROUTE_REGISTRY] === undefined) {
|
|
83
|
+
Object.defineProperty(globalThis, CLIENT_FAILURE_ROUTE_REGISTRY, {
|
|
84
|
+
value: new WeakMap(),
|
|
85
|
+
enumerable: false,
|
|
86
|
+
configurable: false,
|
|
87
|
+
writable: false,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
return globalThis[CLIENT_FAILURE_ROUTE_REGISTRY]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function clientFailureHandler(webServer, options) {
|
|
94
|
+
return async (req, res) => {
|
|
95
|
+
if (req.method !== 'POST') {
|
|
96
|
+
res.writeHead(405, { allow: 'POST' })
|
|
97
|
+
res.end()
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
if (!isSameOrigin(req)) {
|
|
101
|
+
res.writeHead(403)
|
|
102
|
+
res.end('forbidden')
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const input = await readBoundedJson(req)
|
|
107
|
+
const result = await (options.runClientFailureDshx ?? runClientFailureDshx)({
|
|
108
|
+
failedIds: input.failedIds,
|
|
109
|
+
message: input.message,
|
|
110
|
+
hostPid: process.pid,
|
|
111
|
+
hostParentPid: process.ppid,
|
|
112
|
+
hostPort: webServer.port,
|
|
113
|
+
}, options)
|
|
114
|
+
if (result.exitCode !== 0) {
|
|
115
|
+
res.writeHead(503, { 'content-type': 'application/json' })
|
|
116
|
+
res.end(JSON.stringify({ reload: false, error: result.stderr || result.stdout || 'recovery failed' }))
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
const decoded = JSON.parse(result.stdout || '{}')
|
|
120
|
+
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' })
|
|
121
|
+
res.end(JSON.stringify({
|
|
122
|
+
reload: decoded?.data?.reload === true,
|
|
123
|
+
incident: decoded?.data?.incident,
|
|
124
|
+
}))
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const status = Number.isInteger(error?.statusCode) ? error.statusCode : 400
|
|
127
|
+
res.writeHead(status, { 'content-type': 'application/json' })
|
|
128
|
+
res.end(JSON.stringify({ reload: false, error: error instanceof Error ? error.message : String(error) }))
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function acquireClientFailureRoute(hostKey, webServer, handler) {
|
|
134
|
+
const registry = clientFailureRouteRegistry()
|
|
135
|
+
let broker = registry.get(hostKey)
|
|
136
|
+
const lease = { handler }
|
|
137
|
+
if (broker === undefined) {
|
|
138
|
+
broker = { port: webServer.port, leases: new Set([lease]), current: lease, dispose: undefined }
|
|
139
|
+
broker.dispose = webServer.register({
|
|
140
|
+
kind: 'exact',
|
|
141
|
+
path: CLIENT_FAILURE_PATH,
|
|
142
|
+
handler: (req, res) => broker.current.handler(req, res),
|
|
143
|
+
})
|
|
144
|
+
registry.set(hostKey, broker)
|
|
145
|
+
} else {
|
|
146
|
+
if (broker.port !== webServer.port) throw new Error('Creator+ Web Host changed while route leases remain active')
|
|
147
|
+
broker.leases.add(lease)
|
|
148
|
+
broker.current = lease
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return () => {
|
|
152
|
+
if (!broker.leases.delete(lease)) return
|
|
153
|
+
if (broker.leases.size === 0) {
|
|
154
|
+
broker.dispose()
|
|
155
|
+
if (registry.get(hostKey) === broker) registry.delete(hostKey)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
if (broker.current !== lease) return
|
|
159
|
+
for (const remaining of broker.leases) broker.current = remaining
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Register one generation-safe same-origin browser sentry route per Web Host. */
|
|
164
|
+
export function installClientFailureRoute(ctx, options = {}) {
|
|
165
|
+
const webServer = ctx.webServer
|
|
166
|
+
// Cordis returns scope-bound service proxies; their object identity is not a
|
|
167
|
+
// Host identity. The public root Context is shared by all preset generations.
|
|
168
|
+
// Raw-service fixtures without a Context retain their original identity.
|
|
169
|
+
const hostKey = ctx.root ?? webServer
|
|
170
|
+
ctx.effect(
|
|
171
|
+
() => acquireClientFailureRoute(hostKey, webServer, clientFailureHandler(webServer, options)),
|
|
172
|
+
'Creator Mode+ browser failure route',
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Register file-backed Creator Mode+ operations for one preset scope. */
|
|
177
|
+
export function apply(ctx) {
|
|
178
|
+
console.log('[dsh-creator-mode-plus] loaded')
|
|
179
|
+
try {
|
|
180
|
+
const healed = healCreatorPlusPresets()
|
|
181
|
+
if (healed.length > 0) {
|
|
182
|
+
ctx.logger?.info?.(`dsh-creator-mode-plus: migrated 0.1.5 persona in ${healed.join(', ')}`)
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
ctx.logger?.warn?.(`dsh-creator-mode-plus: persona heal failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
186
|
+
}
|
|
187
|
+
const authOptions = { getWebStartupUrl: port => typeof ctx.connection.authenticatedUrl === 'function' ? ctx.connection.authenticatedUrl(`http://127.0.0.1:${port}/`) : undefined }
|
|
188
|
+
installCreatorRecovery(ctx, authOptions)
|
|
189
|
+
installClientFailureRoute(ctx, authOptions)
|
|
190
|
+
installCreatorSafetyGuard(ctx)
|
|
191
|
+
installTakeoverFence(ctx)
|
|
192
|
+
|
|
193
|
+
ctx.tools.register({
|
|
194
|
+
name: 'dshx_claim_plugin',
|
|
195
|
+
description: 'Claim one plugin for this Creator+ session and arm the external Guardian before editing. Different sessions may claim different plugins concurrently.',
|
|
196
|
+
parameters: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: { name: { type: 'string', description: 'Plugin id this session will own' } },
|
|
199
|
+
required: ['name'],
|
|
200
|
+
additionalProperties: false,
|
|
201
|
+
},
|
|
202
|
+
timeoutMs: 30_000,
|
|
203
|
+
output,
|
|
204
|
+
execute(args, exec) {
|
|
205
|
+
const id = pluginId(args.name)
|
|
206
|
+
return runDshx(['creator', 'claim', id], exec, { ...authOptions, hostPort: currentWebPort() }).then((result) => {
|
|
207
|
+
if (result.exitCode === 0) rememberCreatorClaim(exec, id)
|
|
208
|
+
return result
|
|
209
|
+
})
|
|
210
|
+
},
|
|
211
|
+
presentCall: args => ({ card: 'generic', title: `dshx claim ${args.name}`, kind: 'edit', rawInput: args.name }),
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
ctx.tools.register({
|
|
215
|
+
name: 'dshx_request_takeover',
|
|
216
|
+
description: 'Ask the user in this conversation to take over a claimed plugin. Shows the actual owner, stops old work after confirmation, and transfers ownership. Never infer approval from chat text or automation.',
|
|
217
|
+
parameters: { type: 'object', properties: { name: { type: 'string', pattern: '^[a-z][a-z0-9-]*$' } }, required: ['name'], additionalProperties: false },
|
|
218
|
+
timeoutMs: 360_000,
|
|
219
|
+
output,
|
|
220
|
+
execute: (args, exec) => requestTakeover(ctx, pluginId(args.name), exec, { ...authOptions, hostPort: currentWebPort() }),
|
|
221
|
+
presentCall: args => ({ card: 'generic', title: `申请接管 ${args.name}`, kind: 'edit', rawInput: args.name }),
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
ctx.tools.register({
|
|
225
|
+
name: 'dshx_scaffold',
|
|
226
|
+
description: 'Create one plugin inside the calling session workspace and mount it into Harness automatically when needed. It never overwrites an existing project.',
|
|
227
|
+
parameters: {
|
|
228
|
+
type: 'object',
|
|
229
|
+
properties: {
|
|
230
|
+
name: { type: 'string', description: 'Lower-case kebab-case plugin id' },
|
|
231
|
+
kind: { type: 'string', description: 'function, tool, client, object, or class' },
|
|
232
|
+
},
|
|
233
|
+
required: ['name', 'kind'],
|
|
234
|
+
additionalProperties: false,
|
|
235
|
+
},
|
|
236
|
+
timeoutMs: 60_000,
|
|
237
|
+
output,
|
|
238
|
+
execute(args, exec) {
|
|
239
|
+
const id = pluginId(args.name)
|
|
240
|
+
return runClaimedDshx(id, [
|
|
241
|
+
'creator', 'scaffold', id, choice(args.kind, KINDS, 'plugin kind'),
|
|
242
|
+
], exec, { ...authOptions, hostPort: currentWebPort() })
|
|
243
|
+
},
|
|
244
|
+
presentCall: args => ({ card: 'generic', title: `dshx scaffold ${args.name}`, kind: 'edit', rawInput: args }),
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
ctx.tools.register({
|
|
248
|
+
name: 'dshx_check',
|
|
249
|
+
description: 'Run DSHX v0.7 external-plugin checks, including client Cordis service injection and the built-client handoff. Passing proves SOURCE_BUILT only, not live activation.',
|
|
250
|
+
parameters: {
|
|
251
|
+
type: 'object',
|
|
252
|
+
properties: { name: { type: 'string', description: 'Plugin id under my-plugins' } },
|
|
253
|
+
required: ['name'],
|
|
254
|
+
additionalProperties: false,
|
|
255
|
+
},
|
|
256
|
+
timeoutMs: 60_000,
|
|
257
|
+
output,
|
|
258
|
+
execute(args, exec) {
|
|
259
|
+
const id = pluginId(args.name)
|
|
260
|
+
return runClaimedDshx(id, ['check', id], exec, { ...authOptions, hostPort: currentWebPort() })
|
|
261
|
+
},
|
|
262
|
+
presentCall: args => ({ card: 'generic', title: `dshx check ${args.name}`, kind: 'read', rawInput: args.name }),
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
ctx.tools.register({
|
|
266
|
+
name: 'dshx_activation_plan',
|
|
267
|
+
description: 'Select the activation method for one changed component: patch, manifest, preset, client, new-client, server, or artifact.',
|
|
268
|
+
parameters: {
|
|
269
|
+
type: 'object',
|
|
270
|
+
properties: {
|
|
271
|
+
name: { type: 'string', description: 'Plugin id under my-plugins' },
|
|
272
|
+
change: { type: 'string', description: 'patch, manifest, preset, client, new-client, server, or artifact' },
|
|
273
|
+
},
|
|
274
|
+
required: ['name', 'change'],
|
|
275
|
+
additionalProperties: false,
|
|
276
|
+
},
|
|
277
|
+
timeoutMs: 60_000,
|
|
278
|
+
output,
|
|
279
|
+
execute(args, exec) {
|
|
280
|
+
const id = pluginId(args.name)
|
|
281
|
+
return runClaimedDshx(id, [
|
|
282
|
+
'activation-plan', id, '--change', choice(args.change, CHANGES, 'change surface'),
|
|
283
|
+
], exec, { ...authOptions, hostPort: currentWebPort() })
|
|
284
|
+
},
|
|
285
|
+
presentCall: args => ({ card: 'generic', title: `dshx plan ${args.change}`, kind: 'read', rawInput: args }),
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
ctx.tools.register({
|
|
289
|
+
name: 'dshx_activate_new_client',
|
|
290
|
+
description: 'Activate one checked my-plugins Web client in the DSHX v0.7 safe order: profile link, resolution proof, watched-patch transaction, then current-Host manifest proof.',
|
|
291
|
+
parameters: {
|
|
292
|
+
type: 'object',
|
|
293
|
+
properties: { name: { type: 'string', description: 'Plugin id under my-plugins' } },
|
|
294
|
+
required: ['name'],
|
|
295
|
+
additionalProperties: false,
|
|
296
|
+
},
|
|
297
|
+
timeoutMs: 90_000,
|
|
298
|
+
output,
|
|
299
|
+
execute(args, exec) {
|
|
300
|
+
const id = pluginId(args.name)
|
|
301
|
+
const port = currentWebPort()
|
|
302
|
+
return runClaimedDshx(id, [
|
|
303
|
+
'activate-new-client', id, '--profile', 'web', '--port', String(port),
|
|
304
|
+
], exec, { ...authOptions, hostPort: port })
|
|
305
|
+
},
|
|
306
|
+
presentCall: args => ({ card: 'generic', title: `dshx activate ${args.name}`, kind: 'edit', rawInput: args.name }),
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
ctx.tools.register({
|
|
310
|
+
name: 'dshx_remove_plugin',
|
|
311
|
+
description: 'Safely deactivate and unregister one claimed plugin. It proves the current Host no longer contains the plugin before removing the Web-profile dependency, detaches only symlinks, preserves source, and never restarts DSH.',
|
|
312
|
+
parameters: {
|
|
313
|
+
type: 'object',
|
|
314
|
+
properties: { name: { type: 'string', description: 'Claimed plugin id to deactivate and unregister' } },
|
|
315
|
+
required: ['name'],
|
|
316
|
+
additionalProperties: false,
|
|
317
|
+
},
|
|
318
|
+
timeoutMs: 90_000,
|
|
319
|
+
output,
|
|
320
|
+
async execute(args, exec) {
|
|
321
|
+
const id = pluginId(args.name)
|
|
322
|
+
const port = currentWebPort()
|
|
323
|
+
const result = await runClaimedDshx(id, ['creator', 'remove', id], exec, { ...authOptions, hostPort: port })
|
|
324
|
+
if (result.exitCode === 0) forgetCreatorClaim(exec)
|
|
325
|
+
return result
|
|
326
|
+
},
|
|
327
|
+
presentCall: args => ({ card: 'generic', title: `dshx remove ${args.name}`, kind: 'edit', rawInput: args.name }),
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
ctx.tools.register({
|
|
331
|
+
name: 'dshx_hot_reload',
|
|
332
|
+
description: 'Activate the code changes of one already-loaded, checked server plugin. The requested behavior remains RUNTIME_VERIFICATION_REQUIRED until exercised. Use the fixed plugin ID; runtime provenance and cleanup are checked internally.',
|
|
333
|
+
parameters: {
|
|
334
|
+
type: 'object',
|
|
335
|
+
properties: { name: { type: 'string', description: 'Existing checked plugin id under my-plugins' } },
|
|
336
|
+
required: ['name'],
|
|
337
|
+
additionalProperties: false,
|
|
338
|
+
},
|
|
339
|
+
timeoutMs: 90_000,
|
|
340
|
+
output,
|
|
341
|
+
execute(args, exec) {
|
|
342
|
+
const id = pluginId(args.name)
|
|
343
|
+
if (HOT_RELOAD_INFRASTRUCTURE.has(id)) {
|
|
344
|
+
throw new Error(`dshx_hot_reload cannot replace its executing infrastructure plugin: ${id}`)
|
|
345
|
+
}
|
|
346
|
+
const port = currentWebPort()
|
|
347
|
+
return runClaimedDshx(id, [
|
|
348
|
+
'hot-reload', id, '--profile', 'web', '--port', String(port), '--json',
|
|
349
|
+
], exec, { ...authOptions, hostPort: port })
|
|
350
|
+
},
|
|
351
|
+
presentCall: args => ({ card: 'generic', title: `dshx hot reload ${args.name}`, kind: 'edit', rawInput: args.name }),
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
ctx.tools.register({
|
|
355
|
+
name: 'dshx_browser_open',
|
|
356
|
+
description: "Open the current authenticated DSH WebUI through this session's externally approved browser adapter. No URL, path, credential or command input. BROWSER_AUTHENTICATED proves browser access only; continue feature verification without a Host restart.",
|
|
357
|
+
parameters: { type: 'object', properties: {}, additionalProperties: false },
|
|
358
|
+
timeoutMs: 60_000,
|
|
359
|
+
output,
|
|
360
|
+
execute(_args, exec) {
|
|
361
|
+
return runDshx(['browser', 'open', '--json'], exec, { ...authOptions, hostPort: currentWebPort() })
|
|
362
|
+
},
|
|
363
|
+
presentCall: () => ({ card: 'generic', title: 'Open authenticated DSH browser', kind: 'edit', rawInput: {} }),
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
ctx.tools.register({
|
|
367
|
+
name: 'dshx_status',
|
|
368
|
+
description: 'Read the external DSHX v0.7 supervisor and Web Host status. The bridge also reports its pinned contract and capabilities; it never starts, stops, restarts, or updates DSH.',
|
|
369
|
+
parameters: { type: 'object', properties: {}, additionalProperties: false },
|
|
370
|
+
timeoutMs: 30_000,
|
|
371
|
+
output,
|
|
372
|
+
execute(_args, exec) {
|
|
373
|
+
return runDshx(['status'], exec, { ...authOptions, hostPort: currentWebPort() })
|
|
374
|
+
},
|
|
375
|
+
presentCall: () => ({ card: 'generic', title: 'dshx status', kind: 'read' }),
|
|
376
|
+
})
|
|
377
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const PRESET_IDS = ['creator-plus', 'creator-mode-plus']
|
|
6
|
+
|
|
7
|
+
/** Rewrite a leftover 0.1.2 `text` persona field to the 0.1.5 `prefix` field. */
|
|
8
|
+
export function migratePersona015(text) {
|
|
9
|
+
return text.replace(/(^|\n)( )text: ([|>]-?)/g, '$1$2prefix: $3')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Heal already-installed Creator+ compositions in DSH_HOME.
|
|
14
|
+
* The Host plugin can run this even when a stale preset fails to mount.
|
|
15
|
+
*/
|
|
16
|
+
export function healCreatorPlusPresets(home = process.env.DSH_HOME || join(homedir(), '.dsh')) {
|
|
17
|
+
const healed = []
|
|
18
|
+
for (const id of PRESET_IDS) {
|
|
19
|
+
const path = join(home, '.agent-presets', id, 'agent.cordis.yml')
|
|
20
|
+
if (!existsSync(path)) continue
|
|
21
|
+
const before = readFileSync(path, 'utf8')
|
|
22
|
+
const after = migratePersona015(before)
|
|
23
|
+
if (after === before) continue
|
|
24
|
+
writeFileSync(path, after)
|
|
25
|
+
healed.push(path)
|
|
26
|
+
}
|
|
27
|
+
return healed
|
|
28
|
+
}
|