typeclaw 0.1.1 → 0.1.2
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/README.md +12 -12
- package/package.json +1 -1
- package/src/agent/doctor.ts +173 -0
- package/src/agent/subagents.ts +24 -2
- package/src/bundled-plugins/backup/README.md +81 -0
- package/src/bundled-plugins/backup/index.ts +209 -0
- package/src/bundled-plugins/backup/runner.ts +231 -0
- package/src/bundled-plugins/backup/subagents.ts +200 -0
- package/src/bundled-plugins/memory/index.ts +42 -1
- package/src/channels/router.ts +29 -0
- package/src/cli/compose.ts +92 -1
- package/src/cli/doctor.ts +100 -0
- package/src/cli/index.ts +1 -0
- package/src/compose/doctor.ts +141 -0
- package/src/compose/index.ts +8 -0
- package/src/compose/logs.ts +32 -19
- package/src/config/config.ts +20 -0
- package/src/container/log-colors.ts +75 -0
- package/src/container/log-timestamps.ts +84 -0
- package/src/container/logs.ts +71 -5
- package/src/container/start.ts +23 -8
- package/src/cron/consumer.ts +29 -7
- package/src/doctor/checks.ts +426 -0
- package/src/doctor/commit.ts +71 -0
- package/src/doctor/index.ts +287 -0
- package/src/doctor/plugin-bridge.ts +147 -0
- package/src/doctor/report.ts +142 -0
- package/src/doctor/types.ts +87 -0
- package/src/init/cli-version.ts +81 -0
- package/src/init/dockerfile.ts +223 -25
- package/src/init/index.ts +18 -10
- package/src/plugin/hooks.ts +32 -0
- package/src/plugin/index.ts +7 -0
- package/src/plugin/manager.ts +2 -0
- package/src/plugin/registry.ts +32 -3
- package/src/plugin/types.ts +65 -0
- package/src/run/bundled-plugins.ts +8 -0
- package/src/run/index.ts +10 -5
- package/src/server/index.ts +103 -5
- package/src/shared/index.ts +3 -0
- package/src/shared/protocol.ts +22 -0
- package/src/skills/typeclaw-config/SKILL.md +1 -1
- package/typeclaw.schema.json +50 -0
package/src/plugin/types.ts
CHANGED
|
@@ -97,6 +97,24 @@ export type SessionIdleEvent = {
|
|
|
97
97
|
origin?: SessionOrigin
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// Brackets every `session.prompt(...)` invocation. Distinct from
|
|
101
|
+
// `session.start`/`session.end` (which bracket session lifetime) so that
|
|
102
|
+
// long-lived TUI or channel sessions, which can sit idle between turns,
|
|
103
|
+
// don't wedge a turn-counter forever. `origin` carries the session's origin
|
|
104
|
+
// so observers can exclude their own induced turns when counting (e.g. the
|
|
105
|
+
// backup plugin excludes `subagent: 'backup'` to avoid self-gating).
|
|
106
|
+
export type SessionTurnStartEvent = {
|
|
107
|
+
sessionId: string
|
|
108
|
+
agentDir: string
|
|
109
|
+
origin?: SessionOrigin
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type SessionTurnEndEvent = {
|
|
113
|
+
sessionId: string
|
|
114
|
+
agentDir: string
|
|
115
|
+
origin?: SessionOrigin
|
|
116
|
+
}
|
|
117
|
+
|
|
100
118
|
// Provider prompt caching requires byte-identical prefixes. Mutations near the
|
|
101
119
|
// end of `event.prompt` preserve cache hits across sessions; mutations near
|
|
102
120
|
// the start invalidate the cache on every LLM call.
|
|
@@ -136,6 +154,8 @@ export type Hooks = {
|
|
|
136
154
|
'session.end'?: (event: SessionEndEvent, ctx: HookContext) => Promise<void> | void
|
|
137
155
|
'session.idle'?: (event: SessionIdleEvent, ctx: HookContext) => Promise<void> | void
|
|
138
156
|
'session.prompt'?: (event: SessionPromptEvent, ctx: HookContext) => Promise<void> | void
|
|
157
|
+
'session.turn.start'?: (event: SessionTurnStartEvent, ctx: HookContext) => Promise<void> | void
|
|
158
|
+
'session.turn.end'?: (event: SessionTurnEndEvent, ctx: HookContext) => Promise<void> | void
|
|
139
159
|
'tool.before'?: (event: ToolBeforeEvent, ctx: HookContext) => Promise<ToolBeforeResult> | ToolBeforeResult
|
|
140
160
|
'tool.after'?: (event: ToolAfterEvent, ctx: HookContext) => Promise<void> | void
|
|
141
161
|
}
|
|
@@ -164,6 +184,51 @@ export type PluginExports = {
|
|
|
164
184
|
skills?: Record<string, PluginSkill>
|
|
165
185
|
skillsDirs?: string[]
|
|
166
186
|
hooks?: Hooks
|
|
187
|
+
doctorChecks?: Record<string, PluginDoctorCheck>
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// `typeclaw doctor` plugin extension surface. Each check is read-only by
|
|
191
|
+
// default; declaring `fix.apply` opts the check into `typeclaw doctor --fix`,
|
|
192
|
+
// where the host serializes plugin fixes, validates their `changedPaths`
|
|
193
|
+
// against the agent folder, and commits the union of all fixes in a single
|
|
194
|
+
// commit.
|
|
195
|
+
export type PluginDoctorCheck = {
|
|
196
|
+
description: string
|
|
197
|
+
category?: string
|
|
198
|
+
run: (ctx: PluginDoctorContext) => Promise<PluginCheckResult>
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export type PluginDoctorContext = {
|
|
202
|
+
readonly pluginName: string
|
|
203
|
+
readonly agentDir: string
|
|
204
|
+
readonly config: unknown
|
|
205
|
+
readonly logger: PluginLogger
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export type PluginCheckStatus = 'ok' | 'warning' | 'error'
|
|
209
|
+
|
|
210
|
+
export type PluginCheckResult = {
|
|
211
|
+
status: PluginCheckStatus
|
|
212
|
+
message: string
|
|
213
|
+
details?: string[]
|
|
214
|
+
fix?: PluginFixSuggestion
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export type PluginFixSuggestion = {
|
|
218
|
+
description: string
|
|
219
|
+
// When omitted, the fix is advisory-only. `typeclaw doctor --fix` only
|
|
220
|
+
// attempts to remediate checks whose suggestion includes an `apply`.
|
|
221
|
+
apply?: (ctx: PluginDoctorContext) => Promise<PluginFixResult>
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export type PluginFixResult = {
|
|
225
|
+
// One-line description that appears in the commit body as a bullet.
|
|
226
|
+
summary: string
|
|
227
|
+
// POSIX paths relative to agentDir; the host validates each one stays
|
|
228
|
+
// inside agentDir before `git add`ing. Absolute paths and `..` segments
|
|
229
|
+
// are rejected to keep plugin fixes from staging files outside the agent
|
|
230
|
+
// folder. Empty array is valid (e.g. a fix that only logs).
|
|
231
|
+
changedPaths: string[]
|
|
167
232
|
}
|
|
168
233
|
|
|
169
234
|
export type DefinedPlugin<TConfig = never> = {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import agentBrowserPlugin from '@/bundled-plugins/agent-browser'
|
|
2
|
+
import backupPlugin from '@/bundled-plugins/backup'
|
|
2
3
|
import guardPlugin from '@/bundled-plugins/guard'
|
|
3
4
|
import memoryPlugin from '@/bundled-plugins/memory'
|
|
4
5
|
import securityPlugin from '@/bundled-plugins/security'
|
|
@@ -16,9 +17,16 @@ import type { ResolvedPlugin } from '@/plugin'
|
|
|
16
17
|
// Letting `guard` run first would still work today since the two plugins
|
|
17
18
|
// guard disjoint surfaces, but seeding the order now means future overlap
|
|
18
19
|
// (e.g. a security policy on writes) blocks before guard's softer advice.
|
|
20
|
+
//
|
|
21
|
+
// `memory` is registered before `backup` so memory's dreaming commits always
|
|
22
|
+
// land in the same git index window before backup's commit-and-push cycle.
|
|
23
|
+
// They commit disjoint paths today (memory/ vs sessions/ + agent changes),
|
|
24
|
+
// but if either ever holds .git/index.lock the deterministic order makes the
|
|
25
|
+
// contention easier to reason about.
|
|
19
26
|
export const BUNDLED_PLUGINS: ResolvedPlugin[] = [
|
|
20
27
|
{ name: 'security', version: undefined, source: '<bundled>', defined: securityPlugin },
|
|
21
28
|
{ name: 'guard', version: undefined, source: '<bundled>', defined: guardPlugin },
|
|
22
29
|
{ name: 'memory', version: undefined, source: '<bundled>', defined: memoryPlugin },
|
|
30
|
+
{ name: 'backup', version: undefined, source: '<bundled>', defined: backupPlugin },
|
|
23
31
|
{ name: 'agent-browser', version: undefined, source: '<bundled>', defined: agentBrowserPlugin },
|
|
24
32
|
]
|
package/src/run/index.ts
CHANGED
|
@@ -142,14 +142,15 @@ export async function startAgent({
|
|
|
142
142
|
const entry = snap.pluginSubagentByShim.get(subagent)
|
|
143
143
|
if (entry) {
|
|
144
144
|
const sessionId = `subagent-${entry.pluginName}-${crypto.randomUUID()}`
|
|
145
|
+
const origin = {
|
|
146
|
+
kind: 'subagent' as const,
|
|
147
|
+
subagent: subagentOptions?.name ?? entry.subagentName,
|
|
148
|
+
parentSessionId: subagentOptions?.parentSessionId ?? '<unknown>',
|
|
149
|
+
}
|
|
145
150
|
const created = await createSessionWithDispose({
|
|
146
151
|
systemPromptOverride: entry.pluginSubagent.systemPrompt,
|
|
147
152
|
channelRouter: channelManager.router,
|
|
148
|
-
origin
|
|
149
|
-
kind: 'subagent',
|
|
150
|
-
subagent: subagentOptions?.name ?? entry.subagentName,
|
|
151
|
-
parentSessionId: subagentOptions?.parentSessionId ?? '<unknown>',
|
|
152
|
-
},
|
|
153
|
+
origin,
|
|
153
154
|
plugins: {
|
|
154
155
|
registry: snap.registry,
|
|
155
156
|
hooks: snap.hooks,
|
|
@@ -167,6 +168,8 @@ export async function startAgent({
|
|
|
167
168
|
...created,
|
|
168
169
|
hooks: snap.hooks,
|
|
169
170
|
sessionId,
|
|
171
|
+
agentDir: cwd,
|
|
172
|
+
origin,
|
|
170
173
|
}
|
|
171
174
|
}
|
|
172
175
|
return defaultCreateSessionForSubagent(subagent, subagentOptions)
|
|
@@ -221,6 +224,8 @@ export async function startAgent({
|
|
|
221
224
|
prompt: (text) => session.prompt(text),
|
|
222
225
|
dispose: () => session.dispose(),
|
|
223
226
|
sessionId,
|
|
227
|
+
agentDir: cwd,
|
|
228
|
+
origin: { kind: 'cron' as const, jobId: job.id, jobKind: 'prompt' as const },
|
|
224
229
|
...(snap.hasAnyPluginContent ? { hooks: snap.hooks } : {}),
|
|
225
230
|
getTranscriptPath: () => sessionManager.getSessionFile(),
|
|
226
231
|
}
|
package/src/server/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type CreateSessionOptions,
|
|
7
7
|
type CreateSessionResult,
|
|
8
8
|
} from '@/agent'
|
|
9
|
+
import { runPluginDoctorChecks, runPluginDoctorFix } from '@/agent/doctor'
|
|
9
10
|
import type { SessionOrigin } from '@/agent/session-origin'
|
|
10
11
|
import type { ChannelRouter } from '@/channels/router'
|
|
11
12
|
import type { HookBus } from '@/plugin'
|
|
@@ -159,7 +160,7 @@ export function createServer({
|
|
|
159
160
|
|
|
160
161
|
if (stream) {
|
|
161
162
|
state.unsubPrompts = stream.subscribe({ target: { kind: 'session', sessionId: sessionFileId } }, (msg) =>
|
|
162
|
-
enqueuePrompt(ws, state, msg),
|
|
163
|
+
enqueuePrompt(ws, state, msg, agentDir),
|
|
163
164
|
)
|
|
164
165
|
|
|
165
166
|
state.unsubBroadcast = stream.subscribe({ target: { kind: 'broadcast' } }, (msg) => {
|
|
@@ -190,6 +191,16 @@ export function createServer({
|
|
|
190
191
|
return
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
if (msg.type === 'doctor') {
|
|
195
|
+
await handleDoctor(ws, msg.requestId, pluginRuntime, agentDir)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (msg.type === 'doctor_fix') {
|
|
200
|
+
await handleDoctorFix(ws, msg.requestId, msg.checkId, pluginRuntime, agentDir)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
193
204
|
if (msg.type === 'abort') {
|
|
194
205
|
if (!state) return
|
|
195
206
|
await state.session.abort()
|
|
@@ -215,13 +226,27 @@ export function createServer({
|
|
|
215
226
|
return
|
|
216
227
|
}
|
|
217
228
|
send(ws, { type: 'prompt_started', messageId: `local-${crypto.randomUUID()}`, text: msg.text })
|
|
229
|
+
const fallbackHooks = state.runtimeSnapshot?.hooks
|
|
230
|
+
if (fallbackHooks !== undefined && agentDir !== undefined) {
|
|
231
|
+
await fallbackHooks.runSessionTurnStart({
|
|
232
|
+
sessionId: state.sessionFileId,
|
|
233
|
+
agentDir,
|
|
234
|
+
origin: state.origin,
|
|
235
|
+
})
|
|
236
|
+
}
|
|
218
237
|
try {
|
|
219
238
|
await state.session.prompt(msg.text)
|
|
220
239
|
send(ws, { type: 'done' })
|
|
221
240
|
} catch (err) {
|
|
222
241
|
send(ws, { type: 'error', message: err instanceof Error ? err.message : String(err) })
|
|
223
242
|
}
|
|
224
|
-
|
|
243
|
+
if (fallbackHooks !== undefined && agentDir !== undefined) {
|
|
244
|
+
await fallbackHooks.runSessionTurnEnd({
|
|
245
|
+
sessionId: state.sessionFileId,
|
|
246
|
+
agentDir,
|
|
247
|
+
origin: state.origin,
|
|
248
|
+
})
|
|
249
|
+
}
|
|
225
250
|
if (fallbackHooks !== undefined) {
|
|
226
251
|
await fallbackHooks.runSessionIdle({
|
|
227
252
|
sessionId: state.sessionFileId,
|
|
@@ -323,7 +348,7 @@ function forwardAssistantError(ws: Ws, message: unknown): void {
|
|
|
323
348
|
send(ws, { type: 'error', message: text })
|
|
324
349
|
}
|
|
325
350
|
|
|
326
|
-
function enqueuePrompt(ws: Ws, state: SessionState, msg: StreamMessage): void {
|
|
351
|
+
function enqueuePrompt(ws: Ws, state: SessionState, msg: StreamMessage, agentDir: string | undefined): void {
|
|
327
352
|
const payload = msg.payload as { kind?: string; text?: string; delivery?: PromptDelivery }
|
|
328
353
|
if (payload?.kind !== 'prompt' || typeof payload.text !== 'string') return
|
|
329
354
|
const delivery: PromptDelivery = payload.delivery ?? 'queue'
|
|
@@ -339,7 +364,7 @@ function enqueuePrompt(ws: Ws, state: SessionState, msg: StreamMessage): void {
|
|
|
339
364
|
ts: msg.ts,
|
|
340
365
|
})
|
|
341
366
|
pushQueueState(ws, state)
|
|
342
|
-
void drain(ws, state)
|
|
367
|
+
void drain(ws, state, agentDir)
|
|
343
368
|
}
|
|
344
369
|
|
|
345
370
|
// `session.idle` semantically means "the agent finished a prompt and is now
|
|
@@ -360,10 +385,26 @@ function makeIdleHookCaller(state: SessionState): () => Promise<void> {
|
|
|
360
385
|
}
|
|
361
386
|
}
|
|
362
387
|
|
|
363
|
-
|
|
388
|
+
function makeTurnHookCallers(
|
|
389
|
+
state: SessionState,
|
|
390
|
+
agentDir: string | undefined,
|
|
391
|
+
): { fireTurnStart: () => Promise<void>; fireTurnEnd: () => Promise<void> } {
|
|
392
|
+
const hooks: HookBus | undefined = state.runtimeSnapshot?.hooks
|
|
393
|
+
if (hooks === undefined || agentDir === undefined) {
|
|
394
|
+
return { fireTurnStart: async () => {}, fireTurnEnd: async () => {} }
|
|
395
|
+
}
|
|
396
|
+
const event = { sessionId: state.sessionFileId, agentDir, origin: state.origin }
|
|
397
|
+
return {
|
|
398
|
+
fireTurnStart: () => hooks.runSessionTurnStart(event),
|
|
399
|
+
fireTurnEnd: () => hooks.runSessionTurnEnd(event),
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function drain(ws: Ws, state: SessionState, agentDir: string | undefined): Promise<void> {
|
|
364
404
|
if (state.draining) return
|
|
365
405
|
state.draining = true
|
|
366
406
|
const fireIdle = makeIdleHookCaller(state)
|
|
407
|
+
const { fireTurnStart, fireTurnEnd } = makeTurnHookCallers(state, agentDir)
|
|
367
408
|
try {
|
|
368
409
|
while (state.drainQueue.length > 0) {
|
|
369
410
|
const item = state.drainQueue.shift()
|
|
@@ -371,12 +412,14 @@ async function drain(ws: Ws, state: SessionState): Promise<void> {
|
|
|
371
412
|
pushQueueState(ws, state)
|
|
372
413
|
send(ws, { type: 'prompt_started', messageId: item.streamMessageId, text: item.text })
|
|
373
414
|
|
|
415
|
+
await fireTurnStart()
|
|
374
416
|
try {
|
|
375
417
|
await state.session.prompt(item.text)
|
|
376
418
|
send(ws, { type: 'done' })
|
|
377
419
|
} catch (err) {
|
|
378
420
|
send(ws, { type: 'error', message: err instanceof Error ? err.message : String(err) })
|
|
379
421
|
}
|
|
422
|
+
await fireTurnEnd()
|
|
380
423
|
await fireIdle()
|
|
381
424
|
}
|
|
382
425
|
} finally {
|
|
@@ -393,6 +436,61 @@ function pushQueueState(ws: Ws, state: SessionState): void {
|
|
|
393
436
|
send(ws, { type: 'queue_state', pending })
|
|
394
437
|
}
|
|
395
438
|
|
|
439
|
+
async function handleDoctor(
|
|
440
|
+
ws: Ws,
|
|
441
|
+
requestId: string,
|
|
442
|
+
pluginRuntime: PluginRuntime | undefined,
|
|
443
|
+
agentDir: string | undefined,
|
|
444
|
+
): Promise<void> {
|
|
445
|
+
if (pluginRuntime === undefined || agentDir === undefined) {
|
|
446
|
+
send(ws, { type: 'doctor_result', requestId, checks: [] })
|
|
447
|
+
return
|
|
448
|
+
}
|
|
449
|
+
const snapshot = pluginRuntime.get()
|
|
450
|
+
if (snapshot === undefined) {
|
|
451
|
+
send(ws, { type: 'doctor_result', requestId, checks: [] })
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const checks = await runPluginDoctorChecks({ registry: snapshot.registry, agentDir })
|
|
456
|
+
send(ws, { type: 'doctor_result', requestId, checks })
|
|
457
|
+
} catch (err) {
|
|
458
|
+
send(ws, { type: 'error', message: err instanceof Error ? err.message : String(err) })
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function handleDoctorFix(
|
|
463
|
+
ws: Ws,
|
|
464
|
+
requestId: string,
|
|
465
|
+
checkId: string,
|
|
466
|
+
pluginRuntime: PluginRuntime | undefined,
|
|
467
|
+
agentDir: string | undefined,
|
|
468
|
+
): Promise<void> {
|
|
469
|
+
if (pluginRuntime === undefined || agentDir === undefined) {
|
|
470
|
+
send(ws, {
|
|
471
|
+
type: 'doctor_fix_result',
|
|
472
|
+
requestId,
|
|
473
|
+
result: { ok: false, checkId, error: 'plugin runtime not configured' },
|
|
474
|
+
})
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
const snapshot = pluginRuntime.get()
|
|
478
|
+
if (snapshot === undefined) {
|
|
479
|
+
send(ws, {
|
|
480
|
+
type: 'doctor_fix_result',
|
|
481
|
+
requestId,
|
|
482
|
+
result: { ok: false, checkId, error: 'plugin runtime not configured' },
|
|
483
|
+
})
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
const outcome = await runPluginDoctorFix({ registry: snapshot.registry, agentDir, checkId })
|
|
487
|
+
const result =
|
|
488
|
+
outcome.ok === true
|
|
489
|
+
? { ok: true as const, checkId, summary: outcome.summary, changedPaths: outcome.changedPaths }
|
|
490
|
+
: { ok: false as const, checkId, error: outcome.error }
|
|
491
|
+
send(ws, { type: 'doctor_fix_result', requestId, result })
|
|
492
|
+
}
|
|
493
|
+
|
|
396
494
|
async function handleReload(
|
|
397
495
|
ws: Ws,
|
|
398
496
|
reloadAll: ReloadAllFn | undefined,
|
package/src/shared/index.ts
CHANGED
package/src/shared/protocol.ts
CHANGED
|
@@ -4,11 +4,31 @@ export type ReloadResultPayload =
|
|
|
4
4
|
|
|
5
5
|
export type PromptDelivery = 'queue' | 'steer' | 'interrupt'
|
|
6
6
|
|
|
7
|
+
export type DoctorRequestId = string
|
|
8
|
+
|
|
9
|
+
export type DoctorCheckPayload = {
|
|
10
|
+
id: string
|
|
11
|
+
pluginName: string
|
|
12
|
+
checkName: string
|
|
13
|
+
description: string
|
|
14
|
+
category: string
|
|
15
|
+
status: 'ok' | 'warning' | 'error'
|
|
16
|
+
message: string
|
|
17
|
+
details?: string[]
|
|
18
|
+
fix?: { description: string; hasApply: boolean }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type DoctorFixPayload =
|
|
22
|
+
| { ok: true; checkId: string; summary: string; changedPaths: string[] }
|
|
23
|
+
| { ok: false; checkId: string; error: string }
|
|
24
|
+
|
|
7
25
|
export type ClientMessage =
|
|
8
26
|
| { type: 'prompt'; text: string; delivery?: PromptDelivery }
|
|
9
27
|
| { type: 'reload'; scope?: string }
|
|
10
28
|
| { type: 'abort' }
|
|
11
29
|
| { type: 'queue_cancel'; messageId: string }
|
|
30
|
+
| { type: 'doctor'; requestId: DoctorRequestId }
|
|
31
|
+
| { type: 'doctor_fix'; requestId: DoctorRequestId; checkId: string }
|
|
12
32
|
|
|
13
33
|
export type QueueStateItem = { id: string; text: string; ts: number }
|
|
14
34
|
|
|
@@ -23,3 +43,5 @@ export type ServerMessage =
|
|
|
23
43
|
| { type: 'notification'; payload: unknown; replyTo?: string; meta?: Record<string, string> }
|
|
24
44
|
| { type: 'queue_state'; pending: QueueStateItem[] }
|
|
25
45
|
| { type: 'prompt_started'; messageId: string; text: string }
|
|
46
|
+
| { type: 'doctor_result'; requestId: DoctorRequestId; checks: DoctorCheckPayload[] }
|
|
47
|
+
| { type: 'doctor_fix_result'; requestId: DoctorRequestId; result: DoctorFixPayload }
|
|
@@ -403,7 +403,7 @@ RUN apt-get install ... <baseline + enabled toggle packages> ← toggles fan o
|
|
|
403
403
|
ENV NODE_ENV=production
|
|
404
404
|
# Custom lines from typeclaw.json#dockerfile.append. ← only emitted when append is non-empty
|
|
405
405
|
<your appended lines>
|
|
406
|
-
ENTRYPOINT ["
|
|
406
|
+
ENTRYPOINT ["/usr/local/bin/typeclaw-entrypoint"]
|
|
407
407
|
CMD ["run"]
|
|
408
408
|
```
|
|
409
409
|
|
package/typeclaw.schema.json
CHANGED
|
@@ -706,6 +706,18 @@
|
|
|
706
706
|
"allow"
|
|
707
707
|
]
|
|
708
708
|
},
|
|
709
|
+
"network": {
|
|
710
|
+
"default": {
|
|
711
|
+
"blockInternal": false
|
|
712
|
+
},
|
|
713
|
+
"type": "object",
|
|
714
|
+
"properties": {
|
|
715
|
+
"blockInternal": {
|
|
716
|
+
"default": false,
|
|
717
|
+
"type": "boolean"
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
},
|
|
709
721
|
"dockerfile": {
|
|
710
722
|
"default": {
|
|
711
723
|
"ffmpeg": false,
|
|
@@ -816,6 +828,44 @@
|
|
|
816
828
|
}
|
|
817
829
|
}
|
|
818
830
|
}
|
|
831
|
+
},
|
|
832
|
+
"backup": {
|
|
833
|
+
"default": {
|
|
834
|
+
"enabled": true,
|
|
835
|
+
"idleMs": 30000,
|
|
836
|
+
"pushToOrigin": true,
|
|
837
|
+
"commitTimeoutMs": 30000,
|
|
838
|
+
"networkTimeoutMs": 60000
|
|
839
|
+
},
|
|
840
|
+
"type": "object",
|
|
841
|
+
"properties": {
|
|
842
|
+
"enabled": {
|
|
843
|
+
"default": true,
|
|
844
|
+
"type": "boolean"
|
|
845
|
+
},
|
|
846
|
+
"idleMs": {
|
|
847
|
+
"default": 30000,
|
|
848
|
+
"type": "integer",
|
|
849
|
+
"minimum": 1000,
|
|
850
|
+
"maximum": 9007199254740991
|
|
851
|
+
},
|
|
852
|
+
"pushToOrigin": {
|
|
853
|
+
"default": true,
|
|
854
|
+
"type": "boolean"
|
|
855
|
+
},
|
|
856
|
+
"commitTimeoutMs": {
|
|
857
|
+
"default": 30000,
|
|
858
|
+
"type": "integer",
|
|
859
|
+
"minimum": 1,
|
|
860
|
+
"maximum": 9007199254740991
|
|
861
|
+
},
|
|
862
|
+
"networkTimeoutMs": {
|
|
863
|
+
"default": 60000,
|
|
864
|
+
"type": "integer",
|
|
865
|
+
"minimum": 1,
|
|
866
|
+
"maximum": 9007199254740991
|
|
867
|
+
}
|
|
868
|
+
}
|
|
819
869
|
}
|
|
820
870
|
},
|
|
821
871
|
"additionalProperties": {}
|