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.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/auth.schema.json +63 -0
  4. package/cron.schema.json +96 -0
  5. package/package.json +72 -0
  6. package/scripts/emit-base-dockerfile.ts +5 -0
  7. package/scripts/generate-schema.ts +34 -0
  8. package/secrets.schema.json +63 -0
  9. package/src/agent/auth.ts +119 -0
  10. package/src/agent/compaction.ts +35 -0
  11. package/src/agent/git-nudge.ts +95 -0
  12. package/src/agent/index.ts +451 -0
  13. package/src/agent/plugin-tools.ts +269 -0
  14. package/src/agent/reload-tool.ts +71 -0
  15. package/src/agent/self.ts +45 -0
  16. package/src/agent/session-origin.ts +288 -0
  17. package/src/agent/subagents.ts +253 -0
  18. package/src/agent/system-prompt.ts +68 -0
  19. package/src/agent/tools/channel-fetch-attachment.ts +118 -0
  20. package/src/agent/tools/channel-history.ts +119 -0
  21. package/src/agent/tools/channel-reply.ts +182 -0
  22. package/src/agent/tools/channel-send.ts +212 -0
  23. package/src/agent/tools/ddg.ts +218 -0
  24. package/src/agent/tools/restart.ts +122 -0
  25. package/src/agent/tools/stream-snapshot.ts +181 -0
  26. package/src/agent/tools/webfetch/fetch.ts +102 -0
  27. package/src/agent/tools/webfetch/index.ts +1 -0
  28. package/src/agent/tools/webfetch/strategies/grep.ts +70 -0
  29. package/src/agent/tools/webfetch/strategies/jq.ts +31 -0
  30. package/src/agent/tools/webfetch/strategies/raw.ts +3 -0
  31. package/src/agent/tools/webfetch/strategies/readability.ts +30 -0
  32. package/src/agent/tools/webfetch/strategies/selector.ts +41 -0
  33. package/src/agent/tools/webfetch/strategies/snapshot.ts +135 -0
  34. package/src/agent/tools/webfetch/tool.ts +281 -0
  35. package/src/agent/tools/webfetch/types.ts +33 -0
  36. package/src/agent/tools/websearch.ts +96 -0
  37. package/src/agent/tools/wikipedia.ts +52 -0
  38. package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +170 -0
  39. package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +421 -0
  40. package/src/bundled-plugins/agent-browser/index.ts +179 -0
  41. package/src/bundled-plugins/agent-browser/shim-install.ts +158 -0
  42. package/src/bundled-plugins/agent-browser/shim.ts +152 -0
  43. package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +113 -0
  44. package/src/bundled-plugins/guard/index.ts +26 -0
  45. package/src/bundled-plugins/guard/policies/non-workspace-write.ts +98 -0
  46. package/src/bundled-plugins/guard/policies/skill-authoring.ts +185 -0
  47. package/src/bundled-plugins/guard/policies/uncommitted-changes.ts +85 -0
  48. package/src/bundled-plugins/guard/policy.ts +18 -0
  49. package/src/bundled-plugins/memory/README.md +71 -0
  50. package/src/bundled-plugins/memory/append-tool.ts +84 -0
  51. package/src/bundled-plugins/memory/dreaming-state.ts +86 -0
  52. package/src/bundled-plugins/memory/dreaming.ts +470 -0
  53. package/src/bundled-plugins/memory/fragment-parser.ts +67 -0
  54. package/src/bundled-plugins/memory/index.ts +238 -0
  55. package/src/bundled-plugins/memory/load-memory.ts +122 -0
  56. package/src/bundled-plugins/memory/memory-logger.ts +257 -0
  57. package/src/bundled-plugins/memory/secret-detector.ts +49 -0
  58. package/src/bundled-plugins/memory/watermark.ts +15 -0
  59. package/src/bundled-plugins/security/index.ts +35 -0
  60. package/src/bundled-plugins/security/policies/git-exfil.ts +120 -0
  61. package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +167 -0
  62. package/src/bundled-plugins/security/policies/prompt-injection.ts +488 -0
  63. package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +99 -0
  64. package/src/bundled-plugins/security/policies/secret-exfil-read.ts +127 -0
  65. package/src/bundled-plugins/security/policies/session-search-secrets.ts +86 -0
  66. package/src/bundled-plugins/security/policies/ssrf.ts +196 -0
  67. package/src/bundled-plugins/security/policies/system-prompt-leak.ts +81 -0
  68. package/src/bundled-plugins/security/policy.ts +9 -0
  69. package/src/channels/adapters/discord-bot-channel-resolver.ts +77 -0
  70. package/src/channels/adapters/discord-bot-classify.ts +148 -0
  71. package/src/channels/adapters/discord-bot.ts +640 -0
  72. package/src/channels/adapters/kakaotalk-author-resolver.ts +78 -0
  73. package/src/channels/adapters/kakaotalk-channel-resolver.ts +105 -0
  74. package/src/channels/adapters/kakaotalk-classify.ts +77 -0
  75. package/src/channels/adapters/kakaotalk.ts +622 -0
  76. package/src/channels/adapters/slack-bot-author-resolver.ts +80 -0
  77. package/src/channels/adapters/slack-bot-channel-resolver.ts +84 -0
  78. package/src/channels/adapters/slack-bot-classify.ts +213 -0
  79. package/src/channels/adapters/slack-bot-dedupe.ts +51 -0
  80. package/src/channels/adapters/slack-bot-time.ts +10 -0
  81. package/src/channels/adapters/slack-bot.ts +881 -0
  82. package/src/channels/adapters/telegram-bot-classify.ts +155 -0
  83. package/src/channels/adapters/telegram-bot-format.ts +309 -0
  84. package/src/channels/adapters/telegram-bot.ts +604 -0
  85. package/src/channels/engagement.ts +227 -0
  86. package/src/channels/index.ts +21 -0
  87. package/src/channels/manager.ts +292 -0
  88. package/src/channels/membership-cache.ts +116 -0
  89. package/src/channels/membership-from-history.ts +53 -0
  90. package/src/channels/membership.ts +30 -0
  91. package/src/channels/participants.ts +47 -0
  92. package/src/channels/persistence.ts +209 -0
  93. package/src/channels/reloadable.ts +28 -0
  94. package/src/channels/router.ts +1570 -0
  95. package/src/channels/schema.ts +273 -0
  96. package/src/channels/types.ts +160 -0
  97. package/src/cli/channel.ts +403 -0
  98. package/src/cli/compose-status.ts +95 -0
  99. package/src/cli/compose.ts +240 -0
  100. package/src/cli/hostd.ts +163 -0
  101. package/src/cli/index.ts +27 -0
  102. package/src/cli/init.ts +592 -0
  103. package/src/cli/logs.ts +38 -0
  104. package/src/cli/reload.ts +68 -0
  105. package/src/cli/restart.ts +66 -0
  106. package/src/cli/run.ts +77 -0
  107. package/src/cli/shell.ts +33 -0
  108. package/src/cli/start.ts +57 -0
  109. package/src/cli/status.ts +178 -0
  110. package/src/cli/stop.ts +31 -0
  111. package/src/cli/tui.ts +35 -0
  112. package/src/cli/ui.ts +110 -0
  113. package/src/commands/index.ts +74 -0
  114. package/src/compose/discover.ts +43 -0
  115. package/src/compose/index.ts +25 -0
  116. package/src/compose/logs.ts +162 -0
  117. package/src/compose/restart.ts +69 -0
  118. package/src/compose/start.ts +62 -0
  119. package/src/compose/status.ts +28 -0
  120. package/src/compose/stop.ts +43 -0
  121. package/src/config/config.ts +424 -0
  122. package/src/config/index.ts +25 -0
  123. package/src/config/providers.ts +234 -0
  124. package/src/config/reloadable.ts +47 -0
  125. package/src/container/index.ts +27 -0
  126. package/src/container/logs.ts +37 -0
  127. package/src/container/port.ts +137 -0
  128. package/src/container/shared.ts +290 -0
  129. package/src/container/shell.ts +58 -0
  130. package/src/container/start.ts +670 -0
  131. package/src/container/status.ts +76 -0
  132. package/src/container/stop.ts +120 -0
  133. package/src/container/verify-running.ts +149 -0
  134. package/src/cron/consumer.ts +138 -0
  135. package/src/cron/index.ts +54 -0
  136. package/src/cron/reloadable.ts +64 -0
  137. package/src/cron/scheduler.ts +200 -0
  138. package/src/cron/schema.ts +96 -0
  139. package/src/hostd/client.ts +113 -0
  140. package/src/hostd/daemon.ts +587 -0
  141. package/src/hostd/index.ts +25 -0
  142. package/src/hostd/paths.ts +82 -0
  143. package/src/hostd/portbroker-manager.ts +101 -0
  144. package/src/hostd/protocol.ts +48 -0
  145. package/src/hostd/spawn.ts +224 -0
  146. package/src/hostd/supervisor.ts +60 -0
  147. package/src/hostd/tailscale.ts +172 -0
  148. package/src/hostd/version.ts +115 -0
  149. package/src/init/dockerfile.ts +327 -0
  150. package/src/init/ensure-deps.ts +152 -0
  151. package/src/init/gitignore.ts +46 -0
  152. package/src/init/hatching.ts +60 -0
  153. package/src/init/index.ts +786 -0
  154. package/src/init/kakaotalk-auth.ts +114 -0
  155. package/src/init/models-dev.ts +130 -0
  156. package/src/init/oauth-login.ts +74 -0
  157. package/src/init/packagejson.ts +94 -0
  158. package/src/init/paths.ts +2 -0
  159. package/src/init/run-bun-install.ts +20 -0
  160. package/src/markdown/chunk.ts +299 -0
  161. package/src/markdown/index.ts +1 -0
  162. package/src/plugin/context.ts +40 -0
  163. package/src/plugin/define.ts +35 -0
  164. package/src/plugin/hooks.ts +204 -0
  165. package/src/plugin/index.ts +63 -0
  166. package/src/plugin/loader.ts +111 -0
  167. package/src/plugin/manager.ts +136 -0
  168. package/src/plugin/registry.ts +145 -0
  169. package/src/plugin/skills.ts +62 -0
  170. package/src/plugin/types.ts +172 -0
  171. package/src/portbroker/bind-with-forward.ts +102 -0
  172. package/src/portbroker/container-server.ts +305 -0
  173. package/src/portbroker/forward-result-bus.ts +36 -0
  174. package/src/portbroker/hostd-client.ts +443 -0
  175. package/src/portbroker/index.ts +33 -0
  176. package/src/portbroker/policy.ts +24 -0
  177. package/src/portbroker/proc-net-tcp.ts +72 -0
  178. package/src/portbroker/protocol.ts +39 -0
  179. package/src/reload/client.ts +59 -0
  180. package/src/reload/index.ts +3 -0
  181. package/src/reload/registry.ts +60 -0
  182. package/src/reload/types.ts +13 -0
  183. package/src/run/bundled-plugins.ts +24 -0
  184. package/src/run/channel-session-factory.ts +105 -0
  185. package/src/run/index.ts +432 -0
  186. package/src/run/plugin-runtime.ts +43 -0
  187. package/src/run/schema-with-plugins.ts +14 -0
  188. package/src/secrets/index.ts +13 -0
  189. package/src/secrets/migrate.ts +95 -0
  190. package/src/secrets/schema.ts +75 -0
  191. package/src/secrets/storage.ts +231 -0
  192. package/src/server/index.ts +436 -0
  193. package/src/sessions/index.ts +23 -0
  194. package/src/shared/index.ts +9 -0
  195. package/src/shared/local-time.ts +21 -0
  196. package/src/shared/protocol.ts +25 -0
  197. package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +87 -0
  198. package/src/skills/typeclaw-channel-telegram-bot/SKILL.md +64 -0
  199. package/src/skills/typeclaw-config/SKILL.md +643 -0
  200. package/src/skills/typeclaw-cron/SKILL.md +159 -0
  201. package/src/skills/typeclaw-git/SKILL.md +89 -0
  202. package/src/skills/typeclaw-memory/SKILL.md +174 -0
  203. package/src/skills/typeclaw-monorepo/SKILL.md +175 -0
  204. package/src/skills/typeclaw-plugins/SKILL.md +594 -0
  205. package/src/skills/typeclaw-skills/SKILL.md +246 -0
  206. package/src/stream/broker.ts +161 -0
  207. package/src/stream/index.ts +16 -0
  208. package/src/stream/types.ts +69 -0
  209. package/src/tui/client.ts +45 -0
  210. package/src/tui/format.ts +317 -0
  211. package/src/tui/index.ts +225 -0
  212. package/src/tui/theme.ts +41 -0
  213. 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
+ }