@sulala/agent 0.1.6 → 0.1.8

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 (173) hide show
  1. package/README.md +42 -27
  2. package/context/airtable.md +35 -0
  3. package/context/asana.md +37 -0
  4. package/context/bluesky.md +26 -91
  5. package/context/calendar.md +63 -0
  6. package/context/country-info.md +13 -0
  7. package/context/create-skill.md +128 -0
  8. package/context/discord.md +30 -0
  9. package/context/docs.md +29 -0
  10. package/context/drive.md +49 -0
  11. package/context/dropbox.md +39 -0
  12. package/context/facebook.md +47 -0
  13. package/context/fetch-form-api.md +16 -0
  14. package/context/figma.md +30 -0
  15. package/context/github.md +58 -0
  16. package/context/gmail.md +52 -0
  17. package/context/google.md +28 -0
  18. package/context/hellohub.md +29 -0
  19. package/context/jira.md +46 -0
  20. package/context/linear.md +40 -0
  21. package/context/notion.md +45 -0
  22. package/context/portal-integrations.md +42 -0
  23. package/context/post-to-x.md +50 -0
  24. package/context/sheets.md +47 -0
  25. package/context/slack.md +48 -0
  26. package/context/slides.md +35 -0
  27. package/context/stripe.md +38 -0
  28. package/context/tes.md +7 -0
  29. package/context/test.md +7 -0
  30. package/context/zoom.md +28 -0
  31. package/dist/agent/google/calendar.d.ts +2 -0
  32. package/dist/agent/google/calendar.d.ts.map +1 -0
  33. package/dist/agent/google/calendar.js +119 -0
  34. package/dist/agent/google/calendar.js.map +1 -0
  35. package/dist/agent/google/drive.d.ts +2 -0
  36. package/dist/agent/google/drive.d.ts.map +1 -0
  37. package/dist/agent/google/drive.js +51 -0
  38. package/dist/agent/google/drive.js.map +1 -0
  39. package/dist/agent/google/get-token.d.ts +7 -0
  40. package/dist/agent/google/get-token.d.ts.map +1 -0
  41. package/dist/agent/google/get-token.js +37 -0
  42. package/dist/agent/google/get-token.js.map +1 -0
  43. package/dist/agent/google/gmail.d.ts +2 -0
  44. package/dist/agent/google/gmail.d.ts.map +1 -0
  45. package/dist/agent/google/gmail.js +138 -0
  46. package/dist/agent/google/gmail.js.map +1 -0
  47. package/dist/agent/google/index.d.ts +2 -0
  48. package/dist/agent/google/index.d.ts.map +1 -0
  49. package/dist/agent/google/index.js +13 -0
  50. package/dist/agent/google/index.js.map +1 -0
  51. package/dist/agent/loop.d.ts +8 -0
  52. package/dist/agent/loop.d.ts.map +1 -1
  53. package/dist/agent/loop.js +226 -40
  54. package/dist/agent/loop.js.map +1 -1
  55. package/dist/agent/memory.d.ts +21 -0
  56. package/dist/agent/memory.d.ts.map +1 -0
  57. package/dist/agent/memory.js +33 -0
  58. package/dist/agent/memory.js.map +1 -0
  59. package/dist/agent/pending-actions.d.ts +21 -0
  60. package/dist/agent/pending-actions.d.ts.map +1 -0
  61. package/dist/agent/pending-actions.js +65 -0
  62. package/dist/agent/pending-actions.js.map +1 -0
  63. package/dist/agent/pi-runner.d.ts +27 -0
  64. package/dist/agent/pi-runner.d.ts.map +1 -0
  65. package/dist/agent/pi-runner.js +300 -0
  66. package/dist/agent/pi-runner.js.map +1 -0
  67. package/dist/agent/skill-generate.d.ts +63 -0
  68. package/dist/agent/skill-generate.d.ts.map +1 -0
  69. package/dist/agent/skill-generate.js +128 -0
  70. package/dist/agent/skill-generate.js.map +1 -0
  71. package/dist/agent/skill-install.d.ts.map +1 -1
  72. package/dist/agent/skill-install.js +80 -31
  73. package/dist/agent/skill-install.js.map +1 -1
  74. package/dist/agent/skill-templates.d.ts +17 -0
  75. package/dist/agent/skill-templates.d.ts.map +1 -0
  76. package/dist/agent/skill-templates.js +26 -0
  77. package/dist/agent/skill-templates.js.map +1 -0
  78. package/dist/agent/skills-config.d.ts +24 -2
  79. package/dist/agent/skills-config.d.ts.map +1 -1
  80. package/dist/agent/skills-config.js +108 -9
  81. package/dist/agent/skills-config.js.map +1 -1
  82. package/dist/agent/skills-watcher.js +1 -1
  83. package/dist/agent/skills.d.ts +9 -3
  84. package/dist/agent/skills.d.ts.map +1 -1
  85. package/dist/agent/skills.js +104 -9
  86. package/dist/agent/skills.js.map +1 -1
  87. package/dist/agent/tools.d.ts +25 -3
  88. package/dist/agent/tools.d.ts.map +1 -1
  89. package/dist/agent/tools.integrations.test.d.ts +2 -0
  90. package/dist/agent/tools.integrations.test.d.ts.map +1 -0
  91. package/dist/agent/tools.integrations.test.js +269 -0
  92. package/dist/agent/tools.integrations.test.js.map +1 -0
  93. package/dist/agent/tools.js +692 -39
  94. package/dist/agent/tools.js.map +1 -1
  95. package/dist/ai/orchestrator.d.ts +6 -1
  96. package/dist/ai/orchestrator.d.ts.map +1 -1
  97. package/dist/ai/orchestrator.js +499 -212
  98. package/dist/ai/orchestrator.js.map +1 -1
  99. package/dist/ai/pricing.d.ts +6 -0
  100. package/dist/ai/pricing.d.ts.map +1 -0
  101. package/dist/ai/pricing.js +39 -0
  102. package/dist/ai/pricing.js.map +1 -0
  103. package/dist/channels/discord.d.ts +15 -0
  104. package/dist/channels/discord.d.ts.map +1 -0
  105. package/dist/channels/discord.js +55 -0
  106. package/dist/channels/discord.js.map +1 -0
  107. package/dist/channels/stripe.d.ts +15 -0
  108. package/dist/channels/stripe.d.ts.map +1 -0
  109. package/dist/channels/stripe.js +58 -0
  110. package/dist/channels/stripe.js.map +1 -0
  111. package/dist/channels/telegram.d.ts +60 -0
  112. package/dist/channels/telegram.d.ts.map +1 -0
  113. package/dist/channels/telegram.js +562 -0
  114. package/dist/channels/telegram.js.map +1 -0
  115. package/dist/cli.js +69 -11
  116. package/dist/cli.js.map +1 -1
  117. package/dist/config.d.ts +14 -0
  118. package/dist/config.d.ts.map +1 -1
  119. package/dist/config.js +91 -2
  120. package/dist/config.js.map +1 -1
  121. package/dist/db/index.d.ts +83 -0
  122. package/dist/db/index.d.ts.map +1 -1
  123. package/dist/db/index.js +174 -2
  124. package/dist/db/index.js.map +1 -1
  125. package/dist/db/schema.sql +35 -0
  126. package/dist/gateway/server.d.ts.map +1 -1
  127. package/dist/gateway/server.js +1224 -29
  128. package/dist/gateway/server.js.map +1 -1
  129. package/dist/index.js +149 -6
  130. package/dist/index.js.map +1 -1
  131. package/dist/ollama-setup.d.ts +27 -0
  132. package/dist/ollama-setup.d.ts.map +1 -0
  133. package/dist/ollama-setup.js +191 -0
  134. package/dist/ollama-setup.js.map +1 -0
  135. package/dist/onboard-env.d.ts +1 -1
  136. package/dist/onboard-env.d.ts.map +1 -1
  137. package/dist/onboard-env.js +3 -0
  138. package/dist/onboard-env.js.map +1 -1
  139. package/dist/onboard.d.ts +3 -1
  140. package/dist/onboard.d.ts.map +1 -1
  141. package/dist/onboard.js +9 -4
  142. package/dist/onboard.js.map +1 -1
  143. package/dist/plugins/index.d.ts +10 -0
  144. package/dist/plugins/index.d.ts.map +1 -1
  145. package/dist/plugins/index.js +32 -0
  146. package/dist/plugins/index.js.map +1 -1
  147. package/dist/redact.d.ts +15 -0
  148. package/dist/redact.d.ts.map +1 -0
  149. package/dist/redact.js +56 -0
  150. package/dist/redact.js.map +1 -0
  151. package/dist/scheduler/cron.d.ts +21 -0
  152. package/dist/scheduler/cron.d.ts.map +1 -1
  153. package/dist/scheduler/cron.js +60 -0
  154. package/dist/scheduler/cron.js.map +1 -1
  155. package/dist/system-capabilities.d.ts +11 -0
  156. package/dist/system-capabilities.d.ts.map +1 -0
  157. package/dist/system-capabilities.js +109 -0
  158. package/dist/system-capabilities.js.map +1 -0
  159. package/dist/types.d.ts +62 -3
  160. package/dist/types.d.ts.map +1 -1
  161. package/dist/watcher/index.d.ts +2 -0
  162. package/dist/watcher/index.d.ts.map +1 -1
  163. package/dist/watcher/index.js +31 -1
  164. package/dist/watcher/index.js.map +1 -1
  165. package/dist/workspace-automations.d.ts +16 -0
  166. package/dist/workspace-automations.d.ts.map +1 -0
  167. package/dist/workspace-automations.js +133 -0
  168. package/dist/workspace-automations.js.map +1 -0
  169. package/package.json +19 -3
  170. package/registry/bluesky.md +12 -89
  171. package/registry/skills-registry.json +6 -0
  172. package/src/db/schema.sql +35 -0
  173. package/src/index.ts +159 -6
@@ -1,23 +1,146 @@
1
1
  import { log, saveAiResult } from '../db/index.js';
2
2
  const providers = new Map();
3
- const defaultProvider = process.env.AI_DEFAULT_PROVIDER || 'openai';
4
3
  export function registerProvider(name, adapter) {
5
4
  providers.set(name, adapter);
6
5
  }
7
6
  export function getProvider(name) {
8
- const key = name ?? defaultProvider;
7
+ const key = name ?? process.env.AI_DEFAULT_PROVIDER ?? 'ollama';
9
8
  const p = providers.get(key);
10
9
  if (!p)
11
10
  throw new Error(`Unknown AI provider: ${key}. Registered: ${[...providers.keys()].join(', ')}`);
12
11
  return p;
13
12
  }
14
- /** Stream completion (OpenAI and OpenRouter). Falls back to non-streaming for other providers. */
13
+ /** Stream completion (OpenAI, OpenRouter, Ollama). Falls back to non-streaming for other providers. */
15
14
  export async function completeStream(options, onChunk) {
16
- const provider = options.provider ?? defaultProvider;
15
+ const provider = options.provider ?? process.env.AI_DEFAULT_PROVIDER ?? 'ollama';
17
16
  const messages = options.messages ?? [];
18
17
  const model = options.model;
19
18
  const max_tokens = options.max_tokens ?? 1024;
20
19
  const tools = options.tools;
20
+ if (provider === 'ollama' || provider === 'llama') {
21
+ // Tool calling: use non-streaming so tool_calls.arguments are complete (streaming can truncate and cause JSON.parse errors).
22
+ if (tools?.length) {
23
+ const result = await complete({ ...options, provider, messages, model, max_tokens, tools, signal: options.signal });
24
+ onChunk({ type: 'finish', content: result.content ?? '', tool_calls: result.tool_calls, usage: result.usage });
25
+ return { content: result.content ?? '', tool_calls: result.tool_calls, usage: result.usage };
26
+ }
27
+ const think = options.think !== false && ollamaSupportsThinking(model);
28
+ const OLLAMA_BASE = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
29
+ const OLLAMA_DEFAULT = process.env.AI_OLLAMA_DEFAULT_MODEL || 'llama3.2';
30
+ const url = `${OLLAMA_BASE}/api/chat`;
31
+ const ollamaMessages = toOllamaMessages(messages);
32
+ const body = {
33
+ model: model || OLLAMA_DEFAULT,
34
+ messages: ollamaMessages,
35
+ stream: true,
36
+ think,
37
+ options: { num_predict: max_tokens || 1024 },
38
+ };
39
+ if (tools?.length) {
40
+ body.tools = tools.map((t) => ({
41
+ type: 'function',
42
+ function: { name: t.name, description: t.description, parameters: t.parameters ?? {} },
43
+ }));
44
+ }
45
+ let res = await fetch(url, {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify(body),
49
+ signal: options.signal,
50
+ });
51
+ if (!res.ok) {
52
+ const errText = await res.text();
53
+ let didRetryStream = false;
54
+ if (res.status === 400 && errText.includes('does not support tools') && body.tools) {
55
+ delete body.tools;
56
+ console.log('[Ollama] Model does not support tools; retrying without tools');
57
+ res = await fetch(url, {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ body: JSON.stringify(body),
61
+ signal: options.signal,
62
+ });
63
+ didRetryStream = true;
64
+ }
65
+ if (!res.ok) {
66
+ const retryErrText = didRetryStream ? await res.text() : errText;
67
+ if (res.status === 404 && (retryErrText.includes('not found') || retryErrText.includes('model'))) {
68
+ const { pullOllamaModel } = await import('../ollama-setup.js');
69
+ pullOllamaModel(model || OLLAMA_DEFAULT);
70
+ throw new Error(`Model "${model || OLLAMA_DEFAULT}" not found. Pulling now; try again in 1–2 min.`);
71
+ }
72
+ throw new Error(`Ollama: ${res.status} ${retryErrText}`);
73
+ }
74
+ }
75
+ const reader = res.body?.getReader();
76
+ if (!reader) {
77
+ const result = await complete({ ...options, provider, messages, model, max_tokens, tools, signal: options.signal });
78
+ onChunk({ type: 'finish', content: result.content ?? '', tool_calls: result.tool_calls, usage: result.usage });
79
+ return { content: result.content ?? '', tool_calls: result.tool_calls, usage: result.usage };
80
+ }
81
+ const dec = new TextDecoder();
82
+ let buffer = '';
83
+ let content = '';
84
+ let usage;
85
+ let tool_calls;
86
+ try {
87
+ while (true) {
88
+ const { done, value } = await reader.read();
89
+ if (done)
90
+ break;
91
+ buffer += dec.decode(value, { stream: true });
92
+ const lines = buffer.split('\n');
93
+ buffer = lines.pop() ?? '';
94
+ for (const line of lines) {
95
+ if (!line.trim())
96
+ continue;
97
+ try {
98
+ const obj = JSON.parse(line);
99
+ if (obj.message?.thinking) {
100
+ onChunk({ type: 'thinking', delta: obj.message.thinking });
101
+ }
102
+ if (obj.message?.content) {
103
+ content += obj.message.content;
104
+ onChunk({ type: 'delta', content: obj.message.content });
105
+ }
106
+ if (obj.message?.tool_calls?.length) {
107
+ tool_calls = fromOllamaToolCalls(obj.message.tool_calls);
108
+ }
109
+ if (obj.done) {
110
+ if (obj.eval_count != null)
111
+ usage = { completion_tokens: obj.eval_count };
112
+ }
113
+ }
114
+ catch {
115
+ // skip malformed line
116
+ }
117
+ }
118
+ }
119
+ if (buffer.trim()) {
120
+ try {
121
+ const obj = JSON.parse(buffer);
122
+ if (obj.message?.thinking)
123
+ onChunk({ type: 'thinking', delta: obj.message.thinking });
124
+ if (obj.message?.content) {
125
+ content += obj.message.content;
126
+ onChunk({ type: 'delta', content: obj.message.content });
127
+ }
128
+ if (obj.message?.tool_calls?.length)
129
+ tool_calls = fromOllamaToolCalls(obj.message.tool_calls);
130
+ if (obj.done && obj.eval_count != null)
131
+ usage = { completion_tokens: obj.eval_count };
132
+ }
133
+ catch {
134
+ // ignore
135
+ }
136
+ }
137
+ }
138
+ finally {
139
+ reader.releaseLock();
140
+ }
141
+ onChunk({ type: 'finish', content, tool_calls, usage });
142
+ return { content, tool_calls, usage };
143
+ }
21
144
  const streamClient = provider === 'openai' ? openAiStreamClient : provider === 'openrouter' ? openRouterStreamClient : null;
22
145
  const streamDefaultModel = provider === 'openai' ? OPENAI_DEFAULT : provider === 'openrouter' ? OPENROUTER_DEFAULT : '';
23
146
  if (!streamClient || !streamDefaultModel) {
@@ -25,6 +148,12 @@ export async function completeStream(options, onChunk) {
25
148
  onChunk({ type: 'finish', content: result.content ?? '', tool_calls: result.tool_calls, usage: result.usage });
26
149
  return { content: result.content ?? '', tool_calls: result.tool_calls, usage: result.usage };
27
150
  }
151
+ // Tool calling: use non-streaming so tool_calls.arguments are complete JSON (streaming can truncate and cause JSON.parse errors).
152
+ if (tools?.length) {
153
+ const result = await complete({ ...options, provider, messages, model, max_tokens, tools, signal: options.signal });
154
+ onChunk({ type: 'finish', content: result.content ?? '', tool_calls: result.tool_calls, usage: result.usage });
155
+ return { content: result.content ?? '', tool_calls: result.tool_calls, usage: result.usage };
156
+ }
28
157
  const openAiMessages = messages.map((m) => {
29
158
  if (!m.tool_calls?.length)
30
159
  return m;
@@ -87,7 +216,7 @@ export async function completeStream(options, onChunk) {
87
216
  let openAiStreamClient = null;
88
217
  let openRouterStreamClient = null;
89
218
  export async function complete(options = {}) {
90
- const { provider = defaultProvider, model, messages, max_tokens = 1024, task_id = null, tools, signal, } = options;
219
+ const { provider = process.env.AI_DEFAULT_PROVIDER ?? 'ollama', model, messages, max_tokens = 1024, task_id = null, tools, signal, } = options;
91
220
  const start = Date.now();
92
221
  const adapter = getProvider(provider);
93
222
  const id = `ai_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
@@ -96,8 +225,14 @@ export async function complete(options = {}) {
96
225
  result = await adapter.complete({ model, messages: messages ?? [], max_tokens, tools, signal });
97
226
  }
98
227
  catch (err) {
99
- log('ai', 'error', `Completion failed: ${err instanceof Error ? err.message : err}`, {
228
+ const errObj = err;
229
+ const detail = errObj?.status && errObj?.error != null
230
+ ? `${errObj.status} ${typeof errObj.error === 'object' && errObj.error && 'message' in errObj.error ? errObj.error.message : JSON.stringify(errObj.error)}`
231
+ : err instanceof Error ? err.message : String(err);
232
+ log('ai', 'error', `Completion failed: ${detail}`, {
100
233
  provider,
234
+ model: model || adapter.defaultModel,
235
+ status: errObj?.status,
101
236
  stack: err instanceof Error ? err.stack : undefined,
102
237
  });
103
238
  throw err;
@@ -127,234 +262,386 @@ function stubAdapter(defaultModel = 'stub') {
127
262
  },
128
263
  };
129
264
  }
265
+ const OLLAMA_DEFAULT = process.env.AI_OLLAMA_DEFAULT_MODEL || 'llama3.2';
266
+ const OLLAMA_THINKING_MODELS = ['deepseek-r1', 'qwen3', 'deepseek-v3.1', 'gpt-oss'];
267
+ function ollamaSupportsThinking(model) {
268
+ const m = (model || OLLAMA_DEFAULT).toLowerCase();
269
+ return OLLAMA_THINKING_MODELS.some((t) => m === t || m.startsWith(t + ':'));
270
+ }
271
+ /** Convert agent messages to Ollama format (tool_calls use index/object args; tool results use tool_name). */
272
+ function toOllamaMessages(messages) {
273
+ return messages.map((m) => {
274
+ const base = {
275
+ role: m.role,
276
+ content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? ''),
277
+ };
278
+ if (m.role === 'assistant' && m.tool_calls?.length) {
279
+ base.tool_calls = m.tool_calls.map((tc, i) => ({
280
+ type: 'function',
281
+ function: {
282
+ index: i,
283
+ name: tc.name,
284
+ arguments: (() => {
285
+ try {
286
+ return typeof tc.arguments === 'string' ? JSON.parse(tc.arguments) : (tc.arguments ?? {});
287
+ }
288
+ catch {
289
+ return {};
290
+ }
291
+ })(),
292
+ },
293
+ }));
294
+ }
295
+ if (m.role === 'tool') {
296
+ base.tool_name = m.name ?? '';
297
+ }
298
+ return base;
299
+ });
300
+ }
301
+ /** Parse Ollama tool_calls (arguments may be object or string) into agent format. */
302
+ function fromOllamaToolCalls(toolCalls) {
303
+ if (!toolCalls?.length)
304
+ return undefined;
305
+ return toolCalls.map((tc, i) => {
306
+ const fn = tc.function;
307
+ const name = fn?.name ?? '';
308
+ const args = fn?.arguments;
309
+ const argsStr = typeof args === 'string' ? args : JSON.stringify(args ?? {});
310
+ return { id: `ollama_${i}_${Date.now()}`, name, arguments: argsStr };
311
+ });
312
+ }
130
313
  const OPENAI_DEFAULT = process.env.AI_OPENAI_DEFAULT_MODEL || 'gpt-4o-mini';
131
314
  const OPENROUTER_DEFAULT = process.env.AI_OPENROUTER_DEFAULT_MODEL || 'openai/gpt-4o-mini';
132
- registerProvider('stub', stubAdapter('stub'));
133
- if (process.env.OPENAI_API_KEY) {
134
- try {
135
- const mod = await import('openai').catch(() => ({}));
136
- const OpenAI = mod.OpenAI;
137
- if (OpenAI) {
138
- const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
139
- openAiStreamClient = client;
140
- registerProvider('openai', {
141
- defaultModel: OPENAI_DEFAULT,
142
- async complete({ model, messages, max_tokens, tools: toolsOpt, signal }) {
143
- // OpenAI expects tool_calls as { id, type: "function", function: { name, arguments } }
144
- const openAiMessages = (messages ?? []).map((m) => {
145
- if (!m.tool_calls?.length)
146
- return m;
147
- return {
148
- ...m,
149
- tool_calls: m.tool_calls.map((tc) => ({
150
- id: tc.id,
151
- type: 'function',
152
- function: { name: tc.name, arguments: tc.arguments ?? '' },
153
- })),
315
+ async function registerAllProviders() {
316
+ providers.clear();
317
+ registerProvider('stub', stubAdapter('stub'));
318
+ if (process.env.OPENAI_API_KEY) {
319
+ try {
320
+ const mod = await import('openai').catch(() => ({}));
321
+ const OpenAI = mod.OpenAI;
322
+ if (OpenAI) {
323
+ const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
324
+ openAiStreamClient = client;
325
+ registerProvider('openai', {
326
+ defaultModel: OPENAI_DEFAULT,
327
+ async complete({ model, messages, max_tokens, tools: toolsOpt, signal }) {
328
+ // OpenAI expects tool_calls as { id, type: "function", function: { name, arguments } }
329
+ const openAiMessages = (messages ?? []).map((m) => {
330
+ if (!m.tool_calls?.length)
331
+ return m;
332
+ return {
333
+ ...m,
334
+ tool_calls: m.tool_calls.map((tc) => ({
335
+ id: tc.id,
336
+ type: 'function',
337
+ function: { name: tc.name, arguments: tc.arguments ?? '' },
338
+ })),
339
+ };
340
+ });
341
+ const createOpts = {
342
+ model: model || OPENAI_DEFAULT,
343
+ messages: openAiMessages,
344
+ max_tokens: max_tokens || 1024,
154
345
  };
155
- });
156
- const createOpts = {
157
- model: model || OPENAI_DEFAULT,
158
- messages: openAiMessages,
159
- max_tokens: max_tokens || 1024,
160
- };
161
- if (toolsOpt?.length) {
162
- createOpts.tools = toolsOpt.map((t) => ({
163
- type: 'function',
164
- function: { name: t.name, description: t.description, parameters: t.parameters ?? {} },
346
+ if (toolsOpt?.length) {
347
+ createOpts.tools = toolsOpt.map((t) => ({
348
+ type: 'function',
349
+ function: { name: t.name, description: t.description, parameters: t.parameters ?? {} },
350
+ }));
351
+ }
352
+ const res = await client.chat.completions.create(createOpts, signal ? { signal } : undefined);
353
+ const choice = res.choices?.[0];
354
+ const msg = choice?.message;
355
+ const tool_calls = msg?.tool_calls?.map((tc) => ({
356
+ id: tc.id,
357
+ name: tc.function?.name ?? '',
358
+ arguments: tc.function?.arguments ?? '',
165
359
  }));
166
- }
167
- const res = await client.chat.completions.create(createOpts, signal ? { signal } : undefined);
168
- const choice = res.choices?.[0];
169
- const msg = choice?.message;
170
- const tool_calls = msg?.tool_calls?.map((tc) => ({
171
- id: tc.id,
172
- name: tc.function?.name ?? '',
173
- arguments: tc.function?.arguments ?? '',
174
- }));
175
- return {
176
- content: msg?.content ?? '',
177
- usage: res.usage ?? {},
178
- ...(tool_calls?.length ? { tool_calls } : {}),
179
- };
180
- },
181
- });
360
+ return {
361
+ content: msg?.content ?? '',
362
+ usage: res.usage ?? {},
363
+ ...(tool_calls?.length ? { tool_calls } : {}),
364
+ };
365
+ },
366
+ });
367
+ }
368
+ }
369
+ catch {
370
+ // import failed
182
371
  }
183
372
  }
184
- catch {
185
- // import failed
186
- }
187
- }
188
- if (process.env.OPENROUTER_API_KEY) {
189
- try {
190
- const mod = await import('openai').catch(() => ({}));
191
- const OpenAI = mod.OpenAI;
192
- if (OpenAI) {
193
- const client = new OpenAI({
194
- apiKey: process.env.OPENROUTER_API_KEY,
195
- baseURL: 'https://openrouter.ai/api/v1',
196
- });
197
- openRouterStreamClient = client;
198
- registerProvider('openrouter', {
199
- defaultModel: OPENROUTER_DEFAULT,
200
- async complete({ model, messages, max_tokens, tools: toolsOpt, signal }) {
201
- const openAiMessages = (messages ?? []).map((m) => {
202
- if (!m.tool_calls?.length)
203
- return m;
204
- return {
205
- ...m,
206
- tool_calls: m.tool_calls.map((tc) => ({
207
- id: tc.id,
208
- type: 'function',
209
- function: { name: tc.name, arguments: tc.arguments ?? '' },
210
- })),
373
+ if (process.env.OPENROUTER_API_KEY) {
374
+ try {
375
+ const mod = await import('openai').catch(() => ({}));
376
+ const OpenAI = mod.OpenAI;
377
+ if (OpenAI) {
378
+ const client = new OpenAI({
379
+ apiKey: process.env.OPENROUTER_API_KEY,
380
+ baseURL: 'https://openrouter.ai/api/v1',
381
+ });
382
+ openRouterStreamClient = client;
383
+ registerProvider('openrouter', {
384
+ defaultModel: OPENROUTER_DEFAULT,
385
+ async complete({ model, messages, max_tokens, tools: toolsOpt, signal }) {
386
+ const openAiMessages = (messages ?? []).map((m) => {
387
+ if (!m.tool_calls?.length)
388
+ return m;
389
+ return {
390
+ ...m,
391
+ tool_calls: m.tool_calls.map((tc) => ({
392
+ id: tc.id,
393
+ type: 'function',
394
+ function: { name: tc.name, arguments: tc.arguments ?? '' },
395
+ })),
396
+ };
397
+ });
398
+ const createOpts = {
399
+ model: model || OPENROUTER_DEFAULT,
400
+ messages: openAiMessages,
401
+ max_tokens: max_tokens || 1024,
211
402
  };
212
- });
213
- const createOpts = {
214
- model: model || OPENROUTER_DEFAULT,
215
- messages: openAiMessages,
216
- max_tokens: max_tokens || 1024,
217
- };
218
- if (toolsOpt?.length) {
219
- createOpts.tools = toolsOpt.map((t) => ({
220
- type: 'function',
221
- function: { name: t.name, description: t.description, parameters: t.parameters ?? {} },
403
+ if (toolsOpt?.length) {
404
+ createOpts.tools = toolsOpt.map((t) => ({
405
+ type: 'function',
406
+ function: { name: t.name, description: t.description, parameters: t.parameters ?? {} },
407
+ }));
408
+ }
409
+ const res = await client.chat.completions.create(createOpts, signal ? { signal } : undefined);
410
+ const choice = res.choices?.[0];
411
+ const msg = choice?.message;
412
+ const tool_calls = msg?.tool_calls?.map((tc) => ({
413
+ id: tc.id,
414
+ name: tc.function?.name ?? '',
415
+ arguments: tc.function?.arguments ?? '',
222
416
  }));
223
- }
224
- const res = await client.chat.completions.create(createOpts, signal ? { signal } : undefined);
225
- const choice = res.choices?.[0];
226
- const msg = choice?.message;
227
- const tool_calls = msg?.tool_calls?.map((tc) => ({
228
- id: tc.id,
229
- name: tc.function?.name ?? '',
230
- arguments: tc.function?.arguments ?? '',
231
- }));
232
- return {
233
- content: msg?.content ?? '',
234
- usage: res.usage ?? {},
235
- ...(tool_calls?.length ? { tool_calls } : {}),
236
- };
237
- },
238
- });
417
+ return {
418
+ content: msg?.content ?? '',
419
+ usage: res.usage ?? {},
420
+ ...(tool_calls?.length ? { tool_calls } : {}),
421
+ };
422
+ },
423
+ });
424
+ }
425
+ }
426
+ catch {
427
+ // import failed
239
428
  }
240
429
  }
241
- catch {
242
- // import failed
430
+ if (!providers.has('openai')) {
431
+ registerProvider('openai', stubAdapter(OPENAI_DEFAULT));
243
432
  }
244
- }
245
- if (!providers.has('openai')) {
246
- registerProvider('openai', stubAdapter(OPENAI_DEFAULT));
247
- }
248
- const CLAUDE_DEFAULT = process.env.AI_CLAUDE_DEFAULT_MODEL || 'claude-sonnet-4-6';
249
- if (process.env.ANTHROPIC_API_KEY) {
250
- registerProvider('claude', {
251
- defaultModel: CLAUDE_DEFAULT,
252
- async complete({ model, messages, max_tokens }) {
253
- const key = process.env.ANTHROPIC_API_KEY;
254
- const systemMsg = messages.find((m) => m.role === 'system');
255
- const chatMessages = messages.filter((m) => m.role !== 'system').map((m) => ({
256
- role: m.role === 'assistant' ? 'assistant' : 'user',
257
- content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
258
- }));
259
- const body = {
260
- model: model || CLAUDE_DEFAULT,
261
- max_tokens: max_tokens || 1024,
262
- messages: chatMessages,
263
- ...(systemMsg && { system: typeof systemMsg.content === 'string' ? systemMsg.content : JSON.stringify(systemMsg.content) }),
433
+ const CLAUDE_DEFAULT = process.env.AI_CLAUDE_DEFAULT_MODEL || 'claude-sonnet-4-6';
434
+ if (process.env.ANTHROPIC_API_KEY) {
435
+ registerProvider('claude', {
436
+ defaultModel: CLAUDE_DEFAULT,
437
+ async complete({ model, messages, max_tokens }) {
438
+ const key = process.env.ANTHROPIC_API_KEY;
439
+ const systemMsg = messages.find((m) => m.role === 'system');
440
+ const chatMessages = messages.filter((m) => m.role !== 'system').map((m) => ({
441
+ role: m.role === 'assistant' ? 'assistant' : 'user',
442
+ content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
443
+ }));
444
+ const body = {
445
+ model: model || CLAUDE_DEFAULT,
446
+ max_tokens: max_tokens || 1024,
447
+ messages: chatMessages,
448
+ ...(systemMsg && { system: typeof systemMsg.content === 'string' ? systemMsg.content : JSON.stringify(systemMsg.content) }),
449
+ };
450
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
451
+ method: 'POST',
452
+ headers: {
453
+ 'Content-Type': 'application/json',
454
+ 'x-api-key': key,
455
+ 'anthropic-version': '2023-06-01',
456
+ },
457
+ body: JSON.stringify(body),
458
+ });
459
+ if (!res.ok) {
460
+ const err = await res.text();
461
+ throw new Error(`Anthropic API: ${res.status} ${err}`);
462
+ }
463
+ const data = (await res.json());
464
+ const content = data.content?.find((c) => c.type === 'text')?.text || '';
465
+ const usage = data.usage || {};
466
+ return { content, usage: { prompt_tokens: usage.input_tokens ?? 0, completion_tokens: usage.output_tokens ?? 0 } };
467
+ },
468
+ });
469
+ }
470
+ else {
471
+ registerProvider('claude', stubAdapter(CLAUDE_DEFAULT));
472
+ }
473
+ const GEMINI_DEFAULT = process.env.AI_GEMINI_DEFAULT_MODEL || 'gemini-2.5-flash';
474
+ const GEMINI_KEY = process.env.GOOGLE_GEMINI_API_KEY || process.env.GEMINI_API_KEY;
475
+ if (GEMINI_KEY) {
476
+ registerProvider('gemini', {
477
+ defaultModel: GEMINI_DEFAULT,
478
+ async complete({ model, messages, max_tokens }) {
479
+ const modelId = model || GEMINI_DEFAULT;
480
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelId}:generateContent?key=${GEMINI_KEY}`;
481
+ const contents = messages
482
+ .filter((m) => m.role !== 'system')
483
+ .map((m) => ({
484
+ role: (m.role === 'assistant' ? 'model' : 'user'),
485
+ parts: [{ text: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }],
486
+ }));
487
+ const systemInstruction = messages.find((m) => m.role === 'system');
488
+ const body = {
489
+ contents: contents.length ? contents : [{ role: 'user', parts: [{ text: '' }] }],
490
+ generationConfig: { maxOutputTokens: max_tokens || 1024 },
491
+ ...(systemInstruction && {
492
+ systemInstruction: { parts: [{ text: typeof systemInstruction.content === 'string' ? systemInstruction.content : JSON.stringify(systemInstruction.content) }] },
493
+ }),
494
+ };
495
+ const res = await fetch(url, {
496
+ method: 'POST',
497
+ headers: { 'Content-Type': 'application/json' },
498
+ body: JSON.stringify(body),
499
+ });
500
+ if (!res.ok) {
501
+ const err = await res.text();
502
+ throw new Error(`Gemini API: ${res.status} ${err}`);
503
+ }
504
+ const data = (await res.json());
505
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
506
+ const usage = data.usageMetadata || {};
507
+ return {
508
+ content: text,
509
+ usage: { prompt_tokens: usage.promptTokenCount ?? 0, completion_tokens: usage.candidatesTokenCount ?? 0 },
510
+ };
511
+ },
512
+ });
513
+ }
514
+ else {
515
+ registerProvider('gemini', stubAdapter(GEMINI_DEFAULT));
516
+ }
517
+ const OLLAMA_DEFAULT = process.env.AI_OLLAMA_DEFAULT_MODEL || 'llama3.2';
518
+ const OLLAMA_BASE = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
519
+ const OLLAMA_THINKING_MODELS = ['deepseek-r1', 'qwen3', 'deepseek-v3.1', 'gpt-oss'];
520
+ function ollamaSupportsThinking(model) {
521
+ const m = (model || OLLAMA_DEFAULT).toLowerCase();
522
+ return OLLAMA_THINKING_MODELS.some((t) => m === t || m.startsWith(t + ':'));
523
+ }
524
+ /** Convert agent messages to Ollama format (tool_calls use index/object args; tool results use tool_name). */
525
+ function toOllamaMessages(messages) {
526
+ return messages.map((m) => {
527
+ const base = {
528
+ role: m.role,
529
+ content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? ''),
264
530
  };
265
- const res = await fetch('https://api.anthropic.com/v1/messages', {
266
- method: 'POST',
267
- headers: {
268
- 'Content-Type': 'application/json',
269
- 'x-api-key': key,
270
- 'anthropic-version': '2023-06-01',
271
- },
272
- body: JSON.stringify(body),
273
- });
274
- if (!res.ok) {
275
- const err = await res.text();
276
- throw new Error(`Anthropic API: ${res.status} ${err}`);
531
+ if (m.role === 'assistant' && m.tool_calls?.length) {
532
+ base.tool_calls = m.tool_calls.map((tc, i) => ({
533
+ type: 'function',
534
+ function: {
535
+ index: i,
536
+ name: tc.name,
537
+ arguments: (() => {
538
+ try {
539
+ return typeof tc.arguments === 'string' ? JSON.parse(tc.arguments) : (tc.arguments ?? {});
540
+ }
541
+ catch {
542
+ return {};
543
+ }
544
+ })(),
545
+ },
546
+ }));
277
547
  }
278
- const data = (await res.json());
279
- const content = data.content?.find((c) => c.type === 'text')?.text || '';
280
- const usage = data.usage || {};
281
- return { content, usage: { prompt_tokens: usage.input_tokens ?? 0, completion_tokens: usage.output_tokens ?? 0 } };
282
- },
283
- });
284
- }
285
- else {
286
- registerProvider('claude', stubAdapter(CLAUDE_DEFAULT));
287
- }
288
- const GEMINI_DEFAULT = process.env.AI_GEMINI_DEFAULT_MODEL || 'gemini-2.5-flash';
289
- const GEMINI_KEY = process.env.GOOGLE_GEMINI_API_KEY || process.env.GEMINI_API_KEY;
290
- if (GEMINI_KEY) {
291
- registerProvider('gemini', {
292
- defaultModel: GEMINI_DEFAULT,
293
- async complete({ model, messages, max_tokens }) {
294
- const modelId = model || GEMINI_DEFAULT;
295
- const url = `https://generativelanguage.googleapis.com/v1beta/models/${modelId}:generateContent?key=${GEMINI_KEY}`;
296
- const contents = messages
297
- .filter((m) => m.role !== 'system')
298
- .map((m) => ({
299
- role: (m.role === 'assistant' ? 'model' : 'user'),
300
- parts: [{ text: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }],
301
- }));
302
- const systemInstruction = messages.find((m) => m.role === 'system');
548
+ if (m.role === 'tool') {
549
+ base.tool_name = m.name ?? '';
550
+ }
551
+ return base;
552
+ });
553
+ }
554
+ /** Parse Ollama tool_calls (arguments may be object or string) into agent format. */
555
+ function fromOllamaToolCalls(toolCalls) {
556
+ if (!toolCalls?.length)
557
+ return undefined;
558
+ return toolCalls.map((tc, i) => {
559
+ const fn = tc.function;
560
+ const name = fn?.name ?? '';
561
+ const args = fn?.arguments;
562
+ const argsStr = typeof args === 'string' ? args : JSON.stringify(args ?? {});
563
+ return { id: `ollama_${i}_${Date.now()}`, name, arguments: argsStr };
564
+ });
565
+ }
566
+ registerProvider('ollama', {
567
+ defaultModel: OLLAMA_DEFAULT,
568
+ async complete({ model, messages, max_tokens, think, tools: toolsOpt }) {
569
+ const url = `${OLLAMA_BASE}/api/chat`;
570
+ const ollamaMessages = toOllamaMessages(messages ?? []);
303
571
  const body = {
304
- contents: contents.length ? contents : [{ role: 'user', parts: [{ text: '' }] }],
305
- generationConfig: { maxOutputTokens: max_tokens || 1024 },
306
- ...(systemInstruction && {
307
- systemInstruction: { parts: [{ text: typeof systemInstruction.content === 'string' ? systemInstruction.content : JSON.stringify(systemInstruction.content) }] },
308
- }),
572
+ model: model || OLLAMA_DEFAULT,
573
+ messages: ollamaMessages,
574
+ options: { num_predict: max_tokens || 1024 },
575
+ stream: false,
576
+ think: think !== false && ollamaSupportsThinking(model),
309
577
  };
310
- const res = await fetch(url, {
311
- method: 'POST',
312
- headers: { 'Content-Type': 'application/json' },
313
- body: JSON.stringify(body),
314
- });
578
+ if (toolsOpt?.length) {
579
+ body.tools = toolsOpt.map((t) => ({
580
+ type: 'function',
581
+ function: { name: t.name, description: t.description, parameters: t.parameters ?? {} },
582
+ }));
583
+ }
584
+ console.log('[Ollama] POST', url, 'model:', model || OLLAMA_DEFAULT, toolsOpt?.length ? `tools=${toolsOpt.length}` : '');
585
+ let res;
586
+ try {
587
+ res = await fetch(url, {
588
+ method: 'POST',
589
+ headers: { 'Content-Type': 'application/json' },
590
+ body: JSON.stringify(body),
591
+ });
592
+ }
593
+ catch (e) {
594
+ console.error('[Ollama] fetch failed:', e);
595
+ const cause = e?.cause;
596
+ if (cause?.code === 'ECONNREFUSED') {
597
+ throw new Error('Ollama is not running. Start it with: ollama serve (or open the Ollama app).');
598
+ }
599
+ throw e;
600
+ }
315
601
  if (!res.ok) {
316
- const err = await res.text();
317
- throw new Error(`Gemini API: ${res.status} ${err}`);
602
+ const errText = await res.text();
603
+ let didRetry = false;
604
+ if (res.status === 400 && errText.includes('does not support tools') && body.tools) {
605
+ delete body.tools;
606
+ console.log('[Ollama] Model does not support tools; retrying without tools');
607
+ res = await fetch(url, {
608
+ method: 'POST',
609
+ headers: { 'Content-Type': 'application/json' },
610
+ body: JSON.stringify(body),
611
+ });
612
+ didRetry = true;
613
+ }
614
+ if (!res.ok) {
615
+ const err = didRetry ? await res.text() : errText;
616
+ const modelName = model || OLLAMA_DEFAULT;
617
+ if (res.status === 404 && (err.includes('not found') || err.includes('model'))) {
618
+ const { pullOllamaModel } = await import('../ollama-setup.js');
619
+ pullOllamaModel(modelName);
620
+ throw new Error(`Model "${modelName}" not found. Pulling it now (see terminal); try again in 1–2 min. Or run: ollama pull ${modelName}`);
621
+ }
622
+ console.error('[Ollama]', res.status, err);
623
+ throw new Error(`Ollama: ${res.status} ${err}`);
624
+ }
318
625
  }
319
626
  const data = (await res.json());
320
- const text = data.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
321
- const usage = data.usageMetadata || {};
627
+ const msg = data.message;
628
+ const thinking = msg?.thinking ?? '';
629
+ const content = msg?.content ?? '';
630
+ const fullContent = thinking ? thinking + (content ? '\n\n' + content : '') : content;
631
+ const tool_calls = fromOllamaToolCalls(msg?.tool_calls);
322
632
  return {
323
- content: text,
324
- usage: { prompt_tokens: usage.promptTokenCount ?? 0, completion_tokens: usage.candidatesTokenCount ?? 0 },
633
+ content: fullContent,
634
+ usage: data.eval_count != null ? { completion_tokens: data.eval_count } : undefined,
635
+ ...(tool_calls?.length ? { tool_calls } : {}),
325
636
  };
326
637
  },
327
638
  });
639
+ if (!providers.has('llama'))
640
+ registerProvider('llama', getProvider('ollama'));
328
641
  }
329
- else {
330
- registerProvider('gemini', stubAdapter(GEMINI_DEFAULT));
642
+ await registerAllProviders();
643
+ /** Reload AI providers from current process.env (e.g. after onboarding saves new API keys). No restart needed. */
644
+ export async function reloadProviders() {
645
+ await registerAllProviders();
331
646
  }
332
- const OLLAMA_DEFAULT = process.env.AI_OLLAMA_DEFAULT_MODEL || 'llama3.2';
333
- const OLLAMA_BASE = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
334
- registerProvider('ollama', {
335
- defaultModel: OLLAMA_DEFAULT,
336
- async complete({ model, messages, max_tokens }) {
337
- const url = `${OLLAMA_BASE}/api/chat`;
338
- const body = {
339
- model: model || OLLAMA_DEFAULT,
340
- messages: messages.map((m) => ({ role: m.role, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) })),
341
- options: { num_predict: max_tokens || 1024 },
342
- stream: false,
343
- };
344
- const res = await fetch(url, {
345
- method: 'POST',
346
- headers: { 'Content-Type': 'application/json' },
347
- body: JSON.stringify(body),
348
- });
349
- if (!res.ok) {
350
- const err = await res.text();
351
- throw new Error(`Ollama: ${res.status} ${err}`);
352
- }
353
- const data = (await res.json());
354
- const content = data.message?.content || '';
355
- return { content, usage: data.eval_count != null ? { completion_tokens: data.eval_count } : undefined };
356
- },
357
- });
358
- if (!providers.has('llama'))
359
- registerProvider('llama', getProvider('ollama'));
360
647
  //# sourceMappingURL=orchestrator.js.map