@kkelly-offical/kkcode 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +445 -0
  3. package/package.json +46 -0
  4. package/src/agent/agent.mjs +170 -0
  5. package/src/agent/custom-agent-loader.mjs +158 -0
  6. package/src/agent/generator.mjs +115 -0
  7. package/src/agent/prompt/architect.txt +36 -0
  8. package/src/agent/prompt/build-fixer.txt +71 -0
  9. package/src/agent/prompt/build.txt +101 -0
  10. package/src/agent/prompt/compaction.txt +12 -0
  11. package/src/agent/prompt/explore.txt +29 -0
  12. package/src/agent/prompt/guide.txt +40 -0
  13. package/src/agent/prompt/longagent.txt +178 -0
  14. package/src/agent/prompt/plan.txt +50 -0
  15. package/src/agent/prompt/researcher.txt +23 -0
  16. package/src/agent/prompt/reviewer.txt +44 -0
  17. package/src/agent/prompt/security-reviewer.txt +62 -0
  18. package/src/agent/prompt/tdd-guide.txt +84 -0
  19. package/src/agent/prompt/title.txt +8 -0
  20. package/src/command/custom-commands.mjs +57 -0
  21. package/src/commands/agent.mjs +71 -0
  22. package/src/commands/audit.mjs +77 -0
  23. package/src/commands/background.mjs +86 -0
  24. package/src/commands/chat.mjs +114 -0
  25. package/src/commands/command.mjs +41 -0
  26. package/src/commands/config.mjs +44 -0
  27. package/src/commands/doctor.mjs +148 -0
  28. package/src/commands/hook.mjs +29 -0
  29. package/src/commands/init.mjs +141 -0
  30. package/src/commands/longagent.mjs +100 -0
  31. package/src/commands/mcp.mjs +89 -0
  32. package/src/commands/permission.mjs +36 -0
  33. package/src/commands/prompt.mjs +42 -0
  34. package/src/commands/review.mjs +266 -0
  35. package/src/commands/rule.mjs +34 -0
  36. package/src/commands/session.mjs +235 -0
  37. package/src/commands/theme.mjs +98 -0
  38. package/src/commands/usage.mjs +91 -0
  39. package/src/config/defaults.mjs +195 -0
  40. package/src/config/import-config.mjs +76 -0
  41. package/src/config/load-config.mjs +76 -0
  42. package/src/config/schema.mjs +509 -0
  43. package/src/context.mjs +40 -0
  44. package/src/core/constants.mjs +46 -0
  45. package/src/core/errors.mjs +57 -0
  46. package/src/core/events.mjs +29 -0
  47. package/src/core/types.mjs +57 -0
  48. package/src/github/api.mjs +78 -0
  49. package/src/github/auth.mjs +286 -0
  50. package/src/github/flow.mjs +298 -0
  51. package/src/github/workspace.mjs +212 -0
  52. package/src/index.mjs +82 -0
  53. package/src/knowledge/api-design.txt +9 -0
  54. package/src/knowledge/cpp.txt +10 -0
  55. package/src/knowledge/docker.txt +10 -0
  56. package/src/knowledge/dotnet.txt +9 -0
  57. package/src/knowledge/electron.txt +10 -0
  58. package/src/knowledge/flutter.txt +10 -0
  59. package/src/knowledge/go.txt +9 -0
  60. package/src/knowledge/graphql.txt +10 -0
  61. package/src/knowledge/java.txt +9 -0
  62. package/src/knowledge/kotlin.txt +10 -0
  63. package/src/knowledge/loader.mjs +125 -0
  64. package/src/knowledge/next.txt +8 -0
  65. package/src/knowledge/node.txt +8 -0
  66. package/src/knowledge/nuxt.txt +9 -0
  67. package/src/knowledge/php.txt +10 -0
  68. package/src/knowledge/python.txt +10 -0
  69. package/src/knowledge/react-native.txt +10 -0
  70. package/src/knowledge/react.txt +9 -0
  71. package/src/knowledge/ruby.txt +11 -0
  72. package/src/knowledge/rust.txt +9 -0
  73. package/src/knowledge/svelte.txt +9 -0
  74. package/src/knowledge/swift.txt +10 -0
  75. package/src/knowledge/tailwind.txt +10 -0
  76. package/src/knowledge/testing.txt +8 -0
  77. package/src/knowledge/typescript.txt +8 -0
  78. package/src/knowledge/vue.txt +9 -0
  79. package/src/mcp/client-http.mjs +157 -0
  80. package/src/mcp/client-sse.mjs +286 -0
  81. package/src/mcp/client-stdio.mjs +451 -0
  82. package/src/mcp/registry.mjs +394 -0
  83. package/src/mcp/stdio-framing.mjs +127 -0
  84. package/src/orchestration/background-manager.mjs +358 -0
  85. package/src/orchestration/background-worker.mjs +245 -0
  86. package/src/orchestration/longagent-manager.mjs +116 -0
  87. package/src/orchestration/stage-scheduler.mjs +489 -0
  88. package/src/orchestration/subagent-router.mjs +62 -0
  89. package/src/orchestration/task-scheduler.mjs +74 -0
  90. package/src/permission/engine.mjs +92 -0
  91. package/src/permission/exec-policy.mjs +372 -0
  92. package/src/permission/prompt.mjs +39 -0
  93. package/src/permission/rules.mjs +120 -0
  94. package/src/permission/workspace-trust.mjs +44 -0
  95. package/src/plugin/builtin-hooks/console-warn.mjs +41 -0
  96. package/src/plugin/builtin-hooks/extract-patterns.mjs +75 -0
  97. package/src/plugin/builtin-hooks/post-edit-format.mjs +57 -0
  98. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +61 -0
  99. package/src/plugin/builtin-hooks/strategic-compaction.mjs +38 -0
  100. package/src/plugin/hook-bus.mjs +154 -0
  101. package/src/provider/anthropic.mjs +389 -0
  102. package/src/provider/ollama.mjs +236 -0
  103. package/src/provider/openai-compatible.mjs +1 -0
  104. package/src/provider/openai.mjs +339 -0
  105. package/src/provider/retry-policy.mjs +68 -0
  106. package/src/provider/router.mjs +228 -0
  107. package/src/provider/sse.mjs +91 -0
  108. package/src/repl.mjs +2929 -0
  109. package/src/review/diff-parser.mjs +36 -0
  110. package/src/review/rejection-queue.mjs +62 -0
  111. package/src/review/review-store.mjs +21 -0
  112. package/src/review/risk-score.mjs +61 -0
  113. package/src/rules/load-rules.mjs +64 -0
  114. package/src/runtime.mjs +1 -0
  115. package/src/session/checkpoint.mjs +239 -0
  116. package/src/session/compaction.mjs +276 -0
  117. package/src/session/engine.mjs +225 -0
  118. package/src/session/instinct-manager.mjs +172 -0
  119. package/src/session/instruction-loader.mjs +25 -0
  120. package/src/session/longagent-plan.mjs +329 -0
  121. package/src/session/longagent-scaffold.mjs +100 -0
  122. package/src/session/longagent.mjs +1462 -0
  123. package/src/session/loop.mjs +905 -0
  124. package/src/session/memory-loader.mjs +75 -0
  125. package/src/session/project-context.mjs +367 -0
  126. package/src/session/prompt/anthropic.txt +151 -0
  127. package/src/session/prompt/beast.txt +37 -0
  128. package/src/session/prompt/max-steps.txt +6 -0
  129. package/src/session/prompt/plan.txt +9 -0
  130. package/src/session/prompt/qwen.txt +46 -0
  131. package/src/session/prompt-loader.mjs +18 -0
  132. package/src/session/recovery.mjs +52 -0
  133. package/src/session/store.mjs +503 -0
  134. package/src/session/system-prompt.mjs +260 -0
  135. package/src/session/task-validator.mjs +266 -0
  136. package/src/session/usability-gates.mjs +379 -0
  137. package/src/skill/builtin/backend-patterns.mjs +123 -0
  138. package/src/skill/builtin/commit.mjs +64 -0
  139. package/src/skill/builtin/debug.mjs +45 -0
  140. package/src/skill/builtin/frontend-patterns.mjs +120 -0
  141. package/src/skill/builtin/frontend.mjs +188 -0
  142. package/src/skill/builtin/init.mjs +220 -0
  143. package/src/skill/builtin/review.mjs +49 -0
  144. package/src/skill/builtin/security-checklist.mjs +80 -0
  145. package/src/skill/builtin/tdd.mjs +54 -0
  146. package/src/skill/generator.mjs +113 -0
  147. package/src/skill/registry.mjs +336 -0
  148. package/src/storage/audit-store.mjs +83 -0
  149. package/src/storage/event-log.mjs +82 -0
  150. package/src/storage/ghost-commit-store.mjs +235 -0
  151. package/src/storage/json-store.mjs +53 -0
  152. package/src/storage/paths.mjs +148 -0
  153. package/src/theme/color.mjs +64 -0
  154. package/src/theme/default-theme.mjs +29 -0
  155. package/src/theme/load-theme.mjs +71 -0
  156. package/src/theme/markdown.mjs +135 -0
  157. package/src/theme/schema.mjs +45 -0
  158. package/src/theme/status-bar.mjs +158 -0
  159. package/src/tool/audit-wrapper.mjs +38 -0
  160. package/src/tool/edit-transaction.mjs +126 -0
  161. package/src/tool/executor.mjs +109 -0
  162. package/src/tool/file-lock-manager.mjs +85 -0
  163. package/src/tool/git-auto.mjs +545 -0
  164. package/src/tool/git-full-auto.mjs +478 -0
  165. package/src/tool/image-util.mjs +276 -0
  166. package/src/tool/prompt/background_cancel.txt +1 -0
  167. package/src/tool/prompt/background_output.txt +1 -0
  168. package/src/tool/prompt/bash.txt +71 -0
  169. package/src/tool/prompt/codesearch.txt +18 -0
  170. package/src/tool/prompt/edit.txt +27 -0
  171. package/src/tool/prompt/enter_plan.txt +74 -0
  172. package/src/tool/prompt/exit_plan.txt +62 -0
  173. package/src/tool/prompt/glob.txt +33 -0
  174. package/src/tool/prompt/grep.txt +43 -0
  175. package/src/tool/prompt/list.txt +8 -0
  176. package/src/tool/prompt/multiedit.txt +20 -0
  177. package/src/tool/prompt/notebookedit.txt +21 -0
  178. package/src/tool/prompt/patch.txt +24 -0
  179. package/src/tool/prompt/question.txt +44 -0
  180. package/src/tool/prompt/read.txt +40 -0
  181. package/src/tool/prompt/task.txt +83 -0
  182. package/src/tool/prompt/todowrite.txt +117 -0
  183. package/src/tool/prompt/webfetch.txt +38 -0
  184. package/src/tool/prompt/websearch.txt +43 -0
  185. package/src/tool/prompt/write.txt +38 -0
  186. package/src/tool/prompt-loader.mjs +18 -0
  187. package/src/tool/question-prompt.mjs +86 -0
  188. package/src/tool/registry.mjs +1309 -0
  189. package/src/tool/task-tool.mjs +28 -0
  190. package/src/ui/activity-renderer.mjs +410 -0
  191. package/src/ui/repl-dashboard.mjs +357 -0
  192. package/src/usage/pricing.mjs +121 -0
  193. package/src/usage/usage-meter.mjs +113 -0
  194. package/src/util/git.mjs +496 -0
  195. package/src/util/template.mjs +10 -0
  196. package/src/util/yaml.mjs +100 -0
@@ -0,0 +1,389 @@
1
+ import { ProviderError } from "../core/errors.mjs"
2
+ import { requestWithRetry } from "./retry-policy.mjs"
3
+ import { parseSSE } from "./sse.mjs"
4
+
5
+ function sleep(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms))
7
+ }
8
+
9
+ function mapTools(tools) {
10
+ if (!tools || !tools.length) return []
11
+ const mapped = tools.map((tool) => ({
12
+ name: tool.name,
13
+ description: tool.description,
14
+ input_schema: tool.inputSchema
15
+ }))
16
+ // Cache the tool definitions (they rarely change within a session)
17
+ if (mapped.length > 0) {
18
+ mapped[mapped.length - 1].cache_control = { type: "ephemeral" }
19
+ }
20
+ return mapped
21
+ }
22
+
23
+ function systemWithCacheControl(system) {
24
+ if (!system) return undefined
25
+
26
+ // Structured format from buildSystemPromptBlocks: { text, blocks }
27
+ // Strategy: merge all stable content into ONE block with cache_control,
28
+ // keeping dynamic content separate. Combined with the tool breakpoint in
29
+ // mapTools, this gives us 2 breakpoints total — well within the 4-max limit
30
+ // and ensures the cumulative prefix easily exceeds the minimum cacheable
31
+ // threshold (4096 tokens for Opus, 1024 for Sonnet).
32
+ if (system.blocks && Array.isArray(system.blocks)) {
33
+ const stableParts = []
34
+ const dynamicParts = []
35
+ for (const block of system.blocks) {
36
+ if (block.cacheable === false) {
37
+ dynamicParts.push(block.text)
38
+ } else {
39
+ stableParts.push(block.text)
40
+ }
41
+ }
42
+
43
+ const contentBlocks = []
44
+ if (stableParts.length) {
45
+ contentBlocks.push({
46
+ type: "text",
47
+ text: stableParts.join("\n\n"),
48
+ cache_control: { type: "ephemeral" }
49
+ })
50
+ }
51
+ if (dynamicParts.length) {
52
+ contentBlocks.push({ type: "text", text: dynamicParts.join("\n\n") })
53
+ }
54
+ return contentBlocks.length ? contentBlocks : undefined
55
+ }
56
+
57
+ // Legacy: plain string
58
+ if (typeof system === "string") {
59
+ return [{ type: "text", text: system, cache_control: { type: "ephemeral" } }]
60
+ }
61
+ // Legacy: array of strings/blocks
62
+ if (Array.isArray(system)) {
63
+ const blocks = system.map((b) => (typeof b === "string" ? { type: "text", text: b } : { ...b }))
64
+ if (blocks.length > 0) {
65
+ blocks[blocks.length - 1].cache_control = { type: "ephemeral" }
66
+ }
67
+ return blocks
68
+ }
69
+ return system
70
+ }
71
+
72
+ function mapContentBlock(block) {
73
+ if (block.type === "image" && block.data) {
74
+ return {
75
+ type: "image",
76
+ source: {
77
+ type: "base64",
78
+ media_type: block.mediaType || "image/png",
79
+ data: block.data
80
+ }
81
+ }
82
+ }
83
+ // Native Anthropic tool_use block — pass through
84
+ if (block.type === "tool_use") {
85
+ return { type: "tool_use", id: block.id, name: block.name, input: block.input || {} }
86
+ }
87
+ // Native Anthropic tool_result block — pass through
88
+ if (block.type === "tool_result") {
89
+ return {
90
+ type: "tool_result",
91
+ tool_use_id: block.tool_use_id,
92
+ content: String(block.content || ""),
93
+ ...(block.is_error ? { is_error: true } : {})
94
+ }
95
+ }
96
+ return { type: "text", text: String(block.text || block.content || "") }
97
+ }
98
+
99
+ function mapMessages(messages) {
100
+ const mapped = messages.map((message) => {
101
+ const role = message.role === "assistant" ? "assistant" : "user"
102
+ const content = message.content
103
+ if (Array.isArray(content)) {
104
+ return { role, content: content.map(mapContentBlock) }
105
+ }
106
+ return { role, content: String(content || "") }
107
+ })
108
+ // Add cache_control to last user message for multi-turn caching
109
+ for (let i = mapped.length - 1; i >= 0; i--) {
110
+ if (mapped[i].role === "user") {
111
+ const c = mapped[i].content
112
+ if (Array.isArray(c) && c.length) {
113
+ c[c.length - 1].cache_control = { type: "ephemeral" }
114
+ } else if (typeof c === "string") {
115
+ mapped[i].content = [{ type: "text", text: c, cache_control: { type: "ephemeral" } }]
116
+ }
117
+ break
118
+ }
119
+ }
120
+ return mapped
121
+ }
122
+
123
+ function parseContentBlocks(content) {
124
+ const blocks = Array.isArray(content) ? content : []
125
+ const text = blocks.filter((block) => block.type === "text").map((block) => block.text || "").join("\n")
126
+ const toolCalls = blocks
127
+ .filter((block) => block.type === "tool_use" && block.name)
128
+ .map((block) => ({
129
+ id: block.id || `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
130
+ name: block.name,
131
+ args: block.input || {}
132
+ }))
133
+ return { text, toolCalls }
134
+ }
135
+
136
+ function timeoutSignal(ms, parentSignal = null) {
137
+ const own = AbortSignal.timeout(ms)
138
+ if (!parentSignal) return own
139
+ return AbortSignal.any([parentSignal, own])
140
+ }
141
+
142
+ export async function requestAnthropic(input) {
143
+ const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 120000, maxTokens = 16384, retry = {}, signal = null } = input
144
+ if (!apiKey) {
145
+ throw new ProviderError(`missing API key for anthropic provider (env: ${input.apiKeyEnv || "unknown"})`, {
146
+ provider: "anthropic"
147
+ })
148
+ }
149
+
150
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/messages`
151
+ const mappedTools = mapTools(tools)
152
+ const payload = {
153
+ model,
154
+ max_tokens: maxTokens,
155
+ metadata: { user_id: "kkcode" },
156
+ system: systemWithCacheControl(system),
157
+ messages: mapMessages(messages),
158
+ tools: mappedTools.length ? mappedTools : undefined
159
+ }
160
+ if (input.thinking?.type) {
161
+ payload.thinking = { type: input.thinking.type, budget_tokens: input.thinking.budget_tokens || 10000 }
162
+ }
163
+
164
+ return requestWithRetry({
165
+ attempts: Number(retry.attempts ?? 3),
166
+ baseDelayMs: Number(retry.baseDelayMs ?? 800),
167
+ signal,
168
+ execute: async () => {
169
+ const response = await fetch(endpoint, {
170
+ method: "POST",
171
+ headers: {
172
+ "content-type": "application/json",
173
+ "x-api-key": apiKey,
174
+ "anthropic-version": "2023-06-01",
175
+ "anthropic-beta": "prompt-caching-2024-07-31"
176
+ },
177
+ body: JSON.stringify(payload),
178
+ signal: timeoutSignal(timeoutMs, signal)
179
+ })
180
+ if (!response.ok) {
181
+ const text = await response.text().catch(() => "")
182
+ const error = new ProviderError(`anthropic request failed: ${response.status} ${text}`, {
183
+ provider: "anthropic",
184
+ model,
185
+ endpoint
186
+ })
187
+ error.httpStatus = response.status
188
+ throw error
189
+ }
190
+ const json = await response.json()
191
+ const parsed = parseContentBlocks(json?.content)
192
+ const usage = {
193
+ input: json?.usage?.input_tokens ?? 0,
194
+ output: json?.usage?.output_tokens ?? 0,
195
+ cacheRead: json?.usage?.cache_read_input_tokens ?? 0,
196
+ cacheWrite: json?.usage?.cache_creation_input_tokens ?? 0
197
+ }
198
+ return { text: parsed.text, usage, toolCalls: parsed.toolCalls }
199
+ }
200
+ })
201
+ }
202
+
203
+ export async function countTokensAnthropic(input) {
204
+ const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 10000 } = input
205
+ if (!apiKey) return null
206
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/messages/count_tokens`
207
+ const mappedTools = mapTools(tools)
208
+ const payload = {
209
+ model,
210
+ system: systemWithCacheControl(system),
211
+ messages: mapMessages(messages),
212
+ tools: mappedTools.length ? mappedTools : undefined
213
+ }
214
+ try {
215
+ const res = await fetch(endpoint, {
216
+ method: "POST",
217
+ headers: {
218
+ "content-type": "application/json",
219
+ "x-api-key": apiKey,
220
+ "anthropic-version": "2023-06-01"
221
+ },
222
+ body: JSON.stringify(payload),
223
+ signal: AbortSignal.timeout(timeoutMs)
224
+ })
225
+ if (!res.ok) return null
226
+ const json = await res.json()
227
+ return json?.input_tokens ?? null
228
+ } catch {
229
+ return null
230
+ }
231
+ }
232
+
233
+ export async function* requestAnthropicStream(input) {
234
+ const { apiKey, baseUrl, model, system, messages, tools, timeoutMs = 120000, streamIdleTimeoutMs = 120000, maxTokens = 16384, retry = {}, signal = null, compaction = null } = input
235
+ if (!apiKey) {
236
+ throw new ProviderError(`missing API key for anthropic provider (env: ${input.apiKeyEnv || "unknown"})`, {
237
+ provider: "anthropic"
238
+ })
239
+ }
240
+
241
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/messages`
242
+ const mappedTools = mapTools(tools)
243
+ const payload = {
244
+ model,
245
+ max_tokens: maxTokens,
246
+ metadata: { user_id: "kkcode" },
247
+ system: systemWithCacheControl(system),
248
+ messages: mapMessages(messages),
249
+ tools: mappedTools.length ? mappedTools : undefined,
250
+ stream: true,
251
+ ...(compaction ? { context_management: { edits: [{ type: "compact_20260112", trigger: { tokens: compaction.trigger || 150000 } }] } } : {})
252
+ }
253
+ if (input.thinking?.type) {
254
+ payload.thinking = { type: input.thinking.type, budget_tokens: input.thinking.budget_tokens || 10000 }
255
+ }
256
+ const attempts = Number(retry.attempts ?? 3)
257
+ const baseDelayMs = Number(retry.baseDelayMs ?? 800)
258
+
259
+ let response
260
+ for (let attempt = 1; attempt <= attempts; attempt++) {
261
+ try {
262
+ // Use a connection-only timeout for the initial fetch.
263
+ // Once headers arrive, clear it — the SSE idle timeout handles the streaming phase.
264
+ const connController = new AbortController()
265
+ const connTimer = setTimeout(() => connController.abort(), timeoutMs)
266
+ const fetchSignal = signal
267
+ ? AbortSignal.any([signal, connController.signal])
268
+ : connController.signal
269
+
270
+ response = await fetch(endpoint, {
271
+ method: "POST",
272
+ headers: {
273
+ "content-type": "application/json",
274
+ "x-api-key": apiKey,
275
+ "anthropic-version": "2023-06-01",
276
+ "anthropic-beta": compaction ? "prompt-caching-2024-07-31,compact-2026-01-12" : "prompt-caching-2024-07-31"
277
+ },
278
+ body: JSON.stringify(payload),
279
+ signal: fetchSignal
280
+ })
281
+ clearTimeout(connTimer)
282
+
283
+ if (!response.ok) {
284
+ const text = await response.text().catch(() => "")
285
+ const error = new ProviderError(`anthropic stream failed: ${response.status} ${text}`, {
286
+ provider: "anthropic", model, endpoint
287
+ })
288
+ error.httpStatus = response.status
289
+ throw error
290
+ }
291
+ break
292
+ } catch (err) {
293
+ if (signal?.aborted) throw err
294
+ const isNetwork = err?.code === "ETIMEDOUT" || err?.code === "ECONNRESET" || err?.name === "AbortError"
295
+ if (!isNetwork || attempt >= attempts) throw err
296
+ await sleep(baseDelayMs * Math.pow(2, attempt - 1))
297
+ }
298
+ }
299
+
300
+ let currentBlock = null
301
+ let inputUsage = { input: 0, cacheRead: 0, cacheWrite: 0 }
302
+ let outputTokens = 0
303
+ let stopReason = null
304
+
305
+ for await (const { event, data } of parseSSE(response.body, signal, { idleTimeoutMs: streamIdleTimeoutMs })) {
306
+ let parsed
307
+ try { parsed = JSON.parse(data) } catch { continue }
308
+
309
+ if (event === "message_start") {
310
+ const u = parsed.message?.usage
311
+ inputUsage.input = u?.input_tokens ?? 0
312
+ inputUsage.cacheRead = u?.cache_read_input_tokens ?? 0
313
+ inputUsage.cacheWrite = u?.cache_creation_input_tokens ?? 0
314
+ }
315
+
316
+ if (event === "content_block_start") {
317
+ const block = parsed.content_block
318
+ currentBlock = {
319
+ type: block?.type,
320
+ id: block?.id || null,
321
+ name: block?.name || null,
322
+ jsonParts: []
323
+ }
324
+ }
325
+
326
+ if (event === "content_block_delta") {
327
+ if (parsed.delta?.type === "text_delta") {
328
+ const text = parsed.delta.text || ""
329
+ if (text) yield { type: "text", content: text }
330
+ }
331
+ if (parsed.delta?.type === "thinking_delta" && currentBlock?.type !== "redacted_thinking") {
332
+ const thinking = parsed.delta.thinking || ""
333
+ if (thinking) yield { type: "thinking", content: thinking }
334
+ }
335
+ if (parsed.delta?.type === "input_json_delta") {
336
+ if (currentBlock) currentBlock.jsonParts.push(parsed.delta.partial_json || "")
337
+ }
338
+ if (parsed.delta?.type === "compaction_delta") {
339
+ if (currentBlock) currentBlock.compactionContent = parsed.delta.content || ""
340
+ }
341
+ }
342
+
343
+ if (event === "content_block_stop" && currentBlock) {
344
+ if (currentBlock.type === "tool_use") {
345
+ const raw = currentBlock.jsonParts.join("") || "{}"
346
+ let args = {}
347
+ try {
348
+ args = JSON.parse(raw)
349
+ } catch (parseErr) {
350
+ console.error(`[anthropic] tool_call JSON parse failed for "${currentBlock.name}": ${parseErr.message} (${raw.length} chars, first 200: ${raw.slice(0, 200)})`)
351
+ args = { __parse_error: true, __raw_length: raw.length, __error: parseErr.message }
352
+ }
353
+ yield {
354
+ type: "tool_call",
355
+ call: {
356
+ id: currentBlock.id || `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
357
+ name: currentBlock.name,
358
+ args
359
+ }
360
+ }
361
+ }
362
+ if (currentBlock.type === "compaction") {
363
+ yield { type: "compaction", content: currentBlock.compactionContent || "" }
364
+ }
365
+ currentBlock = null
366
+ }
367
+
368
+ if (event === "message_delta") {
369
+ outputTokens = parsed.usage?.output_tokens ?? outputTokens
370
+ if (parsed.delta?.stop_reason) {
371
+ stopReason = parsed.delta.stop_reason
372
+ }
373
+ }
374
+
375
+ if (event === "message_stop") {
376
+ yield {
377
+ type: "usage",
378
+ usage: {
379
+ input: inputUsage.input,
380
+ output: outputTokens,
381
+ cacheRead: inputUsage.cacheRead,
382
+ cacheWrite: inputUsage.cacheWrite
383
+ }
384
+ }
385
+ // Normalize: "end_turn" → "end_turn", "max_tokens" → "max_tokens", "tool_use" → "tool_use"
386
+ yield { type: "stop", reason: stopReason || "end_turn" }
387
+ }
388
+ }
389
+ }
@@ -0,0 +1,236 @@
1
+ import { ProviderError } from "../core/errors.mjs"
2
+
3
+ function mapTools(tools) {
4
+ if (!tools || !tools.length) return undefined
5
+ return tools.map((tool) => ({
6
+ type: "function",
7
+ function: {
8
+ name: tool.name,
9
+ description: tool.description,
10
+ parameters: tool.inputSchema
11
+ }
12
+ }))
13
+ }
14
+
15
+ function resolveSystemText(system) {
16
+ if (!system) return ""
17
+ if (typeof system === "string") return system
18
+ if (system.text) return system.text
19
+ return String(system)
20
+ }
21
+
22
+ function mapMessages(system, messages) {
23
+ const mapped = [{ role: "system", content: resolveSystemText(system) }]
24
+ for (const msg of messages) {
25
+ const content = msg.content
26
+ if (!Array.isArray(content)) {
27
+ mapped.push({ role: msg.role, content: String(content || "") })
28
+ continue
29
+ }
30
+
31
+ // Assistant message with tool_use blocks → tool_calls format
32
+ const toolUseBlocks = content.filter((b) => b.type === "tool_use")
33
+ if (toolUseBlocks.length > 0 && msg.role === "assistant") {
34
+ const textParts = content.filter((b) => b.type === "text").map((b) => b.text || "").join("\n")
35
+ mapped.push({
36
+ role: "assistant",
37
+ content: textParts || "",
38
+ tool_calls: toolUseBlocks.map((b) => ({
39
+ id: b.id,
40
+ type: "function",
41
+ function: {
42
+ name: b.name,
43
+ arguments: JSON.stringify(b.input || {})
44
+ }
45
+ }))
46
+ })
47
+ continue
48
+ }
49
+
50
+ // User message with tool_result blocks → role:"tool" messages
51
+ const toolResultBlocks = content.filter((b) => b.type === "tool_result")
52
+ if (toolResultBlocks.length > 0) {
53
+ for (const result of toolResultBlocks) {
54
+ mapped.push({
55
+ role: "tool",
56
+ tool_call_id: result.tool_use_id,
57
+ content: String(result.content || "")
58
+ })
59
+ }
60
+ continue
61
+ }
62
+
63
+ // Fallback: plain text extraction
64
+ const text = content.filter((b) => b.type === "text").map((b) => b.text || "").join("\n")
65
+ mapped.push({ role: msg.role, content: text || String(content) })
66
+ }
67
+ return mapped
68
+ }
69
+
70
+ function parseToolCalls(message) {
71
+ if (!Array.isArray(message?.tool_calls)) return []
72
+ return message.tool_calls
73
+ .filter((call) => call?.function?.name)
74
+ .map((call) => {
75
+ let args = {}
76
+ if (typeof call.function.arguments === "string") {
77
+ try { args = JSON.parse(call.function.arguments) } catch { args = {} }
78
+ } else if (typeof call.function.arguments === "object" && call.function.arguments !== null) {
79
+ args = call.function.arguments
80
+ }
81
+ return {
82
+ id: call.id || `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
83
+ name: call.function.name,
84
+ args
85
+ }
86
+ })
87
+ }
88
+
89
+ function timeoutSignal(ms, parentSignal = null) {
90
+ const own = AbortSignal.timeout(ms)
91
+ if (!parentSignal) return own
92
+ return AbortSignal.any([parentSignal, own])
93
+ }
94
+
95
+ // --- Non-streaming ---
96
+ export async function requestOllama(input) {
97
+ const { baseUrl, model, system, messages, tools, timeoutMs = 300000, signal = null } = input
98
+
99
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/api/chat`
100
+ const payload = {
101
+ model,
102
+ messages: mapMessages(system, messages),
103
+ stream: false
104
+ }
105
+ const mappedTools = mapTools(tools)
106
+ if (mappedTools) payload.tools = mappedTools
107
+
108
+ const response = await fetch(endpoint, {
109
+ method: "POST",
110
+ headers: { "content-type": "application/json" },
111
+ body: JSON.stringify(payload),
112
+ signal: timeoutSignal(timeoutMs, signal)
113
+ })
114
+
115
+ if (!response.ok) {
116
+ const text = await response.text().catch(() => "")
117
+ const error = new ProviderError(`ollama request failed: ${response.status} ${text}`, {
118
+ provider: "ollama", model, endpoint
119
+ })
120
+ error.httpStatus = response.status
121
+ throw error
122
+ }
123
+
124
+ const json = await response.json()
125
+ const message = json.message || {}
126
+ const text = typeof message.content === "string" ? message.content : ""
127
+ const toolCalls = parseToolCalls(message)
128
+
129
+ return {
130
+ text,
131
+ usage: {
132
+ input: json.prompt_eval_count ?? 0,
133
+ output: json.eval_count ?? 0,
134
+ cacheRead: 0,
135
+ cacheWrite: 0
136
+ },
137
+ toolCalls
138
+ }
139
+ }
140
+
141
+ // --- Streaming (NDJSON) ---
142
+ export async function* requestOllamaStream(input) {
143
+ const { baseUrl, model, system, messages, tools, timeoutMs = 300000, signal = null } = input
144
+
145
+ const endpoint = `${baseUrl.replace(/\/$/, "")}/api/chat`
146
+ const payload = {
147
+ model,
148
+ messages: mapMessages(system, messages),
149
+ stream: true
150
+ }
151
+ const mappedTools = mapTools(tools)
152
+ if (mappedTools) payload.tools = mappedTools
153
+
154
+ const response = await fetch(endpoint, {
155
+ method: "POST",
156
+ headers: { "content-type": "application/json" },
157
+ body: JSON.stringify(payload),
158
+ signal: timeoutSignal(timeoutMs, signal)
159
+ })
160
+
161
+ if (!response.ok) {
162
+ const text = await response.text().catch(() => "")
163
+ const error = new ProviderError(`ollama stream failed: ${response.status} ${text}`, {
164
+ provider: "ollama", model, endpoint
165
+ })
166
+ error.httpStatus = response.status
167
+ throw error
168
+ }
169
+
170
+ const reader = response.body.getReader()
171
+ const decoder = new TextDecoder()
172
+ let buffer = ""
173
+
174
+ try {
175
+ while (true) {
176
+ if (signal?.aborted) break
177
+ const { done, value } = await reader.read()
178
+ if (done) break
179
+ buffer += decoder.decode(value, { stream: true })
180
+
181
+ const lines = buffer.split("\n")
182
+ buffer = lines.pop()
183
+
184
+ for (const line of lines) {
185
+ const trimmed = line.trim()
186
+ if (!trimmed) continue
187
+ let json
188
+ try { json = JSON.parse(trimmed) } catch { continue }
189
+
190
+ if (json.message?.content) {
191
+ yield { type: "text", content: json.message.content }
192
+ }
193
+
194
+ if (json.done) {
195
+ const toolCalls = parseToolCalls(json.message)
196
+ for (const call of toolCalls) {
197
+ yield { type: "tool_call", call }
198
+ }
199
+ yield {
200
+ type: "usage",
201
+ usage: {
202
+ input: json.prompt_eval_count ?? 0,
203
+ output: json.eval_count ?? 0,
204
+ cacheRead: 0,
205
+ cacheWrite: 0
206
+ }
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ if (buffer.trim()) {
213
+ try {
214
+ const json = JSON.parse(buffer.trim())
215
+ if (json.message?.content) yield { type: "text", content: json.message.content }
216
+ if (json.done) {
217
+ const toolCalls = parseToolCalls(json.message)
218
+ for (const call of toolCalls) {
219
+ yield { type: "tool_call", call }
220
+ }
221
+ yield {
222
+ type: "usage",
223
+ usage: {
224
+ input: json.prompt_eval_count ?? 0,
225
+ output: json.eval_count ?? 0,
226
+ cacheRead: 0,
227
+ cacheWrite: 0
228
+ }
229
+ }
230
+ }
231
+ } catch { /* ignore incomplete JSON */ }
232
+ }
233
+ } finally {
234
+ try { reader.releaseLock() } catch { /* reader may have pending read if generator was force-closed */ }
235
+ }
236
+ }
@@ -0,0 +1 @@
1
+ export { requestOpenAI as request, requestOpenAIStream as requestStream } from "./openai.mjs"