typeclaw 0.1.1 → 0.1.3
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 +16 -12
- package/auth.schema.json +238 -7
- package/package.json +1 -1
- package/secrets.schema.json +238 -7
- package/src/agent/auth.ts +19 -38
- package/src/agent/doctor.ts +173 -0
- package/src/agent/subagents.ts +24 -2
- package/src/agent/tools/channel-fetch-attachment.ts +6 -0
- package/src/agent/tools/channel-history.ts +10 -1
- package/src/agent/tools/channel-log.ts +32 -0
- package/src/agent/tools/channel-reply.ts +18 -1
- package/src/agent/tools/channel-send.ts +13 -1
- 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/bundled-plugins/tool-result-cap/README.md +67 -0
- package/src/bundled-plugins/tool-result-cap/cap-result.ts +56 -0
- package/src/bundled-plugins/tool-result-cap/index.ts +51 -0
- package/src/channels/adapters/kakaotalk.ts +25 -16
- package/src/channels/manager.ts +47 -38
- package/src/channels/router.ts +29 -0
- package/src/cli/channel.ts +3 -3
- package/src/cli/compose.ts +92 -1
- package/src/cli/doctor.ts +100 -0
- package/src/cli/index.ts +4 -0
- package/src/cli/init.ts +2 -1
- package/src/cli/ui.ts +11 -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 +31 -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 +113 -9
- 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/hostd/daemon.ts +28 -3
- package/src/hostd/protocol.ts +7 -0
- package/src/init/auto-upgrade.ts +368 -0
- package/src/init/cli-version.ts +81 -0
- package/src/init/dockerfile.ts +234 -25
- package/src/init/index.ts +141 -87
- package/src/init/kakaotalk-auth.ts +9 -3
- package/src/init/run-bun-install.ts +34 -0
- 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 +15 -0
- package/src/run/index.ts +19 -5
- package/src/secrets/defaults.ts +67 -0
- package/src/secrets/hydrate.ts +99 -0
- package/src/secrets/index.ts +6 -12
- package/src/secrets/kakao-store.ts +129 -0
- package/src/secrets/migrate-kakaotalk.ts +82 -0
- package/src/secrets/migrate.ts +5 -4
- package/src/secrets/resolve.ts +57 -0
- package/src/secrets/schema.ts +162 -42
- package/src/secrets/storage.ts +253 -47
- 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 +48 -9
- package/typeclaw.schema.json +84 -0
- package/src/secrets/env.ts +0 -43
package/src/container/logs.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { supportsColor } from './log-colors'
|
|
2
|
+
import { makeLogTimestampReformatter, type TimestampReformatter } from './log-timestamps'
|
|
1
3
|
import { containerExists, containerNameFromCwd, getBun } from './shared'
|
|
2
4
|
|
|
3
5
|
export type LogsPlan = {
|
|
@@ -7,7 +9,25 @@ export type LogsPlan = {
|
|
|
7
9
|
|
|
8
10
|
export type LogsResult = { ok: true; containerName: string; exitCode: number } | { ok: false; reason: string }
|
|
9
11
|
|
|
10
|
-
export
|
|
12
|
+
export type LogsOptions = {
|
|
13
|
+
cwd: string
|
|
14
|
+
follow: boolean
|
|
15
|
+
out?: NodeJS.WritableStream
|
|
16
|
+
err?: NodeJS.WritableStream
|
|
17
|
+
signal?: AbortSignal
|
|
18
|
+
// When undefined, defaults to TTY+NO_COLOR detection on `out`/`err`.
|
|
19
|
+
// Tests pass `false` for deterministic plain output.
|
|
20
|
+
useColor?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function logs({
|
|
24
|
+
cwd,
|
|
25
|
+
follow,
|
|
26
|
+
out = process.stdout,
|
|
27
|
+
err = process.stderr,
|
|
28
|
+
signal,
|
|
29
|
+
useColor,
|
|
30
|
+
}: LogsOptions): Promise<LogsResult> {
|
|
11
31
|
const bun = getBun()
|
|
12
32
|
if (!bun) return { ok: false, reason: 'bun runtime not available' }
|
|
13
33
|
|
|
@@ -18,14 +38,30 @@ export async function logs({ cwd, follow }: { cwd: string; follow: boolean }): P
|
|
|
18
38
|
return { ok: false, reason: `Container ${containerName} not found. Run \`typeclaw start\` first.` }
|
|
19
39
|
}
|
|
20
40
|
|
|
21
|
-
const cmd = ['docker', 'logs']
|
|
41
|
+
const cmd = ['docker', 'logs', '--timestamps']
|
|
22
42
|
if (follow) cmd.push('-f')
|
|
23
43
|
cmd.push(containerName)
|
|
24
44
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
45
|
+
const proc = bun.spawn({ cmd, cwd, stdout: 'pipe', stderr: 'pipe' })
|
|
46
|
+
|
|
47
|
+
const onAbort = (): void => {
|
|
48
|
+
try {
|
|
49
|
+
proc.kill('SIGTERM')
|
|
50
|
+
} catch {
|
|
51
|
+
// already exited
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
55
|
+
|
|
56
|
+
const colorOut = useColor ?? supportsColor(out)
|
|
57
|
+
const colorErr = useColor ?? supportsColor(err)
|
|
58
|
+
await Promise.all([
|
|
59
|
+
pumpWithTimestamps(proc.stdout, out, makeLogTimestampReformatter(undefined, { color: colorOut })),
|
|
60
|
+
pumpWithTimestamps(proc.stderr, err, makeLogTimestampReformatter(undefined, { color: colorErr })),
|
|
61
|
+
])
|
|
28
62
|
const exitCode = await proc.exited
|
|
63
|
+
signal?.removeEventListener('abort', onAbort)
|
|
64
|
+
|
|
29
65
|
return { ok: true, containerName, exitCode }
|
|
30
66
|
} catch (error) {
|
|
31
67
|
return { ok: false, reason: error instanceof Error ? error.message : String(error) }
|
|
@@ -35,3 +71,33 @@ export async function logs({ cwd, follow }: { cwd: string; follow: boolean }): P
|
|
|
35
71
|
export function planLogs(cwd: string, { follow }: { follow: boolean }): LogsPlan {
|
|
36
72
|
return { containerName: containerNameFromCwd(cwd), follow }
|
|
37
73
|
}
|
|
74
|
+
|
|
75
|
+
// Exported for `compose/logs.ts` so the multi-agent path reuses the same
|
|
76
|
+
// reformatter and stays consistent with single-agent output.
|
|
77
|
+
export async function pumpWithTimestamps(
|
|
78
|
+
stream: ReadableStream<Uint8Array>,
|
|
79
|
+
sink: NodeJS.WritableStream,
|
|
80
|
+
reformatter: TimestampReformatter = makeLogTimestampReformatter(),
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
const decoder = new TextDecoder()
|
|
83
|
+
const reader = stream.getReader()
|
|
84
|
+
try {
|
|
85
|
+
while (true) {
|
|
86
|
+
const { done, value } = await reader.read()
|
|
87
|
+
if (done) break
|
|
88
|
+
if (value && value.byteLength > 0) {
|
|
89
|
+
const out = reformatter.write(decoder.decode(value, { stream: true }))
|
|
90
|
+
if (out.length > 0) sink.write(out)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const tail = decoder.decode()
|
|
94
|
+
if (tail.length > 0) {
|
|
95
|
+
const out = reformatter.write(tail)
|
|
96
|
+
if (out.length > 0) sink.write(out)
|
|
97
|
+
}
|
|
98
|
+
const flushed = reformatter.flush()
|
|
99
|
+
if (flushed.length > 0) sink.write(flushed)
|
|
100
|
+
} finally {
|
|
101
|
+
reader.releaseLock()
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/container/start.ts
CHANGED
|
@@ -7,10 +7,20 @@ import { configSchema, expandMountPath, type Config } from '@/config/config'
|
|
|
7
7
|
import { send as sendToDaemon } from '@/hostd/client'
|
|
8
8
|
import type { HttpInfoResult } from '@/hostd/protocol'
|
|
9
9
|
import { ensureDaemon } from '@/hostd/spawn'
|
|
10
|
+
import {
|
|
11
|
+
autoUpgradeTypeclawDep,
|
|
12
|
+
type AutoUpgradeOutcome,
|
|
13
|
+
expectedInstalledAfterUpgrade,
|
|
14
|
+
outcomeForcesInstall,
|
|
15
|
+
readInstalledTypeclawVersionFromAgent,
|
|
16
|
+
} from '@/init/auto-upgrade'
|
|
17
|
+
import { resolveBaseImageVersion } from '@/init/cli-version'
|
|
10
18
|
import { buildDockerfile, DOCKERFILE } from '@/init/dockerfile'
|
|
11
19
|
import { ensureDepsInstalled, type EnsureDepsResult } from '@/init/ensure-deps'
|
|
12
20
|
import { buildGitignore, GITIGNORE_FILE } from '@/init/gitignore'
|
|
13
21
|
import { refreshPackageJson } from '@/init/packagejson'
|
|
22
|
+
import { runBunUpdate, type UpdateRunner } from '@/init/run-bun-install'
|
|
23
|
+
import { migrateKakaotalkCredentials } from '@/secrets'
|
|
14
24
|
|
|
15
25
|
import { CONTAINER_PORT, findFreePort, isPortAllocatedError } from './port'
|
|
16
26
|
import {
|
|
@@ -76,6 +86,25 @@ export type StartOptions = {
|
|
|
76
86
|
// Reusing that daemon avoids a self-shutdown when disk source has drifted.
|
|
77
87
|
reuseCurrentHostDaemon?: boolean
|
|
78
88
|
ensureDeps?: (cwd: string) => Promise<EnsureDepsResult>
|
|
89
|
+
// Test seam for the typeclaw-version auto-upgrade. Production callers omit
|
|
90
|
+
// this and get the real autoUpgradeTypeclawDep (which reads the CLI's own
|
|
91
|
+
// package.json). Tests inject a stub to simulate `bun -g update typeclaw`
|
|
92
|
+
// having bumped the CLI without touching the agent folder.
|
|
93
|
+
autoUpgrade?: (cwd: string) => Promise<AutoUpgradeOutcome>
|
|
94
|
+
// Test seam for the auto-upgrade-triggered registry resolution. Defaults
|
|
95
|
+
// to `bun update typeclaw --latest`. Cannot be `runBunInstall` — see the
|
|
96
|
+
// module header in src/init/auto-upgrade.ts for why install doesn't move
|
|
97
|
+
// an already-locked in-range dep.
|
|
98
|
+
forceBunUpdate?: UpdateRunner
|
|
99
|
+
// Test seam for the post-install verification. Reads the version actually
|
|
100
|
+
// present in <agent>/node_modules/typeclaw/package.json after the upgrade
|
|
101
|
+
// install completes. Defaults to readInstalledTypeclawVersionFromAgent.
|
|
102
|
+
// Verification is mandatory: `bun update` can succeed (exit 0) but still
|
|
103
|
+
// resolve to an older version than expected if the registry has issues
|
|
104
|
+
// or the spec resolution surprises us; we MUST refuse to proceed to
|
|
105
|
+
// refreshDockerfile in that case, otherwise the Dockerfile pins a stale
|
|
106
|
+
// base image and the build either fails or runs against the wrong runtime.
|
|
107
|
+
readInstalledVersion?: (cwd: string) => string | null
|
|
79
108
|
// Post-`docker run` verifier. `docker run -d` returns exit 0 the moment the
|
|
80
109
|
// container is created, even if its entrypoint crashes milliseconds later.
|
|
81
110
|
// The default verifier polls `docker inspect` for 1.5s and converts crashes
|
|
@@ -105,6 +134,7 @@ export type StartResult =
|
|
|
105
134
|
// every fresh launch, including the post-stale-corpse `--rm` recovery
|
|
106
135
|
// path — that one rebuilds the container from scratch.
|
|
107
136
|
alreadyRunning: boolean
|
|
137
|
+
autoUpgrade: AutoUpgradeOutcome
|
|
108
138
|
}
|
|
109
139
|
| { ok: false; reason: string }
|
|
110
140
|
|
|
@@ -117,6 +147,9 @@ export async function start({
|
|
|
117
147
|
cliEntry,
|
|
118
148
|
reuseCurrentHostDaemon = false,
|
|
119
149
|
ensureDeps = (dir) => ensureDepsInstalled({ cwd: dir }),
|
|
150
|
+
autoUpgrade = (dir) => autoUpgradeTypeclawDep({ cwd: dir }),
|
|
151
|
+
forceBunUpdate = runBunUpdate,
|
|
152
|
+
readInstalledVersion = readInstalledTypeclawVersionFromAgent,
|
|
120
153
|
verifyRunning = createVerifyRunning({ exec }),
|
|
121
154
|
}: StartOptions): Promise<StartResult> {
|
|
122
155
|
try {
|
|
@@ -142,13 +175,48 @@ export async function start({
|
|
|
142
175
|
// one-shot and idempotent — once `workspaces` is set, refreshPackageJson
|
|
143
176
|
// is a no-op, so users who never edit their agent folder pay zero cost on
|
|
144
177
|
// subsequent starts and users who customized `workspaces` are not clobbered.
|
|
145
|
-
await refreshDockerfile(cwd)
|
|
146
178
|
await refreshGitignore(cwd)
|
|
147
179
|
const pkgRefresh = await refreshPackageJson(cwd)
|
|
148
180
|
await commitSystemFile(cwd, GITIGNORE_FILE, 'Update .gitignore')
|
|
149
181
|
if (pkgRefresh.changed) {
|
|
150
182
|
await commitSystemFile(cwd, pkgRefresh.files, 'Enable bun workspaces (packages/*)')
|
|
151
183
|
}
|
|
184
|
+
|
|
185
|
+
// Align the agent's typeclaw dep with the global CLI version BEFORE
|
|
186
|
+
// ensureDeps runs. The classic regression this prevents: `bun -g update
|
|
187
|
+
// typeclaw` bumps the global CLI but the agent's node_modules/typeclaw
|
|
188
|
+
// stays pinned to whatever was installed at init time. refreshDockerfile
|
|
189
|
+
// then pins FROM ghcr/typeclaw-base:<old-version> and the docker build
|
|
190
|
+
// either fails (image never published) or runs against a stale runtime.
|
|
191
|
+
//
|
|
192
|
+
// We use `bun update typeclaw --latest` (NOT `bun install`) because plain
|
|
193
|
+
// install honors the lockfile and is a no-op when the lockfile already
|
|
194
|
+
// satisfies the declared spec — which is the canonical regression case
|
|
195
|
+
// (lockfile pins 0.1.0, spec says ^0.1.0, CLI is 0.1.2; install does
|
|
196
|
+
// nothing, update force-resolves to 0.1.2).
|
|
197
|
+
//
|
|
198
|
+
// After the update we MUST verify the installed version actually matches
|
|
199
|
+
// the upgrade target. `bun update` can exit 0 but resolve to a stale
|
|
200
|
+
// version (registry hiccups, surprising spec resolution). If verification
|
|
201
|
+
// fails we abort before refreshDockerfile so we never pin a stale base
|
|
202
|
+
// image to a fresh container build.
|
|
203
|
+
const upgrade = await autoUpgrade(cwd)
|
|
204
|
+
const upgradeCommitMessage = commitMessageForAutoUpgrade(upgrade)
|
|
205
|
+
if (outcomeForcesInstall(upgrade)) {
|
|
206
|
+
const forced = await forceBunUpdate(cwd, 'typeclaw')
|
|
207
|
+
if (!forced.ok) {
|
|
208
|
+
return { ok: false, reason: `typeclaw auto-upgrade install failed: ${forced.reason}` }
|
|
209
|
+
}
|
|
210
|
+
const expected = expectedInstalledAfterUpgrade(upgrade)
|
|
211
|
+
const installedAfter = readInstalledVersion(cwd)
|
|
212
|
+
if (expected !== null && (installedAfter === null || !installedReachesTarget(installedAfter, expected))) {
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
reason: `typeclaw auto-upgrade verification failed: bun update reported success but <agent>/node_modules/typeclaw is ${installedAfter ?? 'missing'} (expected >= ${expected}). Refusing to build a Docker image against a stale runtime.`,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
152
220
|
// Run `bun install` BEFORE the dependency-drift commit so the lockfile
|
|
153
221
|
// changes the install produces are caught by the same commit. Without
|
|
154
222
|
// this, upgrading the typeclaw CLI to a version that adds a new dep
|
|
@@ -160,7 +228,13 @@ export async function start({
|
|
|
160
228
|
if (!deps.ok) {
|
|
161
229
|
return { ok: false, reason: `dependency install failed: ${deps.reason}` }
|
|
162
230
|
}
|
|
163
|
-
await commitSystemFile(cwd, DEPENDENCY_FILES, 'Update dependencies')
|
|
231
|
+
await commitSystemFile(cwd, DEPENDENCY_FILES, upgradeCommitMessage ?? 'Update dependencies')
|
|
232
|
+
// Dockerfile refresh AFTER ensureDeps so the version pin in the FROM
|
|
233
|
+
// line resolves against the agent's installed node_modules/typeclaw —
|
|
234
|
+
// ensures the base image's CLI version matches the runtime the
|
|
235
|
+
// container will actually load.
|
|
236
|
+
await refreshDockerfile(cwd)
|
|
237
|
+
await migrateKakaotalkCredentials(cwd)
|
|
164
238
|
|
|
165
239
|
if (state.exists) {
|
|
166
240
|
// Container holds the name but is not running. Without `--rm`, this is
|
|
@@ -298,12 +372,31 @@ export async function start({
|
|
|
298
372
|
hostPort,
|
|
299
373
|
hostd: stripHostDaemonControl(hostd),
|
|
300
374
|
alreadyRunning: false,
|
|
375
|
+
autoUpgrade: upgrade,
|
|
301
376
|
}
|
|
302
377
|
} catch (error) {
|
|
303
378
|
return { ok: false, reason: error instanceof Error ? error.message : String(error) }
|
|
304
379
|
}
|
|
305
380
|
}
|
|
306
381
|
|
|
382
|
+
function commitMessageForAutoUpgrade(outcome: AutoUpgradeOutcome): string | null {
|
|
383
|
+
if (outcome.kind === 'spec-rewritten') return `Upgrade typeclaw to ${outcome.to}`
|
|
384
|
+
if (outcome.kind === 'reinstall-needed') return `Upgrade typeclaw to ${outcome.to}`
|
|
385
|
+
return null
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function installedReachesTarget(installed: string, target: string): boolean {
|
|
389
|
+
const ai = installed.match(/^(\d+)\.(\d+)\.(\d+)$/)
|
|
390
|
+
const at = target.match(/^(\d+)\.(\d+)\.(\d+)$/)
|
|
391
|
+
if (!ai || !at) return false
|
|
392
|
+
for (let i = 1; i <= 3; i++) {
|
|
393
|
+
const a = Number.parseInt(ai[i]!, 10)
|
|
394
|
+
const t = Number.parseInt(at[i]!, 10)
|
|
395
|
+
if (a !== t) return a > t
|
|
396
|
+
}
|
|
397
|
+
return true
|
|
398
|
+
}
|
|
399
|
+
|
|
307
400
|
export async function planStart({
|
|
308
401
|
cwd,
|
|
309
402
|
hostPort,
|
|
@@ -315,7 +408,8 @@ export async function planStart({
|
|
|
315
408
|
const imageTag = imageTagFromCwd(cwd)
|
|
316
409
|
|
|
317
410
|
const devSourcePath = await detectDevSource(cwd)
|
|
318
|
-
const
|
|
411
|
+
const cfg = await loadTypeclawConfig(cwd)
|
|
412
|
+
const mounts = cfg.mounts
|
|
319
413
|
|
|
320
414
|
// No `--rm`: a crashed container's logs MUST survive past exit so users can
|
|
321
415
|
// debug the failure. `typeclaw stop` removes the container explicitly, and
|
|
@@ -324,6 +418,17 @@ export async function planStart({
|
|
|
324
418
|
// a running container or one the user has not started again yet.
|
|
325
419
|
const runArgs = ['run', '-d', '--name', containerName, '-p', `127.0.0.1:${hostPort}:${CONTAINER_PORT}`]
|
|
326
420
|
|
|
421
|
+
// Network egress filter: when `typeclaw.json#network.blockInternal` is true,
|
|
422
|
+
// grant the container CAP_NET_ADMIN at boot so the entrypoint shim can
|
|
423
|
+
// install iptables OUTPUT rules. The shim drops the capability from the
|
|
424
|
+
// bounding set via setpriv before exec'ing the agent — see the shim source
|
|
425
|
+
// in src/init/dockerfile.ts for the full handoff. The `-e` flag is what
|
|
426
|
+
// tells the shim to take the on-path; absent or set to anything other than
|
|
427
|
+
// "1", the shim is a no-op.
|
|
428
|
+
if (cfg.network.blockInternal) {
|
|
429
|
+
runArgs.push('--cap-add=NET_ADMIN', '-e', 'TYPECLAW_NETWORK_BLOCK_INTERNAL=1')
|
|
430
|
+
}
|
|
431
|
+
|
|
327
432
|
if (hostdControl) {
|
|
328
433
|
runArgs.push('--add-host', HOST_GATEWAY_ALIAS)
|
|
329
434
|
}
|
|
@@ -386,7 +491,10 @@ export async function planStart({
|
|
|
386
491
|
|
|
387
492
|
export async function refreshDockerfile(cwd: string): Promise<void> {
|
|
388
493
|
const cfg = await loadTypeclawConfig(cwd)
|
|
389
|
-
await writeFile(
|
|
494
|
+
await writeFile(
|
|
495
|
+
join(cwd, DOCKERFILE),
|
|
496
|
+
buildDockerfile(cfg.dockerfile, { baseImageVersion: resolveBaseImageVersion(cwd) }),
|
|
497
|
+
)
|
|
390
498
|
}
|
|
391
499
|
|
|
392
500
|
export async function refreshGitignore(cwd: string): Promise<void> {
|
|
@@ -517,6 +625,7 @@ async function reportAlreadyRunning(exec: DockerExec, cwd: string, containerName
|
|
|
517
625
|
hostPort,
|
|
518
626
|
hostd: { state: 'disabled' },
|
|
519
627
|
alreadyRunning: true,
|
|
628
|
+
autoUpgrade: { kind: 'skipped-already-running' },
|
|
520
629
|
}
|
|
521
630
|
}
|
|
522
631
|
|
|
@@ -576,11 +685,6 @@ async function detectDevSource(cwd: string): Promise<string | null> {
|
|
|
576
685
|
// folder mid-init). Anything else — malformed JSON, schema-invalid config,
|
|
577
686
|
// invalid mount entry — must surface so the user sees they configured a mount
|
|
578
687
|
// that won't be applied.
|
|
579
|
-
async function loadMounts(cwd: string): Promise<Config['mounts']> {
|
|
580
|
-
const cfg = await loadTypeclawConfig(cwd)
|
|
581
|
-
return cfg.mounts
|
|
582
|
-
}
|
|
583
|
-
|
|
584
688
|
async function loadTypeclawConfig(cwd: string): Promise<Config> {
|
|
585
689
|
return configSchema.parse(await loadConfigJson(cwd))
|
|
586
690
|
}
|
package/src/cron/consumer.ts
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
|
+
import type { SessionOrigin } from '@/agent/session-origin'
|
|
1
2
|
import type { HookBus } from '@/plugin'
|
|
2
3
|
import type { Stream, Unsubscribe } from '@/stream'
|
|
3
4
|
|
|
4
5
|
import type { CronJob, ExecJob, PromptJob } from './schema'
|
|
5
6
|
|
|
6
|
-
// `hooks`, `sessionId`, and `getTranscriptPath` are optional so
|
|
7
|
-
// stay one-liners. When present, the consumer fires
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// the
|
|
11
|
-
//
|
|
7
|
+
// `hooks`, `sessionId`, `agentDir`, and `getTranscriptPath` are optional so
|
|
8
|
+
// test fakes can stay one-liners. When present, the consumer fires
|
|
9
|
+
// `session.turn.start`/`session.turn.end` around `prompt()`, then
|
|
10
|
+
// `session.idle` after, then `session.end` on dispose — mirroring the
|
|
11
|
+
// lifecycle signals the TUI server emits in `src/server/index.ts`. Without
|
|
12
|
+
// this the bundled memory plugin's debounced `memory-logger` never spawns for
|
|
13
|
+
// cron prompt jobs (it only wakes on `session.idle`), and the bundled backup
|
|
14
|
+
// plugin's turn counter would miss cron-driven activity.
|
|
12
15
|
export type CronSession = {
|
|
13
16
|
prompt: (text: string) => Promise<void>
|
|
14
17
|
dispose?: () => void
|
|
15
18
|
hooks?: HookBus
|
|
16
19
|
sessionId?: string
|
|
20
|
+
agentDir?: string
|
|
17
21
|
getTranscriptPath?: () => string | undefined
|
|
22
|
+
origin?: SessionOrigin
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export type CronConsumerLogger = {
|
|
@@ -102,8 +107,25 @@ async function runPrompt(
|
|
|
102
107
|
return
|
|
103
108
|
}
|
|
104
109
|
const session = await createSessionForCron(job)
|
|
110
|
+
const turnEvent =
|
|
111
|
+
session.hooks && session.sessionId !== undefined && session.agentDir !== undefined
|
|
112
|
+
? {
|
|
113
|
+
sessionId: session.sessionId,
|
|
114
|
+
agentDir: session.agentDir,
|
|
115
|
+
...(session.origin !== undefined ? { origin: session.origin } : {}),
|
|
116
|
+
}
|
|
117
|
+
: undefined
|
|
105
118
|
try {
|
|
106
|
-
|
|
119
|
+
if (session.hooks && turnEvent !== undefined) {
|
|
120
|
+
await session.hooks.runSessionTurnStart(turnEvent)
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await session.prompt(job.prompt)
|
|
124
|
+
} finally {
|
|
125
|
+
if (session.hooks && turnEvent !== undefined) {
|
|
126
|
+
await session.hooks.runSessionTurnEnd(turnEvent)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
107
129
|
if (session.hooks && session.sessionId !== undefined) {
|
|
108
130
|
await session.hooks.runSessionIdle({
|
|
109
131
|
sessionId: session.sessionId,
|