typeclaw 0.8.0 → 0.9.0
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 +6 -6
- package/package.json +5 -3
- package/scripts/require-parallel.ts +41 -0
- package/src/agent/index.ts +55 -6
- package/src/agent/live-sessions.ts +34 -0
- package/src/agent/plugin-tools.ts +2 -0
- package/src/agent/session-meta.ts +21 -2
- package/src/agent/subagent-completion-reminder.ts +89 -0
- package/src/agent/subagents.ts +3 -2
- package/src/agent/system-prompt.ts +10 -8
- package/src/bundled-plugins/explorer/explorer.ts +2 -2
- package/src/bundled-plugins/guard/index.ts +14 -1
- package/src/bundled-plugins/guard/policies/managed-config.ts +43 -13
- package/src/bundled-plugins/guard/policies/memory-retrieval-cache-write.ts +37 -0
- package/src/bundled-plugins/guard/policies/memory-topics-delete.ts +67 -0
- package/src/bundled-plugins/guard/policies/memory-topics-write.ts +33 -0
- package/src/bundled-plugins/guard/policies/non-workspace-write.ts +8 -2
- package/src/bundled-plugins/guard/policy.ts +7 -0
- package/src/bundled-plugins/memory/README.md +76 -62
- package/src/bundled-plugins/memory/append-tool.ts +3 -2
- package/src/bundled-plugins/memory/citation-superset.ts +49 -11
- package/src/bundled-plugins/memory/citations.ts +19 -8
- package/src/bundled-plugins/memory/delete-tool.ts +57 -0
- package/src/bundled-plugins/memory/dreaming-state.ts +1 -1
- package/src/bundled-plugins/memory/dreaming.ts +364 -146
- package/src/bundled-plugins/memory/frontmatter.ts +165 -0
- package/src/bundled-plugins/memory/index.ts +236 -16
- package/src/bundled-plugins/memory/injection-plan.ts +15 -0
- package/src/bundled-plugins/memory/load-memory.ts +102 -103
- package/src/bundled-plugins/memory/load-shards.ts +156 -0
- package/src/bundled-plugins/memory/memory-logger.ts +16 -15
- package/src/bundled-plugins/memory/memory-retrieval.ts +105 -0
- package/src/bundled-plugins/memory/migration.ts +282 -1
- package/src/bundled-plugins/memory/paths.ts +42 -0
- package/src/bundled-plugins/memory/search-tool.ts +232 -0
- package/src/bundled-plugins/memory/secret-detector.ts +2 -2
- package/src/bundled-plugins/memory/shard-snapshot.ts +51 -0
- package/src/bundled-plugins/memory/slug.ts +59 -0
- package/src/bundled-plugins/memory/stream-io.ts +110 -1
- package/src/bundled-plugins/memory/strength.ts +3 -3
- package/src/bundled-plugins/memory/topics.ts +70 -16
- package/src/bundled-plugins/security/index.ts +24 -0
- package/src/bundled-plugins/security/permissions.ts +4 -0
- package/src/bundled-plugins/security/policies/cron-promotion.ts +349 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +2 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +3 -0
- package/src/bundled-plugins/security/policies/role-promotion.ts +419 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +1 -0
- package/src/channels/adapters/kakaotalk-attachment.ts +7 -17
- package/src/channels/adapters/kakaotalk.ts +64 -37
- package/src/channels/adapters/slack-bot-classify.ts +2 -27
- package/src/channels/index.ts +5 -0
- package/src/channels/router.ts +201 -17
- package/src/channels/subagent-completion-bridge.ts +84 -0
- package/src/cli/builtins.ts +1 -0
- package/src/cli/index.ts +1 -0
- package/src/cli/init.ts +122 -14
- package/src/cli/inspect.ts +151 -0
- package/src/cron/consumer.ts +1 -1
- package/src/init/dockerfile.ts +268 -4
- package/src/init/hatching.ts +5 -6
- package/src/init/kakaotalk-auth.ts +6 -47
- package/src/init/validate-api-key.ts +121 -0
- package/src/inspect/index.ts +213 -0
- package/src/inspect/label.ts +50 -0
- package/src/inspect/live.ts +221 -0
- package/src/inspect/render.ts +163 -0
- package/src/inspect/replay.ts +265 -0
- package/src/inspect/session-list.ts +160 -0
- package/src/inspect/types.ts +110 -0
- package/src/plugin/hooks.ts +23 -1
- package/src/plugin/index.ts +2 -0
- package/src/plugin/manager.ts +1 -1
- package/src/plugin/registry.ts +1 -1
- package/src/plugin/types.ts +10 -0
- package/src/run/channel-session-factory.ts +7 -1
- package/src/run/index.ts +87 -21
- package/src/secrets/kakao-renewal.ts +3 -47
- package/src/server/index.ts +241 -60
- package/src/shared/index.ts +3 -0
- package/src/shared/protocol.ts +49 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +9 -9
- package/src/skills/typeclaw-claude-code/SKILL.md +57 -39
- package/src/skills/typeclaw-claude-code/references/stop-hook.md +2 -0
- package/src/skills/typeclaw-claude-code/references/tmux-driving.md +102 -16
- package/src/skills/typeclaw-config/SKILL.md +1 -1
- package/src/skills/typeclaw-cron/SKILL.md +1 -1
- package/src/skills/typeclaw-memory/SKILL.md +16 -163
- package/src/skills/typeclaw-permissions/SKILL.md +2 -2
- package/src/skills/typeclaw-plugins/SKILL.md +25 -14
- package/src/test-helpers/wait-for.ts +7 -1
- package/typeclaw.schema.json +7 -0
package/src/cli/init.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { defineCommand } from 'citty'
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
KNOWN_PROVIDERS,
|
|
9
|
+
providerForModelRef,
|
|
9
10
|
supportsApiKey as providerSupportsApiKey,
|
|
10
11
|
supportsOAuth as providerSupportsOAuth,
|
|
11
12
|
type KnownModelRef,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
import { runKakaotalkBootstrap } from '@/init/kakaotalk-auth'
|
|
31
32
|
import { fetchModelOptions, type ModelOption } from '@/init/models-dev'
|
|
32
33
|
import { makeOAuthLoginRunner, type OAuthLoginResult } from '@/init/oauth-login'
|
|
34
|
+
import { API_KEY_DASHBOARD_URL, validateApiKey, type KeyValidationResult } from '@/init/validate-api-key'
|
|
33
35
|
|
|
34
36
|
import { buildOAuthCallbacks } from './oauth-callbacks'
|
|
35
37
|
import { c, done, errorLine, printSlackAppManifestSetup } from './ui'
|
|
@@ -237,14 +239,29 @@ export const init = defineCommand({
|
|
|
237
239
|
}
|
|
238
240
|
|
|
239
241
|
if (hatchingOk) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
242
|
+
const claimableChannel =
|
|
243
|
+
channelChoice !== 'none' && channelChoice !== 'github' ? channelDisplayName(channelChoice) : null
|
|
244
|
+
const hints: Array<{ label: string; command: string }> = []
|
|
245
|
+
if (claimableChannel !== null) {
|
|
246
|
+
hints.push({ label: 'Claim your agent:', command: 'typeclaw role claim' })
|
|
247
|
+
}
|
|
248
|
+
hints.push(
|
|
249
|
+
{ label: 'Attach TUI:', command: 'typeclaw tui' },
|
|
250
|
+
{ label: 'Follow logs:', command: 'typeclaw logs -f' },
|
|
251
|
+
{ label: 'Stop:', command: 'typeclaw stop' },
|
|
252
|
+
{ label: 'Diagnose issues:', command: 'typeclaw doctor' },
|
|
253
|
+
)
|
|
254
|
+
if (claimableChannel !== null) {
|
|
255
|
+
note(
|
|
256
|
+
[
|
|
257
|
+
`Your agent will not respond on ${claimableChannel} until you claim ownership.`,
|
|
258
|
+
`This prevents strangers from talking to it.`,
|
|
259
|
+
`Run \`typeclaw role claim\` to finish setup.`,
|
|
260
|
+
].join('\n'),
|
|
261
|
+
'Claim ownership before chatting',
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
done({ title: c.green('Hatched. Your agent is ready.'), hints })
|
|
248
265
|
}
|
|
249
266
|
},
|
|
250
267
|
})
|
|
@@ -329,6 +346,7 @@ export interface WizardPrompts {
|
|
|
329
346
|
initial: 'api-key' | 'oauth' | undefined,
|
|
330
347
|
) => Promise<StepResult<'api-key' | 'oauth'>>
|
|
331
348
|
askApiKey: (provider: (typeof KNOWN_PROVIDERS)[KnownProviderId]) => Promise<StepResult<string>>
|
|
349
|
+
validateApiKey: (providerId: KnownProviderId, key: string) => Promise<KeyValidationResult>
|
|
332
350
|
pickVisionProvider: (
|
|
333
351
|
options: ModelOption[],
|
|
334
352
|
initial: KnownProviderId | undefined,
|
|
@@ -368,6 +386,7 @@ export const defaultWizardPrompts: WizardPrompts = {
|
|
|
368
386
|
askReuseExistingKey,
|
|
369
387
|
pickAuthMethod,
|
|
370
388
|
askApiKey,
|
|
389
|
+
validateApiKey,
|
|
371
390
|
pickVisionProvider,
|
|
372
391
|
pickVisionModel,
|
|
373
392
|
pickChannel,
|
|
@@ -502,12 +521,18 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
|
|
|
502
521
|
}
|
|
503
522
|
|
|
504
523
|
case 'enter-api-key': {
|
|
505
|
-
const
|
|
524
|
+
const providerId = state.providerId!
|
|
525
|
+
const provider = KNOWN_PROVIDERS[providerId]
|
|
506
526
|
const result = onResult(step, await prompts.askApiKey(provider))
|
|
507
527
|
if (result.kind === 'back') {
|
|
508
528
|
step = 'pick-auth-method'
|
|
509
529
|
break
|
|
510
530
|
}
|
|
531
|
+
const verdict = await runApiKeyValidation(prompts, providerId, result.value)
|
|
532
|
+
if (verdict === 'retry') {
|
|
533
|
+
step = 'enter-api-key'
|
|
534
|
+
break
|
|
535
|
+
}
|
|
511
536
|
state.llmAuth = { kind: 'api-key', apiKey: result.value }
|
|
512
537
|
step = stepAfterDefaultAuth(state)
|
|
513
538
|
break
|
|
@@ -608,12 +633,18 @@ export async function collectWizardInputs(cwd: string, prompts: WizardPrompts):
|
|
|
608
633
|
}
|
|
609
634
|
|
|
610
635
|
case 'enter-vision-api-key': {
|
|
611
|
-
const
|
|
636
|
+
const providerId = state.visionProviderId!
|
|
637
|
+
const provider = KNOWN_PROVIDERS[providerId]
|
|
612
638
|
const result = onResult(step, await prompts.askApiKey(provider))
|
|
613
639
|
if (result.kind === 'back') {
|
|
614
640
|
step = 'pick-vision-auth-method'
|
|
615
641
|
break
|
|
616
642
|
}
|
|
643
|
+
const verdict = await runApiKeyValidation(prompts, providerId, result.value)
|
|
644
|
+
if (verdict === 'retry') {
|
|
645
|
+
step = 'enter-vision-api-key'
|
|
646
|
+
break
|
|
647
|
+
}
|
|
617
648
|
state.visionLlmAuth = { kind: 'api-key', apiKey: result.value }
|
|
618
649
|
step = 'pick-channel'
|
|
619
650
|
break
|
|
@@ -771,12 +802,12 @@ async function pickModelForProvider(
|
|
|
771
802
|
providerId: KnownProviderId,
|
|
772
803
|
initial: KnownModelRef | undefined,
|
|
773
804
|
): Promise<StepResult<ModelOption>> {
|
|
774
|
-
const candidates = options.filter((o) => o.providerId === providerId)
|
|
805
|
+
const candidates = sortRecommendedFirst(options.filter((o) => o.providerId === providerId))
|
|
775
806
|
const choice = await select<KnownModelRef>({
|
|
776
807
|
message: `Pick a ${KNOWN_PROVIDERS[providerId].name} model`,
|
|
777
808
|
options: candidates.map((o) => ({
|
|
778
809
|
value: o.ref,
|
|
779
|
-
label: o
|
|
810
|
+
label: formatModelLabel(o),
|
|
780
811
|
hint: formatModelHint(o),
|
|
781
812
|
})),
|
|
782
813
|
initialValue: initial ?? candidates[0]?.ref,
|
|
@@ -863,12 +894,12 @@ async function pickVisionModel(
|
|
|
863
894
|
providerId: KnownProviderId,
|
|
864
895
|
initial: KnownModelRef | undefined,
|
|
865
896
|
): Promise<StepResult<ModelOption>> {
|
|
866
|
-
const candidates = options.filter((o) => o.providerId === providerId)
|
|
897
|
+
const candidates = sortRecommendedFirst(options.filter((o) => o.providerId === providerId))
|
|
867
898
|
const choice = await select<KnownModelRef>({
|
|
868
899
|
message: `Pick a vision-capable ${KNOWN_PROVIDERS[providerId].name} model`,
|
|
869
900
|
options: candidates.map((o) => ({
|
|
870
901
|
value: o.ref,
|
|
871
|
-
label: o
|
|
902
|
+
label: formatModelLabel(o),
|
|
872
903
|
hint: formatModelHint(o),
|
|
873
904
|
})),
|
|
874
905
|
initialValue: initial ?? candidates[0]?.ref,
|
|
@@ -879,7 +910,60 @@ async function pickVisionModel(
|
|
|
879
910
|
return value(picked)
|
|
880
911
|
}
|
|
881
912
|
|
|
913
|
+
async function runApiKeyValidation(
|
|
914
|
+
prompts: WizardPrompts,
|
|
915
|
+
providerId: KnownProviderId,
|
|
916
|
+
key: string,
|
|
917
|
+
): Promise<'accepted' | 'retry'> {
|
|
918
|
+
const provider = KNOWN_PROVIDERS[providerId]
|
|
919
|
+
const s = spinner()
|
|
920
|
+
s.start(`Checking your ${provider.name} key...`)
|
|
921
|
+
let result: KeyValidationResult
|
|
922
|
+
try {
|
|
923
|
+
result = await prompts.validateApiKey(providerId, key)
|
|
924
|
+
} catch {
|
|
925
|
+
s.stop(`Couldn't reach ${provider.name} to verify the key. Saving it anyway.`)
|
|
926
|
+
return 'accepted'
|
|
927
|
+
}
|
|
928
|
+
if (result.kind === 'ok') {
|
|
929
|
+
s.stop(`${provider.name} key looks good.`)
|
|
930
|
+
return 'accepted'
|
|
931
|
+
}
|
|
932
|
+
if (result.kind === 'skipped') {
|
|
933
|
+
s.stop(`Couldn't reach ${provider.name} to verify the key. Saving it anyway.`)
|
|
934
|
+
return 'accepted'
|
|
935
|
+
}
|
|
936
|
+
s.error(`${provider.name} rejected the key (HTTP ${result.status}).`)
|
|
937
|
+
const dashboardUrl = API_KEY_DASHBOARD_URL[providerId]
|
|
938
|
+
const lines = [
|
|
939
|
+
'The provider says this key is not valid.',
|
|
940
|
+
'Common causes: typo, expired key, wrong account, or pasting a project-scoped key.',
|
|
941
|
+
]
|
|
942
|
+
if (dashboardUrl) {
|
|
943
|
+
lines.push('', `Get a fresh one at ${dashboardUrl}`)
|
|
944
|
+
}
|
|
945
|
+
note(lines.join('\n'), `${provider.name} key rejected`)
|
|
946
|
+
const choice = await select<'retry' | 'accept'>({
|
|
947
|
+
message: 'What do you want to do?',
|
|
948
|
+
options: [
|
|
949
|
+
{ value: 'retry', label: 'Try a different key' },
|
|
950
|
+
{ value: 'accept', label: 'Save this key anyway', hint: 'init continues, but the agent may fail to start' },
|
|
951
|
+
],
|
|
952
|
+
initialValue: 'retry',
|
|
953
|
+
})
|
|
954
|
+
if (isCancel(choice) || choice === 'retry') return 'retry'
|
|
955
|
+
return 'accepted'
|
|
956
|
+
}
|
|
957
|
+
|
|
882
958
|
async function askApiKey(provider: (typeof KNOWN_PROVIDERS)[KnownProviderId]): Promise<StepResult<string>> {
|
|
959
|
+
const providerId = provider.id as KnownProviderId
|
|
960
|
+
const dashboardUrl = API_KEY_DASHBOARD_URL[providerId]
|
|
961
|
+
if (dashboardUrl) {
|
|
962
|
+
note(
|
|
963
|
+
[`Don't have a key yet?`, `Get one at ${dashboardUrl}`, `Then come back and paste it below.`].join('\n'),
|
|
964
|
+
`Get a ${provider.name} API key`,
|
|
965
|
+
)
|
|
966
|
+
}
|
|
883
967
|
const apiKey = await password({
|
|
884
968
|
message: `Put your ${provider.name} API key (will be saved to secrets.json)`,
|
|
885
969
|
validate: (v) => (v && v.length > 0 ? undefined : 'API key is required'),
|
|
@@ -1371,6 +1455,30 @@ function uniqueProviders(options: ModelOption[]): KnownProviderId[] {
|
|
|
1371
1455
|
return out
|
|
1372
1456
|
}
|
|
1373
1457
|
|
|
1458
|
+
// Per-provider recommended model refs. Surfaces a "(Recommended)" suffix in
|
|
1459
|
+
// the picker label and floats the entry to the top of the list (which also
|
|
1460
|
+
// makes it the default `initialValue` when the caller has no prior choice).
|
|
1461
|
+
// Kept narrow on purpose: one recommendation per provider. gpt-5.4-mini is
|
|
1462
|
+
// listed under both `openai` and `openai-codex` because the same model is
|
|
1463
|
+
// the right default whether the user authenticates with an API key or with
|
|
1464
|
+
// a ChatGPT Plus/Pro subscription. claude-sonnet-4-6 follows Anthropic's
|
|
1465
|
+
// own current-tier guidance (see the model lineup notes in providers.ts).
|
|
1466
|
+
const RECOMMENDED_MODEL_REFS: ReadonlySet<KnownModelRef> = new Set<KnownModelRef>([
|
|
1467
|
+
'openai/gpt-5.4-mini',
|
|
1468
|
+
'openai-codex/gpt-5.4-mini',
|
|
1469
|
+
'anthropic/claude-sonnet-4-6',
|
|
1470
|
+
])
|
|
1471
|
+
|
|
1472
|
+
export function formatModelLabel(o: ModelOption): string {
|
|
1473
|
+
return RECOMMENDED_MODEL_REFS.has(o.ref) ? `${o.modelName} (Recommended)` : o.modelName
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
export function sortRecommendedFirst(options: ModelOption[]): ModelOption[] {
|
|
1477
|
+
const recommended = options.filter((o) => RECOMMENDED_MODEL_REFS.has(o.ref))
|
|
1478
|
+
const rest = options.filter((o) => !RECOMMENDED_MODEL_REFS.has(o.ref))
|
|
1479
|
+
return [...recommended, ...rest]
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1374
1482
|
function formatModelHint(o: ModelOption): string {
|
|
1375
1483
|
const parts: string[] = []
|
|
1376
1484
|
if (o.contextWindow !== null) parts.push(`${(o.contextWindow / 1000).toFixed(0)}K ctx`)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { defineCommand } from 'citty'
|
|
2
|
+
|
|
3
|
+
import { requireContainerRunning, resolveHostPort, resolveTuiToken } from '@/container'
|
|
4
|
+
import { findAgentDir } from '@/init'
|
|
5
|
+
import { runInspect, streamLive, type LiveSourceFactory, type SessionSummary } from '@/inspect'
|
|
6
|
+
import { originLabel, shortSessionId } from '@/inspect/label'
|
|
7
|
+
|
|
8
|
+
import { cancel, c, errorLine, isCancel } from './ui'
|
|
9
|
+
|
|
10
|
+
export const inspectCommand = defineCommand({
|
|
11
|
+
meta: {
|
|
12
|
+
name: 'inspect',
|
|
13
|
+
description: 'replay a session transcript and tail live activity (host stage)',
|
|
14
|
+
},
|
|
15
|
+
args: {
|
|
16
|
+
session: {
|
|
17
|
+
type: 'positional',
|
|
18
|
+
description: 'session id or short prefix (omit to pick from a list)',
|
|
19
|
+
required: false,
|
|
20
|
+
},
|
|
21
|
+
filter: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description:
|
|
24
|
+
'category filter: comma-separated meta/user/assistant/tool/error/done/broadcast/cron-fire; prefix with ! to exclude',
|
|
25
|
+
},
|
|
26
|
+
since: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'only events newer than this (forms: 30s, 5m, 1h, 7d)',
|
|
29
|
+
},
|
|
30
|
+
json: {
|
|
31
|
+
type: 'boolean',
|
|
32
|
+
description: 'emit one JSON event per line; requires an explicit session id',
|
|
33
|
+
default: false,
|
|
34
|
+
},
|
|
35
|
+
follow: {
|
|
36
|
+
type: 'boolean',
|
|
37
|
+
description:
|
|
38
|
+
'tail live activity after replay (default: true when the container is running); pass --no-follow to replay-then-exit',
|
|
39
|
+
default: true,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
async run({ args }) {
|
|
43
|
+
const cwd = findAgentDir(process.cwd()) ?? process.cwd()
|
|
44
|
+
const color = useColor()
|
|
45
|
+
const sessionArg = typeof args.session === 'string' ? args.session : undefined
|
|
46
|
+
const filterArg = typeof args.filter === 'string' ? args.filter : undefined
|
|
47
|
+
const sinceArg = typeof args.since === 'string' ? args.since : undefined
|
|
48
|
+
const follow = args.follow !== false
|
|
49
|
+
|
|
50
|
+
const isJson = args.json === true
|
|
51
|
+
const liveSource = !follow || isJson ? undefined : await buildLiveSource(cwd)
|
|
52
|
+
const signal = installSigintAbort()
|
|
53
|
+
|
|
54
|
+
const result = await runInspect({
|
|
55
|
+
agentDir: cwd,
|
|
56
|
+
...(sessionArg !== undefined ? { sessionIdOrPrefix: sessionArg } : {}),
|
|
57
|
+
...(filterArg !== undefined ? { filter: filterArg } : {}),
|
|
58
|
+
...(sinceArg !== undefined ? { since: sinceArg } : {}),
|
|
59
|
+
json: isJson,
|
|
60
|
+
color,
|
|
61
|
+
selectSession: clackSelect,
|
|
62
|
+
...(liveSource !== undefined ? { liveSource } : {}),
|
|
63
|
+
signal,
|
|
64
|
+
stdout: (line) => process.stdout.write(`${line}\n`),
|
|
65
|
+
stderr: (line) => process.stderr.write(`${line}\n`),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
if (!result.ok) {
|
|
69
|
+
process.stderr.write(`${errorLine(result.reason)}\n`)
|
|
70
|
+
process.exit(result.exitCode)
|
|
71
|
+
}
|
|
72
|
+
process.exit(result.exitCode)
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
async function buildLiveSource(cwd: string): Promise<LiveSourceFactory | undefined> {
|
|
77
|
+
const precheck = await requireContainerRunning({ cwd })
|
|
78
|
+
if (!precheck.ok) {
|
|
79
|
+
process.stderr.write(`${c.yellow('⚠')} ${precheck.reason}; tailing live events disabled\n`)
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
const port = await resolveHostPort({ cwd })
|
|
83
|
+
const token = await resolveTuiToken({ cwd })
|
|
84
|
+
const baseUrl = new URL(`ws://127.0.0.1:${port}/inspect`)
|
|
85
|
+
if (token !== null) baseUrl.searchParams.set('token', token)
|
|
86
|
+
const url = baseUrl.toString()
|
|
87
|
+
return ({ sessionId, sinceMs, signal, onSubscribed }) =>
|
|
88
|
+
streamLive({
|
|
89
|
+
url,
|
|
90
|
+
sessionId,
|
|
91
|
+
...(sinceMs !== undefined ? { sinceMs } : {}),
|
|
92
|
+
...(signal !== undefined ? { signal } : {}),
|
|
93
|
+
...(onSubscribed !== undefined ? { onSubscribed } : {}),
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function installSigintAbort(): AbortSignal {
|
|
98
|
+
const ctrl = new AbortController()
|
|
99
|
+
const onSig = (): void => {
|
|
100
|
+
ctrl.abort()
|
|
101
|
+
}
|
|
102
|
+
process.once('SIGINT', onSig)
|
|
103
|
+
process.once('SIGTERM', onSig)
|
|
104
|
+
return ctrl.signal
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function useColor(): boolean {
|
|
108
|
+
if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') return false
|
|
109
|
+
if (process.env.FORCE_COLOR === '0') return false
|
|
110
|
+
if (process.env.FORCE_COLOR) return true
|
|
111
|
+
return Boolean(process.stdout.isTTY)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function clackSelect(sessions: SessionSummary[]): Promise<SessionSummary | null> {
|
|
115
|
+
const { select } = await import('@clack/prompts')
|
|
116
|
+
const picked = await select<string>({
|
|
117
|
+
message: `Pick a session to inspect (showing ${sessions.length})`,
|
|
118
|
+
options: sessions.map((s) => ({
|
|
119
|
+
value: s.sessionId,
|
|
120
|
+
label: formatRowLabel(s),
|
|
121
|
+
...(s.firstPrompt !== null ? { hint: truncate(s.firstPrompt, 60) } : { hint: '(no prompt)' }),
|
|
122
|
+
})),
|
|
123
|
+
initialValue: sessions[0]?.sessionId,
|
|
124
|
+
})
|
|
125
|
+
if (isCancel(picked)) {
|
|
126
|
+
cancel('Cancelled.')
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
return sessions.find((s) => s.sessionId === picked) ?? null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function formatRowLabel(s: SessionSummary): string {
|
|
133
|
+
const id = shortSessionId(s.sessionId)
|
|
134
|
+
const label = s.origin === null ? '(unknown origin)' : originLabel(s.origin)
|
|
135
|
+
const when = formatRelative(s.mtimeMs)
|
|
136
|
+
return `${c.cyan(id)} ${label} ${c.dim(when)}`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function formatRelative(ms: number): string {
|
|
140
|
+
const diff = Date.now() - ms
|
|
141
|
+
if (diff < 60_000) return 'just now'
|
|
142
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
|
|
143
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
|
|
144
|
+
return `${Math.floor(diff / 86_400_000)}d ago`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function truncate(text: string, max: number): string {
|
|
148
|
+
const oneline = text.replace(/\s+/g, ' ').trim()
|
|
149
|
+
if (oneline.length <= max) return oneline
|
|
150
|
+
return `${oneline.slice(0, max)}…`
|
|
151
|
+
}
|
package/src/cron/consumer.ts
CHANGED
|
@@ -195,7 +195,7 @@ async function runPromptOnce(
|
|
|
195
195
|
}
|
|
196
196
|
: undefined
|
|
197
197
|
if (created.hooks && turnEvent !== undefined) {
|
|
198
|
-
await created.hooks.runSessionTurnStart(turnEvent)
|
|
198
|
+
await created.hooks.runSessionTurnStart({ ...turnEvent, userPrompt: job.prompt })
|
|
199
199
|
}
|
|
200
200
|
// Bridge the CronSession wrapper into the AgentSession surface the
|
|
201
201
|
// fallback helper expects:
|