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.
- package/AGENTS.md +180 -0
- package/CHANGELOG.md +32 -0
- package/README.md +130 -0
- package/bin/freddie.js +116 -0
- package/package.json +59 -0
- package/skills/creative/README.md +3 -0
- package/skills/creative/architecture-diagram/SKILL.md +52 -0
- package/skills/creative/ascii-video/SKILL.md +60 -0
- package/skills/creative/concept-diagrams/SKILL.md +65 -0
- package/skills/data/README.md +3 -0
- package/skills/data/etl-pipelines/SKILL.md +60 -0
- package/skills/data/sql-explainer/SKILL.md +60 -0
- package/skills/ops/README.md +3 -0
- package/skills/ops/incident-response/SKILL.md +74 -0
- package/skills/ops/log-triage/SKILL.md +79 -0
- package/skills/planning/README.md +3 -0
- package/skills/planning/okr-drafter/SKILL.md +60 -0
- package/skills/planning/weekly-review/SKILL.md +64 -0
- package/skills/software-development/README.md +3 -0
- package/skills/software-development/code-review/SKILL.md +70 -0
- package/skills/software-development/rfc-writer/SKILL.md +68 -0
- package/skills/software-development/systematic-debugging/SKILL.md +80 -0
- package/src/acp/auth.js +21 -0
- package/src/acp/entry.js +2 -0
- package/src/acp/events.js +10 -0
- package/src/acp/main.js +8 -0
- package/src/acp/permissions.js +29 -0
- package/src/acp/server.js +84 -0
- package/src/acp/session.js +26 -0
- package/src/acp/tools.js +17 -0
- package/src/agent/account_usage.js +19 -0
- package/src/agent/acptoapi-bridge.js +80 -0
- package/src/agent/anthropic_adapter.js +10 -0
- package/src/agent/auxiliary_client.js +20 -0
- package/src/agent/bedrock_adapter.js +11 -0
- package/src/agent/codex_responses_adapter.js +10 -0
- package/src/agent/compress/compressor.js +55 -0
- package/src/agent/compress/fallback.js +14 -0
- package/src/agent/compress/index.js +6 -0
- package/src/agent/compress/policy.js +47 -0
- package/src/agent/compress/prompt.js +46 -0
- package/src/agent/compress/prune.js +16 -0
- package/src/agent/compress/tokens.js +31 -0
- package/src/agent/context_references.js +40 -0
- package/src/agent/copilot_acp_client.js +6 -0
- package/src/agent/credential_pool.js +30 -0
- package/src/agent/credential_sources.js +18 -0
- package/src/agent/curator.js +5 -0
- package/src/agent/display.js +23 -0
- package/src/agent/error_classifier.js +15 -0
- package/src/agent/file_safety.js +9 -0
- package/src/agent/gemini_cloudcode_adapter.js +9 -0
- package/src/agent/gemini_native_adapter.js +11 -0
- package/src/agent/gemini_schema.js +19 -0
- package/src/agent/google_code_assist.js +8 -0
- package/src/agent/google_oauth.js +21 -0
- package/src/agent/image_gen_provider.js +8 -0
- package/src/agent/image_gen_registry.js +6 -0
- package/src/agent/image_routing.js +13 -0
- package/src/agent/insights.js +9 -0
- package/src/agent/llm_resolver.js +21 -0
- package/src/agent/lmstudio_reasoning.js +13 -0
- package/src/agent/machine.js +102 -0
- package/src/agent/manual_compression_feedback.js +5 -0
- package/src/agent/memory_manager.js +14 -0
- package/src/agent/memory_provider.js +1 -0
- package/src/agent/model_metadata.js +28 -0
- package/src/agent/models_dev.js +13 -0
- package/src/agent/moonshot_schema.js +11 -0
- package/src/agent/oauth_endpoints.js +79 -0
- package/src/agent/onboarding.js +16 -0
- package/src/agent/pi-bridge.js +37 -0
- package/src/agent/prompt_builder.js +12 -0
- package/src/agent/prompt_caching.js +24 -0
- package/src/agent/rate_limit_tracker.js +12 -0
- package/src/agent/redact.js +25 -0
- package/src/agent/retry_utils.js +17 -0
- package/src/agent/shell_hooks.js +16 -0
- package/src/agent/skill_commands.js +16 -0
- package/src/agent/skill_preprocessing.js +12 -0
- package/src/agent/skill_utils.js +14 -0
- package/src/agent/subdirectory_hints.js +17 -0
- package/src/agent/title_generator.js +13 -0
- package/src/agent/trajectory.js +9 -0
- package/src/agent/usage_pricing.js +16 -0
- package/src/auth.js +84 -0
- package/src/batch.js +32 -0
- package/src/cli/auth_commands.js +17 -0
- package/src/cli/azure_detect.js +9 -0
- package/src/cli/backup.js +17 -0
- package/src/cli/banner.js +13 -0
- package/src/cli/browser_connect.js +11 -0
- package/src/cli/callbacks.js +5 -0
- package/src/cli/claw.js +8 -0
- package/src/cli/cli_output.js +19 -0
- package/src/cli/clipboard.js +24 -0
- package/src/cli/codex_models.js +8 -0
- package/src/cli/colors.js +13 -0
- package/src/cli/completer.js +98 -0
- package/src/cli/completion.js +21 -0
- package/src/cli/copilot_auth.js +9 -0
- package/src/cli/curator_cli.js +5 -0
- package/src/cli/curses.js +15 -0
- package/src/cli/debug.js +6 -0
- package/src/cli/default_soul.js +20 -0
- package/src/cli/dingtalk_auth.js +12 -0
- package/src/cli/doctor.js +15 -0
- package/src/cli/dump.js +11 -0
- package/src/cli/env_loader.js +25 -0
- package/src/cli/fallback_cmd.js +9 -0
- package/src/cli/gateway_cli.js +17 -0
- package/src/cli/hooks.js +9 -0
- package/src/cli/interactive.js +61 -0
- package/src/cli/logs.js +32 -0
- package/src/cli/main.js +7 -0
- package/src/cli/mcp_config.js +9 -0
- package/src/cli/memory_setup.js +12 -0
- package/src/cli/model_catalog.js +23 -0
- package/src/cli/model_normalize.js +12 -0
- package/src/cli/model_switch.js +11 -0
- package/src/cli/models.js +13 -0
- package/src/cli/nous_subscription.js +12 -0
- package/src/cli/oneshot.js +6 -0
- package/src/cli/pairing.js +21 -0
- package/src/cli/platforms.js +14 -0
- package/src/cli/plugins.js +4 -0
- package/src/cli/plugins_cmd.js +21 -0
- package/src/cli/profiles_cli.js +6 -0
- package/src/cli/providers.js +18 -0
- package/src/cli/pty_bridge.js +16 -0
- package/src/cli/relaunch.js +7 -0
- package/src/cli/runtime_provider.js +9 -0
- package/src/cli/setup.js +131 -0
- package/src/cli/skills_config.js +6 -0
- package/src/cli/skills_hub.js +8 -0
- package/src/cli/slack_cli.js +17 -0
- package/src/cli/status.js +10 -0
- package/src/cli/timeouts.js +5 -0
- package/src/cli/tips.js +14 -0
- package/src/cli/tools_config.js +15 -0
- package/src/cli/uninstall.js +8 -0
- package/src/cli/vercel_auth.js +13 -0
- package/src/cli/voice.js +6 -0
- package/src/cli/web_server.js +13 -0
- package/src/cli/webhook.js +12 -0
- package/src/commands/profile.js +72 -0
- package/src/commands/registry.js +94 -0
- package/src/config.js +125 -0
- package/src/context/engine.js +42 -0
- package/src/cron/cron-parse.js +27 -0
- package/src/cron/scheduler.js +63 -0
- package/src/db.js +178 -0
- package/src/gateway/base.js +13 -0
- package/src/gateway/builtin_hooks/boot.js +5 -0
- package/src/gateway/builtin_hooks/broadcast.js +3 -0
- package/src/gateway/builtin_hooks/deny.js +6 -0
- package/src/gateway/builtin_hooks/index.js +17 -0
- package/src/gateway/builtin_hooks/presence.js +4 -0
- package/src/gateway/builtin_hooks/routing.js +7 -0
- package/src/gateway/helpers.js +27 -0
- package/src/gateway/platforms/api_server.js +21 -0
- package/src/gateway/platforms/bluebubbles.js +32 -0
- package/src/gateway/platforms/dingtalk.js +32 -0
- package/src/gateway/platforms/discord.js +24 -0
- package/src/gateway/platforms/email.js +51 -0
- package/src/gateway/platforms/feishu.js +32 -0
- package/src/gateway/platforms/feishu_comment.js +12 -0
- package/src/gateway/platforms/feishu_comment_rules.js +11 -0
- package/src/gateway/platforms/homeassistant.js +32 -0
- package/src/gateway/platforms/matrix.js +40 -0
- package/src/gateway/platforms/mattermost.js +29 -0
- package/src/gateway/platforms/qqbot.js +32 -0
- package/src/gateway/platforms/signal.js +33 -0
- package/src/gateway/platforms/slack.js +34 -0
- package/src/gateway/platforms/sms.js +34 -0
- package/src/gateway/platforms/telegram.js +38 -0
- package/src/gateway/platforms/telegram_network.js +17 -0
- package/src/gateway/platforms/webhook.js +19 -0
- package/src/gateway/platforms/wecom.js +32 -0
- package/src/gateway/platforms/wecom_callback.js +15 -0
- package/src/gateway/platforms/wecom_crypto.js +16 -0
- package/src/gateway/platforms/weixin.js +32 -0
- package/src/gateway/platforms/whatsapp.js +40 -0
- package/src/gateway/platforms/yuanbao.js +9 -0
- package/src/gateway/platforms/yuanbao_media.js +5 -0
- package/src/gateway/platforms/yuanbao_proto.js +9 -0
- package/src/gateway/platforms/yuanbao_sticker.js +6 -0
- package/src/gateway/run.js +42 -0
- package/src/gateway/service.js +143 -0
- package/src/home.js +44 -0
- package/src/index.js +47 -0
- package/src/mcp/server.js +49 -0
- package/src/observability/debug.js +31 -0
- package/src/observability/log.js +38 -0
- package/src/plugins/achievements/index.js +9 -0
- package/src/plugins/cockpit/index.js +8 -0
- package/src/plugins/context_engine/index.js +13 -0
- package/src/plugins/disk_cleanup/index.js +22 -0
- package/src/plugins/google_meet/index.js +19 -0
- package/src/plugins/image_gen/index.js +5 -0
- package/src/plugins/manager.js +66 -0
- package/src/plugins/memory/_index.js +8 -0
- package/src/plugins/memory/byterover.js +25 -0
- package/src/plugins/memory/hindsight.js +25 -0
- package/src/plugins/memory/holographic.js +31 -0
- package/src/plugins/memory/honcho.js +25 -0
- package/src/plugins/memory/mem0.js +25 -0
- package/src/plugins/memory/openviking.js +25 -0
- package/src/plugins/memory/provider.js +35 -0
- package/src/plugins/memory/retaindb.js +25 -0
- package/src/plugins/memory/supermemory.js +25 -0
- package/src/plugins/observability/index.js +18 -0
- package/src/plugins/platforms/index.js +20 -0
- package/src/plugins/spotify/index.js +22 -0
- package/src/rl/atropos.js +22 -0
- package/src/rl/cli.js +18 -0
- package/src/sessions.js +84 -0
- package/src/skills/index.js +49 -0
- package/src/skin/engine.js +81 -0
- package/src/swe/runner.js +26 -0
- package/src/time.js +25 -0
- package/src/tools/ansi_strip.js +8 -0
- package/src/tools/approval.js +15 -0
- package/src/tools/bash.js +35 -0
- package/src/tools/binary_extensions.js +22 -0
- package/src/tools/browser.js +48 -0
- package/src/tools/budget_config.js +13 -0
- package/src/tools/checkpoint.js +29 -0
- package/src/tools/clarify.js +15 -0
- package/src/tools/code_execution.js +27 -0
- package/src/tools/credential_files.js +16 -0
- package/src/tools/cronjob.js +16 -0
- package/src/tools/debug_helpers.js +9 -0
- package/src/tools/delegate.js +28 -0
- package/src/tools/discord_tool.js +13 -0
- package/src/tools/edit.js +31 -0
- package/src/tools/env_passthrough.js +15 -0
- package/src/tools/environments/base.js +26 -0
- package/src/tools/environments/daytona.js +48 -0
- package/src/tools/environments/docker.js +14 -0
- package/src/tools/environments/file_sync.js +60 -0
- package/src/tools/environments/index.js +36 -0
- package/src/tools/environments/local.js +31 -0
- package/src/tools/environments/modal.js +33 -0
- package/src/tools/environments/singularity.js +38 -0
- package/src/tools/environments/ssh.js +14 -0
- package/src/tools/environments/vercel_sandbox.js +47 -0
- package/src/tools/feishu_doc.js +15 -0
- package/src/tools/feishu_drive.js +14 -0
- package/src/tools/file_operations.js +17 -0
- package/src/tools/file_state.js +16 -0
- package/src/tools/file_tools.js +23 -0
- package/src/tools/fuzzy_match.js +8 -0
- package/src/tools/grep.js +51 -0
- package/src/tools/homeassistant_tool.js +15 -0
- package/src/tools/image_gen.js +33 -0
- package/src/tools/interrupt.js +18 -0
- package/src/tools/managed_tool_gateway.js +11 -0
- package/src/tools/mcp_oauth.js +21 -0
- package/src/tools/mcp_oauth_manager.js +20 -0
- package/src/tools/mcp_tool.js +36 -0
- package/src/tools/memory.js +66 -0
- package/src/tools/mixture_of_agents.js +14 -0
- package/src/tools/neutts_synth.js +13 -0
- package/src/tools/openrouter_client.js +13 -0
- package/src/tools/osv_check.js +11 -0
- package/src/tools/patch_parser.js +42 -0
- package/src/tools/path_security.js +16 -0
- package/src/tools/process_registry.js +17 -0
- package/src/tools/read.js +26 -0
- package/src/tools/registry.js +54 -0
- package/src/tools/rl_training.js +13 -0
- package/src/tools/schema_sanitizer.js +18 -0
- package/src/tools/send_message.js +32 -0
- package/src/tools/session_search.js +23 -0
- package/src/tools/skill_manager.js +17 -0
- package/src/tools/skill_usage.js +20 -0
- package/src/tools/skills_guard.js +17 -0
- package/src/tools/skills_hub.js +31 -0
- package/src/tools/skills_index.js +14 -0
- package/src/tools/skills_sync.js +19 -0
- package/src/tools/skills_tool.js +11 -0
- package/src/tools/slash_confirm.js +16 -0
- package/src/tools/terminal.js +29 -0
- package/src/tools/tirith_security.js +25 -0
- package/src/tools/todo.js +54 -0
- package/src/tools/tool_backend_helpers.js +26 -0
- package/src/tools/tool_output_limits.js +15 -0
- package/src/tools/tool_result_storage.js +20 -0
- package/src/tools/transcription.js +19 -0
- package/src/tools/tts.js +19 -0
- package/src/tools/url_safety.js +15 -0
- package/src/tools/vision.js +18 -0
- package/src/tools/voice_mode.js +10 -0
- package/src/tools/web_search.js +37 -0
- package/src/tools/web_tools.js +18 -0
- package/src/tools/website_policy.js +14 -0
- package/src/tools/write.js +25 -0
- package/src/tools/xai_http.js +13 -0
- package/src/tools/yuanbao_tools.js +13 -0
- package/src/toolset_distributions.js +18 -0
- package/src/toolsets.js +26 -0
- package/src/tui/index.js +26 -0
- package/src/utils.js +54 -0
- package/src/web/app.js +547 -0
- package/src/web/index.html +167 -0
- 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
|