freddie 0.0.41

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 (307) hide show
  1. package/AGENTS.md +180 -0
  2. package/CHANGELOG.md +32 -0
  3. package/README.md +130 -0
  4. package/bin/freddie.js +116 -0
  5. package/package.json +59 -0
  6. package/skills/creative/README.md +3 -0
  7. package/skills/creative/architecture-diagram/SKILL.md +52 -0
  8. package/skills/creative/ascii-video/SKILL.md +60 -0
  9. package/skills/creative/concept-diagrams/SKILL.md +65 -0
  10. package/skills/data/README.md +3 -0
  11. package/skills/data/etl-pipelines/SKILL.md +60 -0
  12. package/skills/data/sql-explainer/SKILL.md +60 -0
  13. package/skills/ops/README.md +3 -0
  14. package/skills/ops/incident-response/SKILL.md +74 -0
  15. package/skills/ops/log-triage/SKILL.md +79 -0
  16. package/skills/planning/README.md +3 -0
  17. package/skills/planning/okr-drafter/SKILL.md +60 -0
  18. package/skills/planning/weekly-review/SKILL.md +64 -0
  19. package/skills/software-development/README.md +3 -0
  20. package/skills/software-development/code-review/SKILL.md +70 -0
  21. package/skills/software-development/rfc-writer/SKILL.md +68 -0
  22. package/skills/software-development/systematic-debugging/SKILL.md +80 -0
  23. package/src/acp/auth.js +21 -0
  24. package/src/acp/entry.js +2 -0
  25. package/src/acp/events.js +10 -0
  26. package/src/acp/main.js +8 -0
  27. package/src/acp/permissions.js +29 -0
  28. package/src/acp/server.js +84 -0
  29. package/src/acp/session.js +26 -0
  30. package/src/acp/tools.js +17 -0
  31. package/src/agent/account_usage.js +19 -0
  32. package/src/agent/acptoapi-bridge.js +80 -0
  33. package/src/agent/anthropic_adapter.js +10 -0
  34. package/src/agent/auxiliary_client.js +20 -0
  35. package/src/agent/bedrock_adapter.js +11 -0
  36. package/src/agent/codex_responses_adapter.js +10 -0
  37. package/src/agent/compress/compressor.js +55 -0
  38. package/src/agent/compress/fallback.js +14 -0
  39. package/src/agent/compress/index.js +6 -0
  40. package/src/agent/compress/policy.js +47 -0
  41. package/src/agent/compress/prompt.js +46 -0
  42. package/src/agent/compress/prune.js +16 -0
  43. package/src/agent/compress/tokens.js +31 -0
  44. package/src/agent/context_references.js +40 -0
  45. package/src/agent/copilot_acp_client.js +6 -0
  46. package/src/agent/credential_pool.js +30 -0
  47. package/src/agent/credential_sources.js +18 -0
  48. package/src/agent/curator.js +5 -0
  49. package/src/agent/display.js +23 -0
  50. package/src/agent/error_classifier.js +15 -0
  51. package/src/agent/file_safety.js +9 -0
  52. package/src/agent/gemini_cloudcode_adapter.js +9 -0
  53. package/src/agent/gemini_native_adapter.js +11 -0
  54. package/src/agent/gemini_schema.js +19 -0
  55. package/src/agent/google_code_assist.js +8 -0
  56. package/src/agent/google_oauth.js +21 -0
  57. package/src/agent/image_gen_provider.js +8 -0
  58. package/src/agent/image_gen_registry.js +6 -0
  59. package/src/agent/image_routing.js +13 -0
  60. package/src/agent/insights.js +9 -0
  61. package/src/agent/llm_resolver.js +21 -0
  62. package/src/agent/lmstudio_reasoning.js +13 -0
  63. package/src/agent/machine.js +102 -0
  64. package/src/agent/manual_compression_feedback.js +5 -0
  65. package/src/agent/memory_manager.js +14 -0
  66. package/src/agent/memory_provider.js +1 -0
  67. package/src/agent/model_metadata.js +28 -0
  68. package/src/agent/models_dev.js +13 -0
  69. package/src/agent/moonshot_schema.js +11 -0
  70. package/src/agent/oauth_endpoints.js +79 -0
  71. package/src/agent/onboarding.js +16 -0
  72. package/src/agent/pi-bridge.js +37 -0
  73. package/src/agent/prompt_builder.js +12 -0
  74. package/src/agent/prompt_caching.js +24 -0
  75. package/src/agent/rate_limit_tracker.js +12 -0
  76. package/src/agent/redact.js +25 -0
  77. package/src/agent/retry_utils.js +17 -0
  78. package/src/agent/shell_hooks.js +16 -0
  79. package/src/agent/skill_commands.js +16 -0
  80. package/src/agent/skill_preprocessing.js +12 -0
  81. package/src/agent/skill_utils.js +14 -0
  82. package/src/agent/subdirectory_hints.js +17 -0
  83. package/src/agent/title_generator.js +13 -0
  84. package/src/agent/trajectory.js +9 -0
  85. package/src/agent/usage_pricing.js +16 -0
  86. package/src/auth.js +84 -0
  87. package/src/batch.js +32 -0
  88. package/src/cli/auth_commands.js +17 -0
  89. package/src/cli/azure_detect.js +9 -0
  90. package/src/cli/backup.js +17 -0
  91. package/src/cli/banner.js +13 -0
  92. package/src/cli/browser_connect.js +11 -0
  93. package/src/cli/callbacks.js +5 -0
  94. package/src/cli/claw.js +8 -0
  95. package/src/cli/cli_output.js +19 -0
  96. package/src/cli/clipboard.js +24 -0
  97. package/src/cli/codex_models.js +8 -0
  98. package/src/cli/colors.js +13 -0
  99. package/src/cli/completer.js +98 -0
  100. package/src/cli/completion.js +21 -0
  101. package/src/cli/copilot_auth.js +9 -0
  102. package/src/cli/curator_cli.js +5 -0
  103. package/src/cli/curses.js +15 -0
  104. package/src/cli/debug.js +6 -0
  105. package/src/cli/default_soul.js +20 -0
  106. package/src/cli/dingtalk_auth.js +12 -0
  107. package/src/cli/doctor.js +15 -0
  108. package/src/cli/dump.js +11 -0
  109. package/src/cli/env_loader.js +25 -0
  110. package/src/cli/fallback_cmd.js +9 -0
  111. package/src/cli/gateway_cli.js +17 -0
  112. package/src/cli/hooks.js +9 -0
  113. package/src/cli/interactive.js +61 -0
  114. package/src/cli/logs.js +32 -0
  115. package/src/cli/main.js +7 -0
  116. package/src/cli/mcp_config.js +9 -0
  117. package/src/cli/memory_setup.js +12 -0
  118. package/src/cli/model_catalog.js +23 -0
  119. package/src/cli/model_normalize.js +12 -0
  120. package/src/cli/model_switch.js +11 -0
  121. package/src/cli/models.js +13 -0
  122. package/src/cli/nous_subscription.js +12 -0
  123. package/src/cli/oneshot.js +6 -0
  124. package/src/cli/pairing.js +21 -0
  125. package/src/cli/platforms.js +14 -0
  126. package/src/cli/plugins.js +4 -0
  127. package/src/cli/plugins_cmd.js +21 -0
  128. package/src/cli/profiles_cli.js +6 -0
  129. package/src/cli/providers.js +18 -0
  130. package/src/cli/pty_bridge.js +16 -0
  131. package/src/cli/relaunch.js +7 -0
  132. package/src/cli/runtime_provider.js +9 -0
  133. package/src/cli/setup.js +131 -0
  134. package/src/cli/skills_config.js +6 -0
  135. package/src/cli/skills_hub.js +8 -0
  136. package/src/cli/slack_cli.js +17 -0
  137. package/src/cli/status.js +10 -0
  138. package/src/cli/timeouts.js +5 -0
  139. package/src/cli/tips.js +14 -0
  140. package/src/cli/tools_config.js +15 -0
  141. package/src/cli/uninstall.js +8 -0
  142. package/src/cli/vercel_auth.js +13 -0
  143. package/src/cli/voice.js +6 -0
  144. package/src/cli/web_server.js +13 -0
  145. package/src/cli/webhook.js +12 -0
  146. package/src/commands/profile.js +72 -0
  147. package/src/commands/registry.js +94 -0
  148. package/src/config.js +125 -0
  149. package/src/context/engine.js +42 -0
  150. package/src/cron/cron-parse.js +27 -0
  151. package/src/cron/scheduler.js +63 -0
  152. package/src/db.js +178 -0
  153. package/src/gateway/base.js +13 -0
  154. package/src/gateway/builtin_hooks/boot.js +5 -0
  155. package/src/gateway/builtin_hooks/broadcast.js +3 -0
  156. package/src/gateway/builtin_hooks/deny.js +6 -0
  157. package/src/gateway/builtin_hooks/index.js +17 -0
  158. package/src/gateway/builtin_hooks/presence.js +4 -0
  159. package/src/gateway/builtin_hooks/routing.js +7 -0
  160. package/src/gateway/helpers.js +27 -0
  161. package/src/gateway/platforms/api_server.js +21 -0
  162. package/src/gateway/platforms/bluebubbles.js +32 -0
  163. package/src/gateway/platforms/dingtalk.js +32 -0
  164. package/src/gateway/platforms/discord.js +24 -0
  165. package/src/gateway/platforms/email.js +51 -0
  166. package/src/gateway/platforms/feishu.js +32 -0
  167. package/src/gateway/platforms/feishu_comment.js +12 -0
  168. package/src/gateway/platforms/feishu_comment_rules.js +11 -0
  169. package/src/gateway/platforms/homeassistant.js +32 -0
  170. package/src/gateway/platforms/matrix.js +40 -0
  171. package/src/gateway/platforms/mattermost.js +29 -0
  172. package/src/gateway/platforms/qqbot.js +32 -0
  173. package/src/gateway/platforms/signal.js +33 -0
  174. package/src/gateway/platforms/slack.js +34 -0
  175. package/src/gateway/platforms/sms.js +34 -0
  176. package/src/gateway/platforms/telegram.js +38 -0
  177. package/src/gateway/platforms/telegram_network.js +17 -0
  178. package/src/gateway/platforms/webhook.js +19 -0
  179. package/src/gateway/platforms/wecom.js +32 -0
  180. package/src/gateway/platforms/wecom_callback.js +15 -0
  181. package/src/gateway/platforms/wecom_crypto.js +16 -0
  182. package/src/gateway/platforms/weixin.js +32 -0
  183. package/src/gateway/platforms/whatsapp.js +40 -0
  184. package/src/gateway/platforms/yuanbao.js +9 -0
  185. package/src/gateway/platforms/yuanbao_media.js +5 -0
  186. package/src/gateway/platforms/yuanbao_proto.js +9 -0
  187. package/src/gateway/platforms/yuanbao_sticker.js +6 -0
  188. package/src/gateway/run.js +42 -0
  189. package/src/gateway/service.js +143 -0
  190. package/src/home.js +44 -0
  191. package/src/index.js +47 -0
  192. package/src/mcp/server.js +49 -0
  193. package/src/observability/debug.js +31 -0
  194. package/src/observability/log.js +38 -0
  195. package/src/plugins/achievements/index.js +9 -0
  196. package/src/plugins/cockpit/index.js +8 -0
  197. package/src/plugins/context_engine/index.js +13 -0
  198. package/src/plugins/disk_cleanup/index.js +22 -0
  199. package/src/plugins/google_meet/index.js +19 -0
  200. package/src/plugins/image_gen/index.js +5 -0
  201. package/src/plugins/manager.js +66 -0
  202. package/src/plugins/memory/_index.js +8 -0
  203. package/src/plugins/memory/byterover.js +25 -0
  204. package/src/plugins/memory/hindsight.js +25 -0
  205. package/src/plugins/memory/holographic.js +31 -0
  206. package/src/plugins/memory/honcho.js +25 -0
  207. package/src/plugins/memory/mem0.js +25 -0
  208. package/src/plugins/memory/openviking.js +25 -0
  209. package/src/plugins/memory/provider.js +35 -0
  210. package/src/plugins/memory/retaindb.js +25 -0
  211. package/src/plugins/memory/supermemory.js +25 -0
  212. package/src/plugins/observability/index.js +18 -0
  213. package/src/plugins/platforms/index.js +20 -0
  214. package/src/plugins/spotify/index.js +22 -0
  215. package/src/rl/atropos.js +22 -0
  216. package/src/rl/cli.js +18 -0
  217. package/src/sessions.js +84 -0
  218. package/src/skills/index.js +49 -0
  219. package/src/skin/engine.js +81 -0
  220. package/src/swe/runner.js +26 -0
  221. package/src/time.js +25 -0
  222. package/src/tools/ansi_strip.js +8 -0
  223. package/src/tools/approval.js +15 -0
  224. package/src/tools/bash.js +35 -0
  225. package/src/tools/binary_extensions.js +22 -0
  226. package/src/tools/browser.js +48 -0
  227. package/src/tools/budget_config.js +13 -0
  228. package/src/tools/checkpoint.js +29 -0
  229. package/src/tools/clarify.js +15 -0
  230. package/src/tools/code_execution.js +27 -0
  231. package/src/tools/credential_files.js +16 -0
  232. package/src/tools/cronjob.js +16 -0
  233. package/src/tools/debug_helpers.js +9 -0
  234. package/src/tools/delegate.js +28 -0
  235. package/src/tools/discord_tool.js +13 -0
  236. package/src/tools/edit.js +31 -0
  237. package/src/tools/env_passthrough.js +15 -0
  238. package/src/tools/environments/base.js +26 -0
  239. package/src/tools/environments/daytona.js +48 -0
  240. package/src/tools/environments/docker.js +14 -0
  241. package/src/tools/environments/file_sync.js +60 -0
  242. package/src/tools/environments/index.js +36 -0
  243. package/src/tools/environments/local.js +31 -0
  244. package/src/tools/environments/modal.js +33 -0
  245. package/src/tools/environments/singularity.js +38 -0
  246. package/src/tools/environments/ssh.js +14 -0
  247. package/src/tools/environments/vercel_sandbox.js +47 -0
  248. package/src/tools/feishu_doc.js +15 -0
  249. package/src/tools/feishu_drive.js +14 -0
  250. package/src/tools/file_operations.js +17 -0
  251. package/src/tools/file_state.js +16 -0
  252. package/src/tools/file_tools.js +23 -0
  253. package/src/tools/fuzzy_match.js +8 -0
  254. package/src/tools/grep.js +51 -0
  255. package/src/tools/homeassistant_tool.js +15 -0
  256. package/src/tools/image_gen.js +33 -0
  257. package/src/tools/interrupt.js +18 -0
  258. package/src/tools/managed_tool_gateway.js +11 -0
  259. package/src/tools/mcp_oauth.js +21 -0
  260. package/src/tools/mcp_oauth_manager.js +20 -0
  261. package/src/tools/mcp_tool.js +36 -0
  262. package/src/tools/memory.js +66 -0
  263. package/src/tools/mixture_of_agents.js +14 -0
  264. package/src/tools/neutts_synth.js +13 -0
  265. package/src/tools/openrouter_client.js +13 -0
  266. package/src/tools/osv_check.js +11 -0
  267. package/src/tools/patch_parser.js +42 -0
  268. package/src/tools/path_security.js +16 -0
  269. package/src/tools/process_registry.js +17 -0
  270. package/src/tools/read.js +26 -0
  271. package/src/tools/registry.js +54 -0
  272. package/src/tools/rl_training.js +13 -0
  273. package/src/tools/schema_sanitizer.js +18 -0
  274. package/src/tools/send_message.js +32 -0
  275. package/src/tools/session_search.js +23 -0
  276. package/src/tools/skill_manager.js +17 -0
  277. package/src/tools/skill_usage.js +20 -0
  278. package/src/tools/skills_guard.js +17 -0
  279. package/src/tools/skills_hub.js +31 -0
  280. package/src/tools/skills_index.js +14 -0
  281. package/src/tools/skills_sync.js +19 -0
  282. package/src/tools/skills_tool.js +11 -0
  283. package/src/tools/slash_confirm.js +16 -0
  284. package/src/tools/terminal.js +29 -0
  285. package/src/tools/tirith_security.js +25 -0
  286. package/src/tools/todo.js +54 -0
  287. package/src/tools/tool_backend_helpers.js +26 -0
  288. package/src/tools/tool_output_limits.js +15 -0
  289. package/src/tools/tool_result_storage.js +20 -0
  290. package/src/tools/transcription.js +19 -0
  291. package/src/tools/tts.js +19 -0
  292. package/src/tools/url_safety.js +15 -0
  293. package/src/tools/vision.js +18 -0
  294. package/src/tools/voice_mode.js +10 -0
  295. package/src/tools/web_search.js +37 -0
  296. package/src/tools/web_tools.js +18 -0
  297. package/src/tools/website_policy.js +14 -0
  298. package/src/tools/write.js +25 -0
  299. package/src/tools/xai_http.js +13 -0
  300. package/src/tools/yuanbao_tools.js +13 -0
  301. package/src/toolset_distributions.js +18 -0
  302. package/src/toolsets.js +26 -0
  303. package/src/tui/index.js +26 -0
  304. package/src/utils.js +54 -0
  305. package/src/web/app.js +547 -0
  306. package/src/web/index.html +167 -0
  307. package/src/web/server.js +109 -0
@@ -0,0 +1,31 @@
1
+ export const CHARS_PER_TOKEN = 4
2
+ export const IMAGE_TOKEN_ESTIMATE = 1600
3
+ export const IMAGE_CHAR_EQUIVALENT = IMAGE_TOKEN_ESTIMATE * CHARS_PER_TOKEN
4
+
5
+ const IMAGE_TYPES = new Set(['image_url', 'input_image', 'image'])
6
+
7
+ export function contentLengthForBudget(content) {
8
+ if (typeof content === 'string') return content.length
9
+ if (!Array.isArray(content)) return String(content || '').length
10
+ let total = 0
11
+ for (const part of content) {
12
+ if (typeof part === 'string') { total += part.length; continue }
13
+ if (!part || typeof part !== 'object') { total += String(part || '').length; continue }
14
+ if (IMAGE_TYPES.has(part.type)) { total += IMAGE_CHAR_EQUIVALENT; continue }
15
+ if (typeof part.text === 'string') { total += part.text.length; continue }
16
+ total += JSON.stringify(part).length
17
+ }
18
+ return total
19
+ }
20
+
21
+ export function estimateMessageTokens(message) {
22
+ const contentChars = contentLengthForBudget(message?.content)
23
+ const toolCallsChars = message?.tool_calls ? JSON.stringify(message.tool_calls).length : 0
24
+ return Math.ceil((contentChars + toolCallsChars + 8) / CHARS_PER_TOKEN)
25
+ }
26
+
27
+ export function estimateMessagesTokens(messages = []) {
28
+ let total = 0
29
+ for (const m of messages) total += estimateMessageTokens(m)
30
+ return total
31
+ }
@@ -0,0 +1,40 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { getMessages } from '../sessions.js'
4
+
5
+ const REF_RE = /@(file:|url:|session:)?(\S+)/g
6
+
7
+ export async function resolveReferences(text, { cwd = process.cwd(), maxFile = 50_000 } = {}) {
8
+ if (!text || typeof text !== 'string') return text
9
+ const refs = [...text.matchAll(REF_RE)]
10
+ if (!refs.length) return text
11
+ let out = text
12
+ for (const r of refs) {
13
+ const kind = (r[1] || '').replace(':', '') || guessKind(r[2])
14
+ const target = r[2]
15
+ const expansion = await expand(kind, target, { cwd, maxFile })
16
+ if (expansion) out = out.replace(r[0], `${r[0]}\n\n\`\`\`\n${expansion}\n\`\`\``)
17
+ }
18
+ return out
19
+ }
20
+ function guessKind(t) {
21
+ if (/^https?:\/\//.test(t)) return 'url'
22
+ if (/^[a-f0-9-]{8,}$/.test(t)) return 'session'
23
+ return 'file'
24
+ }
25
+ async function expand(kind, target, { cwd, maxFile }) {
26
+ if (kind === 'file') {
27
+ const p = path.resolve(cwd, target.replace(/^\.\/?/, ''))
28
+ if (!fs.existsSync(p)) return null
29
+ const buf = fs.readFileSync(p, 'utf8')
30
+ return buf.length > maxFile ? buf.slice(0, maxFile) + '\n…[truncated]' : buf
31
+ }
32
+ if (kind === 'url') {
33
+ try { const r = await fetch(target); return (await r.text()).slice(0, maxFile) } catch (e) { return null }
34
+ }
35
+ if (kind === 'session') {
36
+ const msgs = getMessages(target)
37
+ return msgs.map(m => `[${m.role}] ${m.content}`).join('\n').slice(0, maxFile)
38
+ }
39
+ return null
40
+ }
@@ -0,0 +1,6 @@
1
+ export class CopilotAcpClient {
2
+ constructor({ url, token } = {}) { this.url = url || process.env.COPILOT_ACP_URL; this.token = token || process.env.COPILOT_TOKEN }
3
+ headers() { return this.token ? { authorization: 'Bearer ' + this.token } : {} }
4
+ async listModels() { if (!this.url) throw new Error('COPILOT_ACP_URL required'); return await (await fetch(this.url + '/models', { headers: this.headers() })).json() }
5
+ async chat({ messages, model }) { if (!this.url) throw new Error('COPILOT_ACP_URL required'); return await (await fetch(this.url + '/chat', { method: 'POST', headers: { ...this.headers(), 'content-type': 'application/json' }, body: JSON.stringify({ messages, model }) })).json() }
6
+ }
@@ -0,0 +1,30 @@
1
+ import { parseEnvKeyList } from '../utils.js'
2
+
3
+ const COOLDOWN_MS = 60_000
4
+ const _state = new Map()
5
+
6
+ function entry(provider) {
7
+ if (_state.has(provider)) return _state.get(provider)
8
+ const keys = parseEnvKeyList(provider.toUpperCase() + '_API_KEYS')
9
+ const single = process.env[provider.toUpperCase() + '_API_KEY']
10
+ const all = keys.length ? keys : (single ? [single] : [])
11
+ const e = { keys: all, idx: 0, blacklist: new Map() }
12
+ _state.set(provider, e)
13
+ return e
14
+ }
15
+ export function next(provider) {
16
+ const e = entry(provider)
17
+ if (!e.keys.length) return null
18
+ for (let i = 0; i < e.keys.length; i++) {
19
+ const k = e.keys[(e.idx + i) % e.keys.length]
20
+ const until = e.blacklist.get(k) || 0
21
+ if (Date.now() >= until) { e.idx = (e.idx + i + 1) % e.keys.length; return k }
22
+ }
23
+ return null
24
+ }
25
+ export function markFailure(provider, key) {
26
+ entry(provider).blacklist.set(key, Date.now() + COOLDOWN_MS)
27
+ }
28
+ export function clearBlacklist(provider) { entry(provider).blacklist.clear() }
29
+ export function listKeys(provider) { return [...entry(provider).keys] }
30
+ export function resetForTests() { _state.clear() }
@@ -0,0 +1,18 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { getFophHome } from '../home.js'
4
+ import { getAuthStore } from '../auth.js'
5
+ const ENV_MAP = { anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY', groq: 'GROQ_API_KEY', xai: 'XAI_API_KEY', openrouter: 'OPENROUTER_API_KEY', mistral: 'MISTRAL_API_KEY', deepseek: 'DEEPSEEK_API_KEY' }
6
+ export async function resolveKey(provider) {
7
+ const env = ENV_MAP[provider] || (provider.toUpperCase() + '_API_KEY')
8
+ if (process.env[env]) return { source: 'env', value: process.env[env] }
9
+ const stored = await getAuthStore().getCredential(env)
10
+ if (stored?.value) return { source: 'auth-store', value: stored.value }
11
+ const dotEnv = path.join(getFophHome(), '.env')
12
+ if (fs.existsSync(dotEnv)) {
13
+ const m = fs.readFileSync(dotEnv, 'utf8').match(new RegExp('^' + env + '=(.+)$', 'm'))
14
+ if (m) return { source: 'dotenv', value: m[1].replace(/^["']|["']$/g, '') }
15
+ }
16
+ return { source: 'none', value: null }
17
+ }
18
+ export function listProviders() { return Object.keys(ENV_MAP) }
@@ -0,0 +1,5 @@
1
+ import { db } from '../sessions.js'
2
+ function init() { const d = db(); d.exec(`CREATE TABLE IF NOT EXISTS curated (id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT, key TEXT, value TEXT, ts INTEGER NOT NULL)`); return d }
3
+ export function add(kind, key, value) { init().prepare(`INSERT INTO curated (kind, key, value, ts) VALUES (?, ?, ?, ?)`).run(kind, key, JSON.stringify(value), Date.now()); return { added: true } }
4
+ export function list(kind) { return init().prepare(`SELECT * FROM curated WHERE kind = ? ORDER BY id DESC`).all(kind).map(r => ({ ...r, value: JSON.parse(r.value) })) }
5
+ export function clear(kind) { init().prepare(`DELETE FROM curated WHERE kind = ?`).run(kind); return { cleared: kind } }
@@ -0,0 +1,23 @@
1
+ import { getActiveSkin } from '../skin/engine.js'
2
+
3
+ const FRAMES_PER_SECOND = 8
4
+
5
+ export function spinnerFrames() {
6
+ const skin = getActiveSkin()
7
+ return [...skin.spinner.waiting_faces, ...skin.spinner.thinking_faces]
8
+ }
9
+ export function activityPrefix() { return getActiveSkin().tool_prefix || '┊' }
10
+ export function renderActivity(line) { return `${activityPrefix()} ${line}` }
11
+ export function renderResponseLabel() { return getActiveSkin().branding.response_label }
12
+
13
+ export function startSpinner({ output = process.stdout, label = '' } = {}) {
14
+ const frames = spinnerFrames()
15
+ let i = 0
16
+ const t = setInterval(() => {
17
+ const f = frames[i++ % frames.length]
18
+ output.write(`\r${f} ${label}${' '.repeat(8)}`)
19
+ }, Math.floor(1000 / FRAMES_PER_SECOND))
20
+ return {
21
+ stop() { clearInterval(t); output.write('\r' + ' '.repeat(40 + label.length) + '\r') },
22
+ }
23
+ }
@@ -0,0 +1,15 @@
1
+ const CLASSES = [
2
+ [/rate.?limit|429|too many requests/i, 'rate_limit', true],
3
+ [/context.?length|maximum.?context|token.?limit/i, 'context_overflow', false],
4
+ [/api.?key|unauthor|401|403/i, 'auth', false],
5
+ [/timeout|timed out|ETIMEDOUT/i, 'timeout', true],
6
+ [/connection|ENETUNREACH|ECONNREFUSED|ECONNRESET/i, 'network', true],
7
+ [/invalid.?json|malformed/i, 'parse', false],
8
+ [/server error|5\d\d/i, 'server', true],
9
+ ]
10
+ export function classifyError(e) {
11
+ const msg = String(e?.message || e || '')
12
+ for (const [re, kind, retryable] of CLASSES) if (re.test(msg)) return { kind, retryable, message: msg }
13
+ return { kind: 'unknown', retryable: false, message: msg }
14
+ }
15
+ export function isRetryable(e) { return classifyError(e).retryable }
@@ -0,0 +1,9 @@
1
+ import path from 'node:path'
2
+ const FORBIDDEN_PATTERNS = [/(^|[\\/])etc[\\/]passwd$/i, /(^|[\\/])etc[\\/]shadow$/i, /[\\/]\.ssh[\\/]id_/i, /[\\/]\.aws[\\/]credentials$/i, /[\\/]\.docker[\\/]config\.json$/i, /[\\/]\.npmrc$/i, /[\\/]\.pypirc$/i]
3
+ export function checkFileSafety(p, { cwd = process.cwd(), op = 'read' } = {}) {
4
+ const abs = path.resolve(cwd, p)
5
+ const norm = abs.replace(/\\/g, '/')
6
+ for (const re of FORBIDDEN_PATTERNS) if (re.test(norm)) return { safe: false, reason: 'matches forbidden pattern: ' + re.source }
7
+ if (op === 'write' && /^\/(bin|usr|etc|sys|proc)\//i.test(norm)) return { safe: false, reason: 'system path write blocked' }
8
+ return { safe: true, abs }
9
+ }
@@ -0,0 +1,9 @@
1
+ import { resolveKey } from './credential_sources.js'
2
+ const ENDPOINT = 'https://cloudcode-pa.googleapis.com/v1internal:generateContent'
3
+ export async function chat({ messages, model = 'gemini-2.5-pro' } = {}) {
4
+ const k = await resolveKey('google_oauth')
5
+ if (!k.value) throw new Error('GOOGLE_OAUTH_TOKEN required for cloudcode')
6
+ const r = await fetch(ENDPOINT, { method: 'POST', headers: { authorization: 'Bearer ' + k.value, 'content-type': 'application/json' }, body: JSON.stringify({ model, messages }) })
7
+ return await r.json()
8
+ }
9
+ export const provider = 'gemini_cloudcode'
@@ -0,0 +1,11 @@
1
+ import { resolveKey } from './credential_sources.js'
2
+ import { adaptToolForGemini, adaptMessagesForGemini } from './gemini_schema.js'
3
+ export async function chat({ messages, model = 'gemini-2.5-flash', tools = [] } = {}) {
4
+ const k = await resolveKey('google')
5
+ if (!k.value) throw new Error('GOOGLE_API_KEY required')
6
+ const url = 'https://generativelanguage.googleapis.com/v1beta/models/' + model + ':generateContent?key=' + k.value
7
+ const body = { contents: adaptMessagesForGemini(messages), ...(tools.length ? { tools: [{ function_declarations: tools.map(adaptToolForGemini) }] } : {}) }
8
+ const r = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
9
+ return await r.json()
10
+ }
11
+ export const provider = 'google'
@@ -0,0 +1,19 @@
1
+ function clean(o) {
2
+ if (Array.isArray(o)) return o.map(clean)
3
+ if (!o || typeof o !== 'object') return o
4
+ const out = {}
5
+ for (const [k, v] of Object.entries(o)) {
6
+ if (k === '$schema' || k === 'additionalProperties' || k === '$ref' || k === 'oneOf' || k === 'anyOf') continue
7
+ out[k] = clean(v)
8
+ }
9
+ return out
10
+ }
11
+ export function adaptToolForGemini(tool) {
12
+ return { name: tool.name, description: tool.description || '', parameters: clean(tool.input_schema || tool.parameters || { type: 'object', properties: {} }) }
13
+ }
14
+ export function adaptMessagesForGemini(messages) {
15
+ return messages.filter(m => m.role !== 'system').map(m => ({
16
+ role: m.role === 'assistant' ? 'model' : 'user',
17
+ parts: typeof m.content === 'string' ? [{ text: m.content }] : (Array.isArray(m.content) ? m.content.map(p => p.text ? { text: p.text } : p) : [{ text: JSON.stringify(m.content) }]),
18
+ }))
19
+ }
@@ -0,0 +1,8 @@
1
+ import { getToken } from './google_oauth.js'
2
+ const ENDPOINT = 'https://cloudcode-pa.googleapis.com/v1internal'
3
+ export async function complete({ prompt, language = 'auto' } = {}) {
4
+ const t = (await getToken()).value
5
+ if (!t) throw new Error('GOOGLE_OAUTH_TOKEN required')
6
+ const r = await fetch(ENDPOINT + ':complete', { method: 'POST', headers: { authorization: 'Bearer ' + t, 'content-type': 'application/json' }, body: JSON.stringify({ prompt, language }) })
7
+ return await r.json()
8
+ }
@@ -0,0 +1,21 @@
1
+ import { getAuthStore } from '../auth.js'
2
+ const KEY = 'GOOGLE_OAUTH_TOKEN'
3
+ const REFRESH_KEY = 'GOOGLE_OAUTH_REFRESH'
4
+ export async function getToken() {
5
+ if (process.env.GOOGLE_OAUTH_TOKEN) return { source: 'env', value: process.env.GOOGLE_OAUTH_TOKEN }
6
+ const stored = await getAuthStore().getCredential(KEY)
7
+ return stored?.value ? { source: 'auth-store', value: stored.value } : { source: 'none', value: null }
8
+ }
9
+ export async function setToken(token, refreshToken = null) {
10
+ await getAuthStore().setCredential(KEY, token)
11
+ if (refreshToken) await getAuthStore().setCredential(REFRESH_KEY, refreshToken)
12
+ return { stored: true }
13
+ }
14
+ export async function refresh({ clientId, clientSecret } = {}) {
15
+ const refreshToken = (await getAuthStore().getCredential(REFRESH_KEY))?.value
16
+ if (!refreshToken) return { error: 'no refresh token' }
17
+ const r = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken, client_id: clientId, client_secret: clientSecret }).toString() })
18
+ const j = await r.json()
19
+ if (j.access_token) await setToken(j.access_token)
20
+ return j
21
+ }
@@ -0,0 +1,8 @@
1
+ import { resolveKey } from './credential_sources.js'
2
+ const PROVIDERS = {
3
+ openai: async ({ prompt, size, model }) => { const k = (await resolveKey('openai')).value; const r = await fetch('https://api.openai.com/v1/images/generations', { method: 'POST', headers: { authorization: 'Bearer ' + k, 'content-type': 'application/json' }, body: JSON.stringify({ model: model || 'gpt-image-1', prompt, size }) }); return await r.json() },
4
+ replicate: async ({ prompt, model }) => { const k = process.env.REPLICATE_API_TOKEN; const r = await fetch('https://api.replicate.com/v1/predictions', { method: 'POST', headers: { authorization: 'Token ' + k, 'content-type': 'application/json' }, body: JSON.stringify({ version: model || 'black-forest-labs/flux-schnell', input: { prompt } }) }); return await r.json() },
5
+ stability: async ({ prompt, model }) => { const k = process.env.STABILITY_API_KEY; const r = await fetch('https://api.stability.ai/v2beta/stable-image/generate/' + (model || 'core'), { method: 'POST', headers: { authorization: 'Bearer ' + k, accept: 'application/json' }, body: (() => { const fd = new FormData(); fd.append('prompt', prompt); return fd })() }); return await r.json() },
6
+ }
7
+ export async function generate({ provider = 'openai', ...args } = {}) { const fn = PROVIDERS[provider]; if (!fn) throw new Error('unknown image provider: ' + provider); return await fn(args) }
8
+ export function listProviders() { return Object.keys(PROVIDERS) }
@@ -0,0 +1,6 @@
1
+ import { generate, listProviders } from './image_gen_provider.js'
2
+ const _generations = []
3
+ export async function generateAndRecord(args) { const out = await generate(args); _generations.push({ ts: Date.now(), provider: args.provider || 'openai', prompt: args.prompt, result: out }); return out }
4
+ export function listGenerations(limit = 50) { return _generations.slice(-limit).reverse() }
5
+ export function clearGenerations() { _generations.length = 0 }
6
+ export { listProviders }
@@ -0,0 +1,13 @@
1
+ const ROUTERS = {
2
+ anthropic: (parts) => parts.map(p => p.type === 'image_url' ? { type: 'image', source: { type: 'base64', media_type: p.image_url?.media_type || 'image/png', data: p.image_url?.url?.split(',').pop() || '' } } : p),
3
+ openai: (parts) => parts.map(p => p.type === 'image' ? { type: 'image_url', image_url: { url: typeof p.source === 'string' ? p.source : ('data:' + (p.source?.media_type || 'image/png') + ';base64,' + (p.source?.data || '')) } } : p),
4
+ google: (parts) => parts.map(p => p.type === 'image_url' ? { inline_data: { mime_type: p.image_url?.media_type || 'image/png', data: p.image_url?.url?.split(',').pop() || '' } } : p),
5
+ }
6
+ export function routeImagesNative(messages, provider = 'anthropic') {
7
+ const router = ROUTERS[provider] || ((p) => p)
8
+ return messages.map(m => {
9
+ if (!Array.isArray(m.content)) return m
10
+ return { ...m, content: router(m.content) }
11
+ })
12
+ }
13
+ export function listImageProviders() { return Object.keys(ROUTERS) }
@@ -0,0 +1,9 @@
1
+ import { db } from '../sessions.js'
2
+ import { calculateCost } from './usage_pricing.js'
3
+ export function modelInsights() {
4
+ const rows = db().prepare(`SELECT model, SUM(prompt_tokens) AS p, SUM(completion_tokens) AS c, SUM(cost_usd) AS cost, COUNT(*) AS calls FROM account_usage GROUP BY model`).all()
5
+ return rows.map(r => ({ ...r, has_pricing: calculateCost({ model: r.model, prompt_tokens: 1, completion_tokens: 1 }) > 0 }))
6
+ }
7
+ export function sessionInsights(sessionId) {
8
+ return db().prepare(`SELECT model, SUM(prompt_tokens) AS p, SUM(completion_tokens) AS c, SUM(cost_usd) AS cost FROM account_usage WHERE session_id = ? GROUP BY model`).all(sessionId)
9
+ }
@@ -0,0 +1,21 @@
1
+ import { callLLM as acptoapiCall, isReachable as acptoapiReachable } from './acptoapi-bridge.js'
2
+ import { callLLM as piCall } from './pi-bridge.js'
3
+
4
+ const KEYS = { anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY', groq: 'GROQ_API_KEY', openrouter: 'OPENROUTER_API_KEY' }
5
+
6
+ export function resolveCallLLM({ provider, model } = {}) {
7
+ return async (input) => {
8
+ const explicitProvider = provider || input.provider
9
+ const explicitKey = explicitProvider && KEYS[explicitProvider] ? process.env[KEYS[explicitProvider]] : null
10
+ if (explicitProvider && explicitKey) {
11
+ return await piCall({ ...input, provider: explicitProvider, model: model || input.model })
12
+ }
13
+ if (await acptoapiReachable()) {
14
+ return await acptoapiCall({ ...input, model: model || input.model })
15
+ }
16
+ for (const [p, k] of Object.entries(KEYS)) {
17
+ if (process.env[k]) return await piCall({ ...input, provider: p, model: model || input.model })
18
+ }
19
+ throw new Error('no LLM backend reachable: start acptoapi (http://127.0.0.1:4800/v1) or set ANTHROPIC_API_KEY/OPENAI_API_KEY/GROQ_API_KEY/OPENROUTER_API_KEY')
20
+ }
21
+ }
@@ -0,0 +1,13 @@
1
+ export function extractReasoning(content) {
2
+ const tags = [/<think>([\s\S]*?)<\/think>/g, /<reasoning>([\s\S]*?)<\/reasoning>/g]
3
+ const reasoning = []
4
+ let cleaned = String(content || '')
5
+ for (const re of tags) {
6
+ for (const m of cleaned.matchAll(re)) reasoning.push(m[1])
7
+ cleaned = cleaned.replace(re, '')
8
+ }
9
+ return { reasoning: reasoning.join('\n').trim(), content: cleaned.trim() }
10
+ }
11
+ export function isLmStudio(provider, baseUrl) {
12
+ return provider === 'lmstudio' || /(^|\.)lmstudio\.|:1234\b|:8000\b/.test(String(baseUrl || ''))
13
+ }
@@ -0,0 +1,102 @@
1
+ import { createMachine, createActor, assign, fromPromise } from 'xstate'
2
+ import { registry } from '../tools/registry.js'
3
+ import { getEnabledToolSchemas } from '../toolsets.js'
4
+ import { logger } from '../observability/log.js'
5
+ import { resolveCallLLM } from './llm_resolver.js'
6
+
7
+ const log = logger('agent')
8
+
9
+ export function createAgentMachine({ provider, model, maxIterations = 90, callLLM, enabledToolsets = ['core'], disabledToolsets = [] } = {}) {
10
+ const llm = callLLM || resolveCallLLM({ provider, model })
11
+ return createMachine({
12
+ id: 'freddie-agent',
13
+ initial: 'idle',
14
+ output: ({ context }) => ({ messages: context.messages, result: context.lastResult, error: context.error, iterations: context.iterations }),
15
+ context: ({ input }) => ({
16
+ messages: input?.messages ? [...input.messages] : [],
17
+ iterations: 0,
18
+ maxIterations,
19
+ interrupt: false,
20
+ lastResult: null,
21
+ error: null,
22
+ provider, model,
23
+ enabledToolsets, disabledToolsets,
24
+ }),
25
+ states: {
26
+ idle: {
27
+ on: {
28
+ SUBMIT: {
29
+ target: 'prompting',
30
+ actions: assign({
31
+ messages: ({ context, event }) => [...context.messages, { role: 'user', content: event.prompt }],
32
+ iterations: 0, interrupt: false, error: null,
33
+ }),
34
+ },
35
+ INTERRUPT: { actions: assign({ interrupt: true }) },
36
+ },
37
+ },
38
+ prompting: {
39
+ invoke: {
40
+ src: fromPromise(async ({ input }) => {
41
+ const schemas = await getEnabledToolSchemas(input.enabledToolsets, input.disabledToolsets)
42
+ return llm({ messages: input.messages, tools: schemas, model: input.model, provider: input.provider })
43
+ }),
44
+ input: ({ context }) => ({ messages: context.messages, model: context.model, provider: context.provider, enabledToolsets: context.enabledToolsets, disabledToolsets: context.disabledToolsets }),
45
+ onDone: [
46
+ { guard: ({ event }) => Array.isArray(event.output?.tool_calls) && event.output.tool_calls.length > 0, target: 'tool_calls', actions: assign({ messages: ({ context, event }) => [...context.messages, { role: 'assistant', content: event.output.content || '', tool_calls: event.output.tool_calls }] }) },
47
+ { target: 'done', actions: assign({ messages: ({ context, event }) => [...context.messages, { role: 'assistant', content: event.output.content || '' }], lastResult: ({ event }) => event.output.content || '' }) },
48
+ ],
49
+ onError: { target: 'done', actions: assign({ error: ({ event }) => String(event.error?.message || event.error) }) },
50
+ },
51
+ },
52
+ tool_calls: {
53
+ always: [
54
+ { guard: ({ context }) => context.iterations >= context.maxIterations, target: 'done', actions: assign({ error: 'iteration budget exhausted' }) },
55
+ { guard: ({ context }) => context.interrupt, target: 'done', actions: assign({ error: 'interrupted' }) },
56
+ { target: 'executing_tools' },
57
+ ],
58
+ },
59
+ executing_tools: {
60
+ invoke: {
61
+ src: fromPromise(async ({ input }) => {
62
+ const last = input.messages[input.messages.length - 1]
63
+ const calls = last.tool_calls || []
64
+ const results = []
65
+ for (const call of calls) {
66
+ const res = await registry.dispatch(call.name || call.function?.name, call.arguments || call.function?.arguments || {})
67
+ results.push({ tool_call_id: call.id || call.tool_call_id, content: res })
68
+ }
69
+ return results
70
+ }),
71
+ input: ({ context }) => ({ messages: context.messages }),
72
+ onDone: { target: 'prompting', actions: assign({
73
+ messages: ({ context, event }) => [...context.messages, ...event.output.map(r => ({ role: 'tool', tool_call_id: r.tool_call_id, content: r.content }))],
74
+ iterations: ({ context }) => context.iterations + 1,
75
+ }) },
76
+ onError: { target: 'done', actions: assign({ error: ({ event }) => String(event.error?.message || event.error) }) },
77
+ },
78
+ },
79
+ done: {
80
+ type: 'final',
81
+ output: ({ context }) => ({ messages: context.messages, result: context.lastResult, error: context.error, iterations: context.iterations }),
82
+ },
83
+ },
84
+ })
85
+ }
86
+
87
+ export async function runTurn({ prompt, messages = [], model, provider, callLLM, enabledToolsets, disabledToolsets, maxIterations = 90, timeoutMs = 30000 } = {}) {
88
+ const machine = createAgentMachine({ model, provider, callLLM, enabledToolsets, disabledToolsets, maxIterations })
89
+ const actor = createActor(machine, { input: { messages } })
90
+ actor.start()
91
+ actor.send({ type: 'SUBMIT', prompt })
92
+ return await new Promise((resolve, reject) => {
93
+ const t = setTimeout(() => { try { actor.stop() } catch {} reject(new Error('agent turn timeout')) }, timeoutMs)
94
+ actor.subscribe(snap => {
95
+ if (snap.status === 'done') {
96
+ clearTimeout(t)
97
+ resolve(snap.output)
98
+ }
99
+ })
100
+ })
101
+ }
102
+
@@ -0,0 +1,5 @@
1
+ import { db } from '../sessions.js'
2
+ function init() { const d = db(); d.exec(`CREATE TABLE IF NOT EXISTS compression_feedback (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, summary TEXT, rating INTEGER, notes TEXT, ts INTEGER NOT NULL)`); return d }
3
+ export function record({ sessionId, summary, rating, notes = '' }) { init().prepare(`INSERT INTO compression_feedback (session_id, summary, rating, notes, ts) VALUES (?, ?, ?, ?, ?)`).run(sessionId, summary || '', rating, notes, Date.now()); return { recorded: true } }
4
+ export function listForSession(sessionId) { return init().prepare(`SELECT * FROM compression_feedback WHERE session_id = ? ORDER BY id DESC`).all(sessionId) }
5
+ export function aggregate() { return init().prepare(`SELECT AVG(rating) AS avg, COUNT(*) AS n FROM compression_feedback`).get() }
@@ -0,0 +1,14 @@
1
+ import { createMemoryProvider, listMemoryProviders } from '../plugins/memory/provider.js'
2
+ import { getConfigValue } from '../config.js'
3
+ let _provider = null
4
+ export function getMemoryProvider() {
5
+ if (_provider) return _provider
6
+ const name = getConfigValue('memory.provider')
7
+ if (!name) return null
8
+ try { _provider = createMemoryProvider(name, getConfigValue('memory.options', {})) } catch { _provider = null }
9
+ return _provider
10
+ }
11
+ export async function syncTurnIfConfigured(messages) { const p = getMemoryProvider(); if (p) try { return await p.syncTurn(messages) } catch (e) { return { error: String(e.message || e) } } return null }
12
+ export async function prefetchIfConfigured(query) { const p = getMemoryProvider(); if (p) try { return await p.prefetch(query) } catch { return { items: [] } } return { items: [] } }
13
+ export function listAvailable() { return listMemoryProviders() }
14
+ export function resetForTests() { _provider = null }
@@ -0,0 +1 @@
1
+ export { MemoryProvider, registerMemoryProvider, listMemoryProviders, createMemoryProvider } from '../plugins/memory/provider.js'
@@ -0,0 +1,28 @@
1
+ export const MINIMUM_CONTEXT_LENGTH = 8000
2
+ const TABLE = {
3
+ 'claude-opus-4-7': 1_000_000, 'claude-sonnet-4-6': 200_000, 'claude-haiku-4-5': 200_000,
4
+ 'claude-3-5-sonnet': 200_000, 'claude-3-5-haiku': 200_000, 'claude-3-opus': 200_000,
5
+ 'gpt-5': 400_000, 'gpt-5-mini': 400_000, 'gpt-4o': 128_000, 'gpt-4o-mini': 128_000, 'gpt-4-turbo': 128_000,
6
+ 'o1': 200_000, 'o1-mini': 128_000, 'o3': 200_000, 'o3-mini': 200_000,
7
+ 'gemini-2.5-pro': 2_000_000, 'gemini-2.5-flash': 1_000_000, 'gemini-2.0-flash': 1_000_000,
8
+ 'llama-3.3-70b': 128_000, 'llama-3.1-405b': 128_000,
9
+ 'grok-2': 128_000, 'grok-3': 1_000_000, 'grok-4': 256_000,
10
+ 'deepseek-v3': 64_000, 'deepseek-r1': 128_000,
11
+ 'qwen-2.5-72b': 128_000, 'qwen-3-coder': 256_000,
12
+ }
13
+ export function getModelContextLength(model) {
14
+ if (!model) return MINIMUM_CONTEXT_LENGTH
15
+ if (TABLE[model]) return TABLE[model]
16
+ for (const [k, v] of Object.entries(TABLE)) if (model.startsWith(k)) return v
17
+ return MINIMUM_CONTEXT_LENGTH
18
+ }
19
+ export function estimateMessagesTokensRough(messages = []) {
20
+ let chars = 0
21
+ for (const m of messages) {
22
+ const c = m?.content
23
+ if (typeof c === 'string') chars += c.length
24
+ else if (Array.isArray(c)) for (const p of c) chars += typeof p === 'string' ? p.length : (p?.text?.length || 100)
25
+ if (m?.tool_calls) chars += JSON.stringify(m.tool_calls).length
26
+ }
27
+ return Math.ceil(chars / 4)
28
+ }
@@ -0,0 +1,13 @@
1
+ let _cache = null
2
+ const ENDPOINT = 'https://models.dev/api/models.json'
3
+ export async function fetchModels({ refresh = false } = {}) {
4
+ if (_cache && !refresh) return _cache
5
+ try { const r = await fetch(ENDPOINT); _cache = await r.json(); return _cache } catch { return _cache || {} }
6
+ }
7
+ export async function findModel(slug) {
8
+ const data = await fetchModels()
9
+ if (Array.isArray(data)) return data.find(m => m.slug === slug || m.id === slug) || null
10
+ if (data && typeof data === 'object') return data[slug] || null
11
+ return null
12
+ }
13
+ export function clearCache() { _cache = null }
@@ -0,0 +1,11 @@
1
+ export function adaptToolSchema(tool) {
2
+ const s = { type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.input_schema || tool.parameters || { type: 'object', properties: {} } } }
3
+ if (s.function.parameters.required && !s.function.parameters.required.length) delete s.function.parameters.required
4
+ return s
5
+ }
6
+ export function adaptMessages(messages) {
7
+ return messages.map(m => {
8
+ if (m.role === 'tool') return { role: 'tool', tool_call_id: m.tool_call_id, content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content) }
9
+ return m
10
+ })
11
+ }
@@ -0,0 +1,79 @@
1
+ import { decodeJwtClaims } from '../auth.js'
2
+
3
+ const KIMI_BASE_URLS = {
4
+ intl: 'https://api.moonshot.ai/v1',
5
+ cn: 'https://api.moonshot.cn/v1',
6
+ }
7
+
8
+ const ZAI_BASE_URLS = {
9
+ bigmodel: 'https://open.bigmodel.cn/api/paas/v4',
10
+ z: 'https://api.z.ai/api/paas/v4',
11
+ }
12
+
13
+ export function resolveKimiBaseUrl({ region } = {}) {
14
+ if (process.env.KIMI_BASE_URL) return process.env.KIMI_BASE_URL
15
+ const r = (region || process.env.KIMI_REGION || 'intl').toLowerCase()
16
+ return KIMI_BASE_URLS[r] || KIMI_BASE_URLS.intl
17
+ }
18
+
19
+ export function resolveZaiBaseUrl({ endpoint } = {}) {
20
+ if (process.env.ZAI_BASE_URL) return process.env.ZAI_BASE_URL
21
+ const e = (endpoint || process.env.ZAI_ENDPOINT || '').toLowerCase()
22
+ if (e.includes('z.ai')) return ZAI_BASE_URLS.z
23
+ if (e.includes('bigmodel')) return ZAI_BASE_URLS.bigmodel
24
+ return ZAI_BASE_URLS.bigmodel
25
+ }
26
+
27
+ export async function detectZaiEndpoint(apiKey) {
28
+ if (!apiKey) return 'bigmodel'
29
+ for (const [name, base] of Object.entries(ZAI_BASE_URLS)) {
30
+ try {
31
+ const res = await fetch(base + '/models', { headers: { authorization: 'Bearer ' + apiKey }, signal: AbortSignal.timeout(3000) })
32
+ if (res.ok) return name
33
+ } catch {}
34
+ }
35
+ return 'bigmodel'
36
+ }
37
+
38
+ export function isCodexAccessTokenExpiring(token, { skewSeconds = 60 } = {}) {
39
+ const claims = decodeJwtClaims(typeof token === 'string' ? token : (token?.access_token || ''))
40
+ if (!claims) return true
41
+ if (!claims.exp) return false
42
+ return claims.exp - Math.floor(Date.now() / 1000) < skewSeconds
43
+ }
44
+
45
+ export async function refreshOauthToken({ tokenUrl, clientId, refreshToken, clientSecret } = {}) {
46
+ if (!tokenUrl || !refreshToken) throw new Error('tokenUrl and refreshToken required')
47
+ const body = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken })
48
+ if (clientId) body.set('client_id', clientId)
49
+ if (clientSecret) body.set('client_secret', clientSecret)
50
+ const res = await fetch(tokenUrl, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: body.toString() })
51
+ if (!res.ok) throw new Error('refresh failed: ' + res.status + ' ' + await res.text())
52
+ const j = await res.json()
53
+ if (j.expires_in && !j.expires_at) j.expires_at = Math.floor(Date.now() / 1000) + j.expires_in
54
+ return j
55
+ }
56
+
57
+ export function buildAuthorizeUrl({ authorizeUrl, clientId, redirectUri, scope, state, codeChallenge, codeChallengeMethod = 'S256' } = {}) {
58
+ const u = new URL(authorizeUrl)
59
+ u.searchParams.set('response_type', 'code')
60
+ u.searchParams.set('client_id', clientId)
61
+ u.searchParams.set('redirect_uri', redirectUri)
62
+ if (scope) u.searchParams.set('scope', scope)
63
+ if (state) u.searchParams.set('state', state)
64
+ if (codeChallenge) { u.searchParams.set('code_challenge', codeChallenge); u.searchParams.set('code_challenge_method', codeChallengeMethod) }
65
+ return u.toString()
66
+ }
67
+
68
+ export async function exchangeCodeForToken({ tokenUrl, clientId, code, redirectUri, codeVerifier, clientSecret } = {}) {
69
+ const body = new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: redirectUri, client_id: clientId })
70
+ if (codeVerifier) body.set('code_verifier', codeVerifier)
71
+ if (clientSecret) body.set('client_secret', clientSecret)
72
+ const res = await fetch(tokenUrl, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: body.toString() })
73
+ if (!res.ok) throw new Error('token exchange failed: ' + res.status + ' ' + await res.text())
74
+ const j = await res.json()
75
+ if (j.expires_in && !j.expires_at) j.expires_at = Math.floor(Date.now() / 1000) + j.expires_in
76
+ return j
77
+ }
78
+
79
+ export { KIMI_BASE_URLS, ZAI_BASE_URLS }
@@ -0,0 +1,16 @@
1
+ import { getConfigValue, saveConfigValue } from '../config.js'
2
+ import { getAuthStore } from '../auth.js'
3
+ const STEPS = ['provider', 'api-key', 'model', 'skin', 'memory']
4
+ export async function isOnboardingComplete() { return Boolean(getConfigValue('onboarding.completed')) }
5
+ export async function nextStep() {
6
+ if (!getConfigValue('agent.provider')) return 'provider'
7
+ const provider = getConfigValue('agent.provider')
8
+ const env = (provider || '').toUpperCase() + '_API_KEY'
9
+ if (!process.env[env] && !(await getAuthStore().getCredential(env))) return 'api-key'
10
+ if (!getConfigValue('agent.model')) return 'model'
11
+ if (!getConfigValue('display.skin')) return 'skin'
12
+ if (!getConfigValue('memory.provider')) return 'memory'
13
+ return null
14
+ }
15
+ export async function markComplete() { saveConfigValue('onboarding.completed', Date.now()) }
16
+ export const ONBOARDING_STEPS = STEPS