typeclaw 0.1.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/LICENSE +21 -0
- package/README.md +134 -0
- package/auth.schema.json +63 -0
- package/cron.schema.json +96 -0
- package/package.json +72 -0
- package/scripts/emit-base-dockerfile.ts +5 -0
- package/scripts/generate-schema.ts +34 -0
- package/secrets.schema.json +63 -0
- package/src/agent/auth.ts +119 -0
- package/src/agent/compaction.ts +35 -0
- package/src/agent/git-nudge.ts +95 -0
- package/src/agent/index.ts +451 -0
- package/src/agent/plugin-tools.ts +269 -0
- package/src/agent/reload-tool.ts +71 -0
- package/src/agent/self.ts +45 -0
- package/src/agent/session-origin.ts +288 -0
- package/src/agent/subagents.ts +253 -0
- package/src/agent/system-prompt.ts +68 -0
- package/src/agent/tools/channel-fetch-attachment.ts +118 -0
- package/src/agent/tools/channel-history.ts +119 -0
- package/src/agent/tools/channel-reply.ts +182 -0
- package/src/agent/tools/channel-send.ts +212 -0
- package/src/agent/tools/ddg.ts +218 -0
- package/src/agent/tools/restart.ts +122 -0
- package/src/agent/tools/stream-snapshot.ts +181 -0
- package/src/agent/tools/webfetch/fetch.ts +102 -0
- package/src/agent/tools/webfetch/index.ts +1 -0
- package/src/agent/tools/webfetch/strategies/grep.ts +70 -0
- package/src/agent/tools/webfetch/strategies/jq.ts +31 -0
- package/src/agent/tools/webfetch/strategies/raw.ts +3 -0
- package/src/agent/tools/webfetch/strategies/readability.ts +30 -0
- package/src/agent/tools/webfetch/strategies/selector.ts +41 -0
- package/src/agent/tools/webfetch/strategies/snapshot.ts +135 -0
- package/src/agent/tools/webfetch/tool.ts +281 -0
- package/src/agent/tools/webfetch/types.ts +33 -0
- package/src/agent/tools/websearch.ts +96 -0
- package/src/agent/tools/wikipedia.ts +52 -0
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +170 -0
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +421 -0
- package/src/bundled-plugins/agent-browser/index.ts +179 -0
- package/src/bundled-plugins/agent-browser/shim-install.ts +158 -0
- package/src/bundled-plugins/agent-browser/shim.ts +152 -0
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +113 -0
- package/src/bundled-plugins/guard/index.ts +26 -0
- package/src/bundled-plugins/guard/policies/non-workspace-write.ts +98 -0
- package/src/bundled-plugins/guard/policies/skill-authoring.ts +185 -0
- package/src/bundled-plugins/guard/policies/uncommitted-changes.ts +85 -0
- package/src/bundled-plugins/guard/policy.ts +18 -0
- package/src/bundled-plugins/memory/README.md +71 -0
- package/src/bundled-plugins/memory/append-tool.ts +84 -0
- package/src/bundled-plugins/memory/dreaming-state.ts +86 -0
- package/src/bundled-plugins/memory/dreaming.ts +470 -0
- package/src/bundled-plugins/memory/fragment-parser.ts +67 -0
- package/src/bundled-plugins/memory/index.ts +238 -0
- package/src/bundled-plugins/memory/load-memory.ts +122 -0
- package/src/bundled-plugins/memory/memory-logger.ts +257 -0
- package/src/bundled-plugins/memory/secret-detector.ts +49 -0
- package/src/bundled-plugins/memory/watermark.ts +15 -0
- package/src/bundled-plugins/security/index.ts +35 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +120 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +167 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +488 -0
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +99 -0
- package/src/bundled-plugins/security/policies/secret-exfil-read.ts +127 -0
- package/src/bundled-plugins/security/policies/session-search-secrets.ts +86 -0
- package/src/bundled-plugins/security/policies/ssrf.ts +196 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +81 -0
- package/src/bundled-plugins/security/policy.ts +9 -0
- package/src/channels/adapters/discord-bot-channel-resolver.ts +77 -0
- package/src/channels/adapters/discord-bot-classify.ts +148 -0
- package/src/channels/adapters/discord-bot.ts +640 -0
- package/src/channels/adapters/kakaotalk-author-resolver.ts +78 -0
- package/src/channels/adapters/kakaotalk-channel-resolver.ts +105 -0
- package/src/channels/adapters/kakaotalk-classify.ts +77 -0
- package/src/channels/adapters/kakaotalk.ts +622 -0
- package/src/channels/adapters/slack-bot-author-resolver.ts +80 -0
- package/src/channels/adapters/slack-bot-channel-resolver.ts +84 -0
- package/src/channels/adapters/slack-bot-classify.ts +213 -0
- package/src/channels/adapters/slack-bot-dedupe.ts +51 -0
- package/src/channels/adapters/slack-bot-time.ts +10 -0
- package/src/channels/adapters/slack-bot.ts +881 -0
- package/src/channels/adapters/telegram-bot-classify.ts +155 -0
- package/src/channels/adapters/telegram-bot-format.ts +309 -0
- package/src/channels/adapters/telegram-bot.ts +604 -0
- package/src/channels/engagement.ts +227 -0
- package/src/channels/index.ts +21 -0
- package/src/channels/manager.ts +292 -0
- package/src/channels/membership-cache.ts +116 -0
- package/src/channels/membership-from-history.ts +53 -0
- package/src/channels/membership.ts +30 -0
- package/src/channels/participants.ts +47 -0
- package/src/channels/persistence.ts +209 -0
- package/src/channels/reloadable.ts +28 -0
- package/src/channels/router.ts +1570 -0
- package/src/channels/schema.ts +273 -0
- package/src/channels/types.ts +160 -0
- package/src/cli/channel.ts +403 -0
- package/src/cli/compose-status.ts +95 -0
- package/src/cli/compose.ts +240 -0
- package/src/cli/hostd.ts +163 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/init.ts +592 -0
- package/src/cli/logs.ts +38 -0
- package/src/cli/reload.ts +68 -0
- package/src/cli/restart.ts +66 -0
- package/src/cli/run.ts +77 -0
- package/src/cli/shell.ts +33 -0
- package/src/cli/start.ts +57 -0
- package/src/cli/status.ts +178 -0
- package/src/cli/stop.ts +31 -0
- package/src/cli/tui.ts +35 -0
- package/src/cli/ui.ts +110 -0
- package/src/commands/index.ts +74 -0
- package/src/compose/discover.ts +43 -0
- package/src/compose/index.ts +25 -0
- package/src/compose/logs.ts +162 -0
- package/src/compose/restart.ts +69 -0
- package/src/compose/start.ts +62 -0
- package/src/compose/status.ts +28 -0
- package/src/compose/stop.ts +43 -0
- package/src/config/config.ts +424 -0
- package/src/config/index.ts +25 -0
- package/src/config/providers.ts +234 -0
- package/src/config/reloadable.ts +47 -0
- package/src/container/index.ts +27 -0
- package/src/container/logs.ts +37 -0
- package/src/container/port.ts +137 -0
- package/src/container/shared.ts +290 -0
- package/src/container/shell.ts +58 -0
- package/src/container/start.ts +670 -0
- package/src/container/status.ts +76 -0
- package/src/container/stop.ts +120 -0
- package/src/container/verify-running.ts +149 -0
- package/src/cron/consumer.ts +138 -0
- package/src/cron/index.ts +54 -0
- package/src/cron/reloadable.ts +64 -0
- package/src/cron/scheduler.ts +200 -0
- package/src/cron/schema.ts +96 -0
- package/src/hostd/client.ts +113 -0
- package/src/hostd/daemon.ts +587 -0
- package/src/hostd/index.ts +25 -0
- package/src/hostd/paths.ts +82 -0
- package/src/hostd/portbroker-manager.ts +101 -0
- package/src/hostd/protocol.ts +48 -0
- package/src/hostd/spawn.ts +224 -0
- package/src/hostd/supervisor.ts +60 -0
- package/src/hostd/tailscale.ts +172 -0
- package/src/hostd/version.ts +115 -0
- package/src/init/dockerfile.ts +327 -0
- package/src/init/ensure-deps.ts +152 -0
- package/src/init/gitignore.ts +46 -0
- package/src/init/hatching.ts +60 -0
- package/src/init/index.ts +786 -0
- package/src/init/kakaotalk-auth.ts +114 -0
- package/src/init/models-dev.ts +130 -0
- package/src/init/oauth-login.ts +74 -0
- package/src/init/packagejson.ts +94 -0
- package/src/init/paths.ts +2 -0
- package/src/init/run-bun-install.ts +20 -0
- package/src/markdown/chunk.ts +299 -0
- package/src/markdown/index.ts +1 -0
- package/src/plugin/context.ts +40 -0
- package/src/plugin/define.ts +35 -0
- package/src/plugin/hooks.ts +204 -0
- package/src/plugin/index.ts +63 -0
- package/src/plugin/loader.ts +111 -0
- package/src/plugin/manager.ts +136 -0
- package/src/plugin/registry.ts +145 -0
- package/src/plugin/skills.ts +62 -0
- package/src/plugin/types.ts +172 -0
- package/src/portbroker/bind-with-forward.ts +102 -0
- package/src/portbroker/container-server.ts +305 -0
- package/src/portbroker/forward-result-bus.ts +36 -0
- package/src/portbroker/hostd-client.ts +443 -0
- package/src/portbroker/index.ts +33 -0
- package/src/portbroker/policy.ts +24 -0
- package/src/portbroker/proc-net-tcp.ts +72 -0
- package/src/portbroker/protocol.ts +39 -0
- package/src/reload/client.ts +59 -0
- package/src/reload/index.ts +3 -0
- package/src/reload/registry.ts +60 -0
- package/src/reload/types.ts +13 -0
- package/src/run/bundled-plugins.ts +24 -0
- package/src/run/channel-session-factory.ts +105 -0
- package/src/run/index.ts +432 -0
- package/src/run/plugin-runtime.ts +43 -0
- package/src/run/schema-with-plugins.ts +14 -0
- package/src/secrets/index.ts +13 -0
- package/src/secrets/migrate.ts +95 -0
- package/src/secrets/schema.ts +75 -0
- package/src/secrets/storage.ts +231 -0
- package/src/server/index.ts +436 -0
- package/src/sessions/index.ts +23 -0
- package/src/shared/index.ts +9 -0
- package/src/shared/local-time.ts +21 -0
- package/src/shared/protocol.ts +25 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +87 -0
- package/src/skills/typeclaw-channel-telegram-bot/SKILL.md +64 -0
- package/src/skills/typeclaw-config/SKILL.md +643 -0
- package/src/skills/typeclaw-cron/SKILL.md +159 -0
- package/src/skills/typeclaw-git/SKILL.md +89 -0
- package/src/skills/typeclaw-memory/SKILL.md +174 -0
- package/src/skills/typeclaw-monorepo/SKILL.md +175 -0
- package/src/skills/typeclaw-plugins/SKILL.md +594 -0
- package/src/skills/typeclaw-skills/SKILL.md +246 -0
- package/src/stream/broker.ts +161 -0
- package/src/stream/index.ts +16 -0
- package/src/stream/types.ts +69 -0
- package/src/tui/client.ts +45 -0
- package/src/tui/format.ts +317 -0
- package/src/tui/index.ts +225 -0
- package/src/tui/theme.ts +41 -0
- package/typeclaw.schema.json +826 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { CronExpressionParser } from 'cron-parser'
|
|
2
|
+
|
|
3
|
+
import type { CronJob } from './schema'
|
|
4
|
+
|
|
5
|
+
export type SchedulerClock = {
|
|
6
|
+
now: () => number
|
|
7
|
+
setTimeout: (cb: () => void, ms: number) => number
|
|
8
|
+
clearTimeout: (handle: number) => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type SchedulerLogger = {
|
|
12
|
+
info: (msg: string) => void
|
|
13
|
+
warn: (msg: string) => void
|
|
14
|
+
error: (msg: string) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type CreateSchedulerOptions = {
|
|
18
|
+
jobs: CronJob[]
|
|
19
|
+
onFire: (job: CronJob) => void
|
|
20
|
+
clock?: SchedulerClock
|
|
21
|
+
logger?: SchedulerLogger
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type JobDiff = {
|
|
25
|
+
added: CronJob[]
|
|
26
|
+
removed: CronJob[]
|
|
27
|
+
updated: CronJob[]
|
|
28
|
+
unchanged: CronJob[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type Scheduler = {
|
|
32
|
+
start: () => void
|
|
33
|
+
stop: () => void
|
|
34
|
+
replaceJobs: (jobs: CronJob[]) => JobDiff
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const realClock: SchedulerClock = {
|
|
38
|
+
now: () => Date.now(),
|
|
39
|
+
setTimeout: (cb, ms) => setTimeout(cb, ms) as unknown as number,
|
|
40
|
+
clearTimeout: (handle) => clearTimeout(handle as unknown as ReturnType<typeof setTimeout>),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const consoleLogger: SchedulerLogger = {
|
|
44
|
+
info: (m) => console.log(m),
|
|
45
|
+
warn: (m) => console.warn(m),
|
|
46
|
+
error: (m) => console.error(m),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createScheduler({
|
|
50
|
+
jobs,
|
|
51
|
+
onFire,
|
|
52
|
+
clock = realClock,
|
|
53
|
+
logger = consoleLogger,
|
|
54
|
+
}: CreateSchedulerOptions): Scheduler {
|
|
55
|
+
const registry = new Map<string, CronJob>()
|
|
56
|
+
for (const job of jobs) registry.set(job.id, job)
|
|
57
|
+
|
|
58
|
+
const handles = new Map<string, number>()
|
|
59
|
+
let started = false
|
|
60
|
+
|
|
61
|
+
function currentEnabled(id: string): CronJob | null {
|
|
62
|
+
const job = registry.get(id)
|
|
63
|
+
if (!job || !job.enabled) return null
|
|
64
|
+
return job
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function scheduleNext(id: string): void {
|
|
68
|
+
if (!started) return
|
|
69
|
+
const job = currentEnabled(id)
|
|
70
|
+
if (!job) return
|
|
71
|
+
|
|
72
|
+
const result = computeNextFire(job, clock.now())
|
|
73
|
+
if (!result.ok) {
|
|
74
|
+
logger.warn(`[cron] ${id} not scheduled: invalid schedule "${job.schedule}"${tzSuffix(job)}: ${result.reason}`)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
cancel(id)
|
|
79
|
+
|
|
80
|
+
const delay = Math.max(0, result.nextFire - clock.now())
|
|
81
|
+
const handle = clock.setTimeout(() => {
|
|
82
|
+
handles.delete(id)
|
|
83
|
+
if (!started) return
|
|
84
|
+
const live = currentEnabled(id)
|
|
85
|
+
if (!live) return
|
|
86
|
+
fire(live)
|
|
87
|
+
scheduleNext(id)
|
|
88
|
+
}, delay)
|
|
89
|
+
handles.set(id, handle)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function fire(job: CronJob): void {
|
|
93
|
+
logger.info(`[cron] firing ${job.kind} ${job.id}`)
|
|
94
|
+
try {
|
|
95
|
+
onFire(job)
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
98
|
+
logger.error(`[cron] ${job.id} onFire threw synchronously: ${message}`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cancel(id: string): void {
|
|
103
|
+
const handle = handles.get(id)
|
|
104
|
+
if (handle === undefined) return
|
|
105
|
+
clock.clearTimeout(handle)
|
|
106
|
+
handles.delete(id)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function diff(next: CronJob[]): JobDiff {
|
|
110
|
+
const added: CronJob[] = []
|
|
111
|
+
const removed: CronJob[] = []
|
|
112
|
+
const updated: CronJob[] = []
|
|
113
|
+
const unchanged: CronJob[] = []
|
|
114
|
+
|
|
115
|
+
const nextById = new Map<string, CronJob>()
|
|
116
|
+
for (const job of next) nextById.set(job.id, job)
|
|
117
|
+
|
|
118
|
+
for (const [id, before] of registry) {
|
|
119
|
+
const after = nextById.get(id)
|
|
120
|
+
if (!after) {
|
|
121
|
+
removed.push(before)
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
if (jobFingerprint(before) === jobFingerprint(after)) {
|
|
125
|
+
unchanged.push(after)
|
|
126
|
+
} else {
|
|
127
|
+
updated.push(after)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const [id, after] of nextById) {
|
|
131
|
+
if (!registry.has(id)) added.push(after)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { added, removed, updated, unchanged }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
start() {
|
|
139
|
+
if (started) return
|
|
140
|
+
started = true
|
|
141
|
+
for (const id of registry.keys()) scheduleNext(id)
|
|
142
|
+
},
|
|
143
|
+
stop() {
|
|
144
|
+
started = false
|
|
145
|
+
for (const handle of handles.values()) clock.clearTimeout(handle)
|
|
146
|
+
handles.clear()
|
|
147
|
+
},
|
|
148
|
+
replaceJobs(next) {
|
|
149
|
+
const result = diff(next)
|
|
150
|
+
|
|
151
|
+
const newRegistry = new Map<string, CronJob>()
|
|
152
|
+
for (const job of next) newRegistry.set(job.id, job)
|
|
153
|
+
registry.clear()
|
|
154
|
+
for (const [id, job] of newRegistry) registry.set(id, job)
|
|
155
|
+
|
|
156
|
+
for (const job of result.removed) cancel(job.id)
|
|
157
|
+
for (const job of result.updated) {
|
|
158
|
+
cancel(job.id)
|
|
159
|
+
scheduleNext(job.id)
|
|
160
|
+
}
|
|
161
|
+
for (const job of result.added) scheduleNext(job.id)
|
|
162
|
+
|
|
163
|
+
return result
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function jobFingerprint(job: CronJob): string {
|
|
169
|
+
return JSON.stringify({
|
|
170
|
+
schedule: job.schedule,
|
|
171
|
+
enabled: job.enabled,
|
|
172
|
+
timezone: job.timezone ?? null,
|
|
173
|
+
kind: job.kind,
|
|
174
|
+
payload: jobPayload(job),
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function jobPayload(job: CronJob): unknown {
|
|
179
|
+
if (job.kind === 'prompt') return { prompt: job.prompt, subagent: job.subagent ?? null, payload: job.payload ?? null }
|
|
180
|
+
return job.command
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
type ComputeNextFireResult = { ok: true; nextFire: number } | { ok: false; reason: string }
|
|
184
|
+
|
|
185
|
+
function computeNextFire(job: CronJob, now: number): ComputeNextFireResult {
|
|
186
|
+
try {
|
|
187
|
+
const expr = CronExpressionParser.parse(job.schedule, {
|
|
188
|
+
currentDate: new Date(now),
|
|
189
|
+
...(job.timezone ? { tz: job.timezone } : {}),
|
|
190
|
+
})
|
|
191
|
+
return { ok: true, nextFire: expr.next().getTime() }
|
|
192
|
+
} catch (err) {
|
|
193
|
+
const reason = err instanceof Error ? err.message : String(err)
|
|
194
|
+
return { ok: false, reason }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function tzSuffix(job: CronJob): string {
|
|
199
|
+
return job.timezone ? ` (timezone "${job.timezone}")` : ''
|
|
200
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { CronExpressionParser } from 'cron-parser'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import type { SubagentRegistry } from '@/agent/subagents'
|
|
5
|
+
import { validateSubagentPayload } from '@/agent/subagents'
|
|
6
|
+
|
|
7
|
+
const idPattern = /^[a-zA-Z0-9_-]+$/
|
|
8
|
+
|
|
9
|
+
const baseJob = z.object({
|
|
10
|
+
id: z.string().min(1).regex(idPattern, 'id must contain only letters, digits, hyphens, or underscores'),
|
|
11
|
+
schedule: z.string().min(1),
|
|
12
|
+
enabled: z.boolean().default(true),
|
|
13
|
+
timezone: z.string().optional(),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const promptJob = baseJob.extend({
|
|
17
|
+
kind: z.literal('prompt'),
|
|
18
|
+
prompt: z.string().min(1),
|
|
19
|
+
subagent: z.string().min(1).optional(),
|
|
20
|
+
payload: z.unknown().optional(),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const execJob = baseJob.extend({
|
|
24
|
+
kind: z.literal('exec'),
|
|
25
|
+
command: z.array(z.string().min(1)).min(1),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const cronJobSchema = z.discriminatedUnion('kind', [promptJob, execJob])
|
|
29
|
+
|
|
30
|
+
export const cronFileSchema = z.object({
|
|
31
|
+
$schema: z.string().optional(),
|
|
32
|
+
jobs: z.array(cronJobSchema).default([]),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export type CronJob = z.infer<typeof cronJobSchema>
|
|
36
|
+
export type PromptJob = Extract<CronJob, { kind: 'prompt' }>
|
|
37
|
+
export type ExecJob = Extract<CronJob, { kind: 'exec' }>
|
|
38
|
+
export type CronFile = z.infer<typeof cronFileSchema>
|
|
39
|
+
|
|
40
|
+
export type ParseCronResult = { ok: true; file: CronFile } | { ok: false; reason: string }
|
|
41
|
+
|
|
42
|
+
export type ParseCronOptions = {
|
|
43
|
+
// When provided, prompt jobs with a `subagent` field are validated against
|
|
44
|
+
// the registry: the name must exist, and the optional `payload` must match
|
|
45
|
+
// the registered subagent's payloadSchema (or be absent if no schema).
|
|
46
|
+
subagents?: SubagentRegistry
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseCronFile(raw: unknown, options: ParseCronOptions = {}): ParseCronResult {
|
|
50
|
+
const parsed = cronFileSchema.safeParse(raw)
|
|
51
|
+
if (!parsed.success) {
|
|
52
|
+
return { ok: false, reason: parsed.error.issues.map(formatIssue).join('; ') }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const file = parsed.data
|
|
56
|
+
const seen = new Set<string>()
|
|
57
|
+
for (const job of file.jobs) {
|
|
58
|
+
if (seen.has(job.id)) {
|
|
59
|
+
return { ok: false, reason: `duplicate job id: ${job.id}` }
|
|
60
|
+
}
|
|
61
|
+
seen.add(job.id)
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const expr = CronExpressionParser.parse(job.schedule, job.timezone ? { tz: job.timezone } : undefined)
|
|
65
|
+
// cron-parser validates the timezone lazily on first next() call, not at
|
|
66
|
+
// parse time, so we must force evaluation here to catch bogus zones.
|
|
67
|
+
expr.next()
|
|
68
|
+
} catch (err) {
|
|
69
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
70
|
+
if (job.timezone && /invalid|unhandled timestamp|unrecognized/i.test(message)) {
|
|
71
|
+
return { ok: false, reason: `job ${job.id}: invalid timezone "${job.timezone}": ${message}` }
|
|
72
|
+
}
|
|
73
|
+
return { ok: false, reason: `job ${job.id}: invalid schedule "${job.schedule}": ${message}` }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (job.kind === 'prompt' && job.subagent !== undefined && options.subagents !== undefined) {
|
|
77
|
+
const subagent = options.subagents[job.subagent]
|
|
78
|
+
if (!subagent) {
|
|
79
|
+
return { ok: false, reason: `job ${job.id}: unknown subagent "${job.subagent}"` }
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
validateSubagentPayload(job.subagent, subagent, job.payload)
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
85
|
+
return { ok: false, reason: `job ${job.id}: ${message}` }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { ok: true, file }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatIssue(issue: { path: PropertyKey[]; message: string }): string {
|
|
94
|
+
const path = issue.path.length > 0 ? issue.path.map(String).join('.') : '<root>'
|
|
95
|
+
return `${path}: ${issue.message}`
|
|
96
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
import type { Socket } from 'bun'
|
|
4
|
+
|
|
5
|
+
import { socketPath } from './paths'
|
|
6
|
+
import type { Request, Response } from './protocol'
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 3_000
|
|
9
|
+
|
|
10
|
+
export async function isDaemonReachable(timeoutMs = DEFAULT_TIMEOUT_MS): Promise<boolean> {
|
|
11
|
+
if (!existsSync(socketPath())) return false
|
|
12
|
+
try {
|
|
13
|
+
const reply = await send({ kind: 'list' }, { timeoutMs })
|
|
14
|
+
return reply.ok
|
|
15
|
+
} catch {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type SendOptions = {
|
|
21
|
+
timeoutMs?: number
|
|
22
|
+
socket?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type SendHttpOptions = {
|
|
26
|
+
timeoutMs?: number
|
|
27
|
+
url: string
|
|
28
|
+
token: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function sendHttp(req: Request, opts: SendHttpOptions): Promise<Response> {
|
|
32
|
+
const controller = new AbortController()
|
|
33
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
34
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(new URL('/rpc', opts.url), {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: {
|
|
39
|
+
authorization: `Bearer ${opts.token}`,
|
|
40
|
+
'content-type': 'application/json',
|
|
41
|
+
},
|
|
42
|
+
body: JSON.stringify(req),
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
})
|
|
45
|
+
const parsed = (await res.json()) as Response
|
|
46
|
+
return parsed
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
49
|
+
return { ok: false, reason: `daemon ack timeout after ${timeoutMs}ms` }
|
|
50
|
+
}
|
|
51
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) }
|
|
52
|
+
} finally {
|
|
53
|
+
clearTimeout(timer)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function send(req: Request, opts: SendOptions = {}): Promise<Response> {
|
|
58
|
+
const path = opts.socket ?? socketPath()
|
|
59
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
60
|
+
|
|
61
|
+
type State = { buf: string; resolve: (r: Response) => void }
|
|
62
|
+
const state: State = {
|
|
63
|
+
buf: '',
|
|
64
|
+
resolve: () => {},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const replyPromise = new Promise<Response>((resolve) => {
|
|
68
|
+
state.resolve = resolve
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
let sock: Socket<State>
|
|
72
|
+
try {
|
|
73
|
+
sock = await Bun.connect<State>({
|
|
74
|
+
unix: path,
|
|
75
|
+
socket: {
|
|
76
|
+
data: (s, chunk) => {
|
|
77
|
+
s.data.buf += chunk.toString('utf8')
|
|
78
|
+
const newline = s.data.buf.indexOf('\n')
|
|
79
|
+
if (newline < 0) return
|
|
80
|
+
const line = s.data.buf.slice(0, newline)
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(line) as Response
|
|
83
|
+
s.data.resolve(parsed)
|
|
84
|
+
} catch {
|
|
85
|
+
s.data.resolve({ ok: false, reason: 'invalid response from daemon' })
|
|
86
|
+
}
|
|
87
|
+
s.end()
|
|
88
|
+
},
|
|
89
|
+
close: () => {},
|
|
90
|
+
error: () => {
|
|
91
|
+
state.resolve({ ok: false, reason: 'socket error' })
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) }
|
|
97
|
+
}
|
|
98
|
+
sock.data = state
|
|
99
|
+
sock.write(`${JSON.stringify(req)}\n`)
|
|
100
|
+
|
|
101
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
102
|
+
const timeoutPromise = new Promise<Response>((resolve) => {
|
|
103
|
+
timer = setTimeout(() => resolve({ ok: false, reason: `daemon ack timeout after ${timeoutMs}ms` }), timeoutMs)
|
|
104
|
+
})
|
|
105
|
+
try {
|
|
106
|
+
return await Promise.race([replyPromise, timeoutPromise])
|
|
107
|
+
} finally {
|
|
108
|
+
if (timer) clearTimeout(timer)
|
|
109
|
+
try {
|
|
110
|
+
sock.end()
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
}
|