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,37 @@
|
|
|
1
|
+
import { logger } from '../observability/log.js'
|
|
2
|
+
|
|
3
|
+
const log = logger('pi-bridge')
|
|
4
|
+
|
|
5
|
+
let _piAi = null
|
|
6
|
+
async function pi() {
|
|
7
|
+
if (_piAi) return _piAi
|
|
8
|
+
_piAi = await import('@mariozechner/pi-ai')
|
|
9
|
+
return _piAi
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function callLLM({ messages, tools = [], model, provider = 'anthropic' } = {}) {
|
|
13
|
+
const m = await pi()
|
|
14
|
+
const modelObj = m.getModel ? m.getModel(provider, model) : { provider, id: model }
|
|
15
|
+
const apiKey = m.getEnvApiKey ? m.getEnvApiKey(provider) : process.env[providerEnv(provider)]
|
|
16
|
+
if (!apiKey) throw new Error(`pi-bridge: no API key for ${provider} (set ${providerEnv(provider)})`)
|
|
17
|
+
const result = await m.complete({ model: modelObj, apiKey, messages: messages.map(adaptMessage), tools: tools.map(adaptTool) })
|
|
18
|
+
log.info('completed', { model: model || 'default', usage: result.usage })
|
|
19
|
+
return adaptResponse(result)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function providerEnv(p) { return ({ anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY', groq: 'GROQ_API_KEY' })[p] || `${String(p).toUpperCase()}_API_KEY` }
|
|
23
|
+
|
|
24
|
+
function adaptMessage(m) {
|
|
25
|
+
if (m.role === 'tool') return { role: 'tool', tool_call_id: m.tool_call_id, content: m.content }
|
|
26
|
+
return { role: m.role, content: m.content || '', tool_calls: m.tool_calls }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function adaptTool(t) {
|
|
30
|
+
return { name: t.name, description: t.description, input_schema: t.parameters || t.input_schema || { type: 'object', properties: {} } }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function adaptResponse(r) {
|
|
34
|
+
const content = typeof r.content === 'string' ? r.content : (Array.isArray(r.content) ? r.content.filter(c => c.type === 'text').map(c => c.text).join('') : '')
|
|
35
|
+
const tool_calls = (Array.isArray(r.content) ? r.content.filter(c => c.type === 'tool_use').map(c => ({ id: c.id, name: c.name, arguments: c.input })) : (r.tool_calls || []))
|
|
36
|
+
return { content, tool_calls, raw: r }
|
|
37
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SUMMARY_PREFIX } from './compress/index.js'
|
|
2
|
+
export function buildSystemPrompt({ persona = '', skills = [], context = [], cacheBreakpoint = true } = {}) {
|
|
3
|
+
const parts = []
|
|
4
|
+
if (persona) parts.push(persona)
|
|
5
|
+
if (skills.length) parts.push('## Available skills\n' + skills.map(s => '- ' + s.name + ': ' + (s.description || '')).join('\n'))
|
|
6
|
+
if (context.length) parts.push('## Context\n' + context.map(c => '[' + c.name + ']\n' + c.body).join('\n\n'))
|
|
7
|
+
return { content: parts.join('\n\n'), cacheBreakpoint }
|
|
8
|
+
}
|
|
9
|
+
export function injectSummaryHandoff(messages, summary) {
|
|
10
|
+
if (!summary) return messages
|
|
11
|
+
return [...messages, { role: 'user', content: SUMMARY_PREFIX + '\n\n' + summary }]
|
|
12
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const CACHE_BREAKPOINT_MAX = 4
|
|
2
|
+
export function annotateBreakpoints(messages, { provider = 'anthropic' } = {}) {
|
|
3
|
+
if (provider !== 'anthropic') return messages
|
|
4
|
+
const out = messages.map(m => ({ ...m }))
|
|
5
|
+
const candidates = []
|
|
6
|
+
for (let i = out.length - 1; i >= 0; i--) {
|
|
7
|
+
const r = out[i].role
|
|
8
|
+
if (r === 'system' || r === 'user') candidates.push(i)
|
|
9
|
+
if (candidates.length >= CACHE_BREAKPOINT_MAX) break
|
|
10
|
+
}
|
|
11
|
+
for (const i of candidates.slice(0, CACHE_BREAKPOINT_MAX)) {
|
|
12
|
+
const m = out[i]
|
|
13
|
+
if (typeof m.content === 'string') m.content = [{ type: 'text', text: m.content, cache_control: { type: 'ephemeral' } }]
|
|
14
|
+
else if (Array.isArray(m.content) && m.content.length) m.content[m.content.length - 1] = { ...m.content[m.content.length - 1], cache_control: { type: 'ephemeral' } }
|
|
15
|
+
}
|
|
16
|
+
return out
|
|
17
|
+
}
|
|
18
|
+
export function countBreakpoints(messages) {
|
|
19
|
+
let n = 0
|
|
20
|
+
for (const m of messages) {
|
|
21
|
+
if (Array.isArray(m.content)) for (const p of m.content) if (p?.cache_control) n++
|
|
22
|
+
}
|
|
23
|
+
return n
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const _windows = new Map()
|
|
2
|
+
export function record(provider, e) {
|
|
3
|
+
const w = _windows.get(provider) || { count: 0, until: 0 }
|
|
4
|
+
w.count++
|
|
5
|
+
const m = String(e?.message || e || '').match(/retry.?after[:\s]+(\d+)/i) || String(e?.headers?.get?.('retry-after') || '').match(/(\d+)/)
|
|
6
|
+
if (m) w.until = Date.now() + Number(m[1]) * 1000
|
|
7
|
+
_windows.set(provider, w)
|
|
8
|
+
return w
|
|
9
|
+
}
|
|
10
|
+
export function shouldThrottle(provider) { const w = _windows.get(provider); return w ? Date.now() < w.until : false }
|
|
11
|
+
export function clear(provider) { _windows.delete(provider) }
|
|
12
|
+
export function snapshot() { return Object.fromEntries(_windows) }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const SECRET_PATTERNS = [
|
|
2
|
+
[/sk-[A-Za-z0-9-_]{20,}/g, 'openai-key'],
|
|
3
|
+
[/sk-ant-[A-Za-z0-9-_]{20,}/g, 'anthropic-key'],
|
|
4
|
+
[/ghp_[A-Za-z0-9]{36}/g, 'github-pat'],
|
|
5
|
+
[/gho_[A-Za-z0-9]{36}/g, 'github-oauth'],
|
|
6
|
+
[/xox[baprs]-[A-Za-z0-9-]{10,}/g, 'slack-token'],
|
|
7
|
+
[/AKIA[0-9A-Z]{16}/g, 'aws-key-id'],
|
|
8
|
+
[/[a-zA-Z0-9._%+-]+:[^@\s]{4,}@[a-zA-Z0-9.-]+/g, 'url-credentials'],
|
|
9
|
+
[/Bearer\s+[A-Za-z0-9._-]{20,}/gi, 'bearer-token'],
|
|
10
|
+
[/[\w-]{20,}\.[\w-]{6,}\.[\w-]{20,}/g, 'jwt'],
|
|
11
|
+
[/-----BEGIN [A-Z ]+PRIVATE KEY-----[\s\S]+?-----END [A-Z ]+PRIVATE KEY-----/g, 'private-key'],
|
|
12
|
+
]
|
|
13
|
+
export function redactSensitive(text) {
|
|
14
|
+
let out = String(text)
|
|
15
|
+
for (const [re] of SECRET_PATTERNS) out = out.replace(re, '[REDACTED]')
|
|
16
|
+
return out
|
|
17
|
+
}
|
|
18
|
+
export function detectSecrets(text) {
|
|
19
|
+
const found = []
|
|
20
|
+
for (const [re, kind] of SECRET_PATTERNS) {
|
|
21
|
+
const m = String(text).match(re)
|
|
22
|
+
if (m) for (const s of m) found.push({ kind, value: s.slice(0, 20) + '…' })
|
|
23
|
+
}
|
|
24
|
+
return found
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isRetryable } from './error_classifier.js'
|
|
2
|
+
export async function retryAsync(fn, { attempts = 3, backoff = 200, factor = 2, max = 10_000, jitter = 0.2 } = {}) {
|
|
3
|
+
let last
|
|
4
|
+
for (let i = 0; i < attempts; i++) {
|
|
5
|
+
try { return await fn(i) } catch (e) {
|
|
6
|
+
last = e
|
|
7
|
+
if (i === attempts - 1) break
|
|
8
|
+
if (!isRetryable(e)) break
|
|
9
|
+
const wait = Math.min(max, backoff * Math.pow(factor, i)) * (1 + (Math.random() * 2 - 1) * jitter)
|
|
10
|
+
await new Promise(r => setTimeout(r, wait))
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
throw last
|
|
14
|
+
}
|
|
15
|
+
export async function withTimeout(fn, ms) {
|
|
16
|
+
return await Promise.race([fn(), new Promise((_, rej) => setTimeout(() => rej(new Error('timeout after ' + ms + 'ms')), ms))])
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { getFophHome } from '../home.js'
|
|
4
|
+
function hookFile() { return path.join(getFophHome(), 'shell-hooks.json') }
|
|
5
|
+
export function loadHooks() { try { return JSON.parse(fs.readFileSync(hookFile(), 'utf8')) } catch { return { pre_run: [], post_run: [] } } }
|
|
6
|
+
export function saveHooks(h) { fs.writeFileSync(hookFile(), JSON.stringify(h, null, 2), 'utf8') }
|
|
7
|
+
export function addHook(stage, command) { const h = loadHooks(); (h[stage] = h[stage] || []).push(command); saveHooks(h); return h }
|
|
8
|
+
export async function runHooks(stage, ctx = {}) {
|
|
9
|
+
const { spawnSync } = await import('node:child_process')
|
|
10
|
+
const out = []
|
|
11
|
+
for (const cmd of (loadHooks()[stage] || [])) {
|
|
12
|
+
const r = spawnSync(process.platform === 'win32' ? 'cmd' : 'sh', [process.platform === 'win32' ? '/c' : '-c', cmd], { encoding: 'utf8', env: { ...process.env, ...ctx.env } })
|
|
13
|
+
out.push({ cmd, exitCode: r.status, stdout: r.stdout, stderr: r.stderr })
|
|
14
|
+
}
|
|
15
|
+
return out
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { findSkill } from '../skills/index.js'
|
|
2
|
+
|
|
3
|
+
export function buildSkillUserMessage(name, args = '') {
|
|
4
|
+
const s = findSkill(name)
|
|
5
|
+
if (!s) return null
|
|
6
|
+
const argLine = args ? `\nArguments: ${args}\n` : '\n'
|
|
7
|
+
return { role: 'user', content: `[skill:${name}]${argLine}\n${s.body}` }
|
|
8
|
+
}
|
|
9
|
+
export function isSkillCommand(input) {
|
|
10
|
+
return typeof input === 'string' && /^\/skill\s+\S+/.test(input.trim())
|
|
11
|
+
}
|
|
12
|
+
export function parseSkillCommand(input) {
|
|
13
|
+
const m = String(input).trim().match(/^\/skill\s+(\S+)\s*(.*)$/)
|
|
14
|
+
if (!m) return null
|
|
15
|
+
return { name: m[1], args: m[2] }
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { findSkill } from '../skills/index.js'
|
|
2
|
+
const REF_RE = /@skill\/([\w-]+)(?:\s+([^\n@]*))?/g
|
|
3
|
+
export function preprocessSkillRefs(text) {
|
|
4
|
+
if (!text || typeof text !== 'string') return text
|
|
5
|
+
return text.replace(REF_RE, (m, name, args) => {
|
|
6
|
+
const s = findSkill(name)
|
|
7
|
+
return s ? '\n[skill:' + name + ']\n' + s.body + '\n' : m
|
|
8
|
+
})
|
|
9
|
+
}
|
|
10
|
+
export function listSkillRefs(text) {
|
|
11
|
+
return [...String(text || '').matchAll(REF_RE)].map(m => ({ name: m[1], args: m[2]?.trim() || '' }))
|
|
12
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { listSkills, findSkill } from '../skills/index.js'
|
|
2
|
+
export function fuzzyFindSkill(needle) {
|
|
3
|
+
const all = listSkills()
|
|
4
|
+
const lower = String(needle).toLowerCase()
|
|
5
|
+
const exact = all.find(s => s.name.toLowerCase() === lower)
|
|
6
|
+
if (exact) return exact
|
|
7
|
+
const starts = all.find(s => s.name.toLowerCase().startsWith(lower))
|
|
8
|
+
if (starts) return starts
|
|
9
|
+
return all.find(s => s.name.toLowerCase().includes(lower)) || null
|
|
10
|
+
}
|
|
11
|
+
export function skillByCategory(category) {
|
|
12
|
+
return listSkills().filter(s => s.frontmatter?.category === category)
|
|
13
|
+
}
|
|
14
|
+
export function skillExists(name) { return Boolean(findSkill(name)) }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
const HINT_FILES = ['CLAUDE.md', 'AGENTS.md', '.freddie-context', 'README.md']
|
|
4
|
+
export function collectHints({ cwd = process.cwd(), maxDepth = 3 } = {}) {
|
|
5
|
+
const hints = []
|
|
6
|
+
let dir = cwd
|
|
7
|
+
for (let d = 0; d < maxDepth; d++) {
|
|
8
|
+
for (const f of HINT_FILES) {
|
|
9
|
+
const p = path.join(dir, f)
|
|
10
|
+
if (fs.existsSync(p)) { try { hints.push({ path: p, body: fs.readFileSync(p, 'utf8').slice(0, 4000) }) } catch {} }
|
|
11
|
+
}
|
|
12
|
+
const parent = path.dirname(dir)
|
|
13
|
+
if (parent === dir) break
|
|
14
|
+
dir = parent
|
|
15
|
+
}
|
|
16
|
+
return hints
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function generateTitle(prompt, { maxWords = 8 } = {}) {
|
|
2
|
+
const text = String(prompt || '').replace(/[\n\r]+/g, ' ').trim()
|
|
3
|
+
if (!text) return 'untitled'
|
|
4
|
+
const words = text.split(/\s+/).slice(0, maxWords)
|
|
5
|
+
let title = words.join(' ')
|
|
6
|
+
if (title.length > 60) title = title.slice(0, 57) + '…'
|
|
7
|
+
return title.charAt(0).toUpperCase() + title.slice(1)
|
|
8
|
+
}
|
|
9
|
+
export async function llmTitle(messages, { callLLM, model = null } = {}) {
|
|
10
|
+
if (!callLLM) return generateTitle(messages?.[0]?.content || '')
|
|
11
|
+
const out = await callLLM({ model, messages: [{ role: 'user', content: 'Title this conversation in <=8 words. Reply only with the title.\n\n' + JSON.stringify(messages.slice(0, 4)) }] })
|
|
12
|
+
return (out?.content || '').trim().replace(/^["']|["']$/g, '') || generateTitle(messages?.[0]?.content || '')
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function compressTrajectory({ messages = [], maxKeep = 20 } = {}) {
|
|
2
|
+
if (messages.length <= maxKeep * 2) return { compressed: messages, removed: 0 }
|
|
3
|
+
const head = messages.slice(0, maxKeep)
|
|
4
|
+
const tail = messages.slice(-maxKeep)
|
|
5
|
+
const middleCount = messages.length - head.length - tail.length
|
|
6
|
+
const summary = { role: 'system', content: `[trajectory.compressed] ${middleCount} middle messages elided` }
|
|
7
|
+
return { compressed: [...head, summary, ...tail], removed: middleCount }
|
|
8
|
+
}
|
|
9
|
+
export function expandTrajectory(trajectory) { return trajectory.compressed }
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const PRICING = {
|
|
2
|
+
'claude-opus-4-7': [15, 75], 'claude-sonnet-4-6': [3, 15], 'claude-haiku-4-5': [0.8, 4],
|
|
3
|
+
'gpt-5': [10, 30], 'gpt-5-mini': [0.25, 2], 'gpt-4o': [2.5, 10], 'gpt-4o-mini': [0.15, 0.6],
|
|
4
|
+
'o3': [15, 60], 'o3-mini': [1.1, 4.4], 'o1': [15, 60],
|
|
5
|
+
'gemini-2.5-pro': [1.25, 10], 'gemini-2.5-flash': [0.075, 0.3],
|
|
6
|
+
'grok-3': [3, 15], 'grok-4': [5, 25], 'deepseek-v3': [0.27, 1.1],
|
|
7
|
+
}
|
|
8
|
+
export function priceFor(model) {
|
|
9
|
+
if (PRICING[model]) return PRICING[model]
|
|
10
|
+
for (const [k, v] of Object.entries(PRICING)) if (model.startsWith(k)) return v
|
|
11
|
+
return [0, 0]
|
|
12
|
+
}
|
|
13
|
+
export function calculateCost({ model, prompt_tokens = 0, completion_tokens = 0 }) {
|
|
14
|
+
const [pIn, pOut] = priceFor(model)
|
|
15
|
+
return (prompt_tokens / 1_000_000) * pIn + (completion_tokens / 1_000_000) * pOut
|
|
16
|
+
}
|
package/src/auth.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { getFophHome } from './home.js'
|
|
4
|
+
|
|
5
|
+
class FileAuthStore {
|
|
6
|
+
constructor() { this.dir = path.join(getFophHome(), 'auth'); fs.mkdirSync(this.dir, { recursive: true }) }
|
|
7
|
+
_path(name) { return path.join(this.dir, name + '.json') }
|
|
8
|
+
async setCredential(name, value) {
|
|
9
|
+
fs.writeFileSync(this._path(name), JSON.stringify({ name, value, updated: Date.now() }), { encoding: 'utf8', mode: 0o600 })
|
|
10
|
+
return { name, stored: true }
|
|
11
|
+
}
|
|
12
|
+
async getCredential(name) {
|
|
13
|
+
const p = this._path(name)
|
|
14
|
+
if (!fs.existsSync(p)) return null
|
|
15
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'))
|
|
16
|
+
}
|
|
17
|
+
async listCredentials() {
|
|
18
|
+
return fs.readdirSync(this.dir).filter(f => f.endsWith('.json')).map(f => f.replace(/\.json$/, ''))
|
|
19
|
+
}
|
|
20
|
+
async deleteCredential(name) {
|
|
21
|
+
const p = this._path(name)
|
|
22
|
+
if (fs.existsSync(p)) fs.unlinkSync(p)
|
|
23
|
+
return { name, deleted: true }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let _store = null
|
|
28
|
+
export function getAuthStore() {
|
|
29
|
+
if (!_store) _store = new FileAuthStore()
|
|
30
|
+
return _store
|
|
31
|
+
}
|
|
32
|
+
export function resetAuthStoreForTests() { _store = null }
|
|
33
|
+
|
|
34
|
+
const PROVIDERS = ['anthropic', 'openai', 'groq', 'openrouter', 'xai', 'gemini', 'bedrock', 'codex', 'kimi', 'zai', 'deepseek', 'mistral', 'perplexity']
|
|
35
|
+
const ENV_OF = { anthropic: 'ANTHROPIC_API_KEY', openai: 'OPENAI_API_KEY', groq: 'GROQ_API_KEY', openrouter: 'OPENROUTER_API_KEY', xai: 'XAI_API_KEY', gemini: 'GEMINI_API_KEY', bedrock: 'AWS_ACCESS_KEY_ID', codex: 'OPENAI_API_KEY', kimi: 'KIMI_API_KEY', zai: 'ZAI_API_KEY', deepseek: 'DEEPSEEK_API_KEY', mistral: 'MISTRAL_API_KEY', perplexity: 'PERPLEXITY_API_KEY' }
|
|
36
|
+
|
|
37
|
+
export function isKnownAuthProvider(name) { return PROVIDERS.includes(name) }
|
|
38
|
+
export function listAuthProviders() { return [...PROVIDERS] }
|
|
39
|
+
export function envForProvider(name) { return ENV_OF[name] || null }
|
|
40
|
+
|
|
41
|
+
export async function hasUsableSecret(provider) {
|
|
42
|
+
const env = envForProvider(provider)
|
|
43
|
+
if (!env) return false
|
|
44
|
+
if (process.env[env]) return true
|
|
45
|
+
const cred = await getAuthStore().getCredential(env)
|
|
46
|
+
return Boolean(cred?.value)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function clearProviderAuth(provider) {
|
|
50
|
+
const env = envForProvider(provider)
|
|
51
|
+
if (!env) return false
|
|
52
|
+
await getAuthStore().deleteCredential(env)
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isExpiring(token, { skewSeconds = 60 } = {}) {
|
|
57
|
+
if (!token || typeof token !== 'object') return true
|
|
58
|
+
const exp = token.expires_at || token.exp
|
|
59
|
+
if (!exp) return false
|
|
60
|
+
const now = Math.floor(Date.now() / 1000)
|
|
61
|
+
const expSec = typeof exp === 'string' ? Math.floor(new Date(exp).getTime() / 1000) : exp
|
|
62
|
+
return expSec - now < skewSeconds
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function decodeJwtClaims(jwt) {
|
|
66
|
+
if (typeof jwt !== 'string') return null
|
|
67
|
+
const parts = jwt.split('.')
|
|
68
|
+
if (parts.length < 2) return null
|
|
69
|
+
try { return JSON.parse(Buffer.from(parts[1].replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8')) } catch { return null }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function tokenFingerprint(token) {
|
|
73
|
+
const s = typeof token === 'string' ? token : (token?.access_token || token?.value || '')
|
|
74
|
+
if (!s) return ''
|
|
75
|
+
return s.slice(0, 4) + '…' + s.slice(-4)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function getProviderAuthState(provider) {
|
|
79
|
+
return {
|
|
80
|
+
provider,
|
|
81
|
+
env: envForProvider(provider),
|
|
82
|
+
hasSecret: await hasUsableSecret(provider),
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/batch.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { runTurn } from './agent/machine.js'
|
|
4
|
+
import { getFophHome } from './home.js'
|
|
5
|
+
import { randomUUID } from 'node:crypto'
|
|
6
|
+
|
|
7
|
+
export async function runBatch({ prompts = [], concurrency = 4, model, callLLM } = {}) {
|
|
8
|
+
if (!Array.isArray(prompts) || prompts.length === 0) throw new Error('prompts required')
|
|
9
|
+
const id = randomUUID()
|
|
10
|
+
const dir = path.join(getFophHome(), 'batches')
|
|
11
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
12
|
+
const file = path.join(dir, id + '.jsonl')
|
|
13
|
+
const stream = fs.createWriteStream(file, { flags: 'a' })
|
|
14
|
+
const queue = prompts.map((p, i) => ({ i, p }))
|
|
15
|
+
const results = new Array(prompts.length)
|
|
16
|
+
const workers = Array.from({ length: Math.min(concurrency, prompts.length) }, async () => {
|
|
17
|
+
while (queue.length) {
|
|
18
|
+
const job = queue.shift()
|
|
19
|
+
if (!job) break
|
|
20
|
+
try {
|
|
21
|
+
const out = await runTurn({ prompt: job.p, model, callLLM, timeoutMs: 60000 })
|
|
22
|
+
results[job.i] = { i: job.i, prompt: job.p, result: out.result, error: out.error }
|
|
23
|
+
} catch (e) {
|
|
24
|
+
results[job.i] = { i: job.i, prompt: job.p, error: String(e?.message || e) }
|
|
25
|
+
}
|
|
26
|
+
stream.write(JSON.stringify(results[job.i]) + '\n')
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
await Promise.all(workers)
|
|
30
|
+
await new Promise(r => stream.end(r))
|
|
31
|
+
return { id, file, results }
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getAuthStore } from '../auth.js'
|
|
2
|
+
import { listProviders, resolveKey } from '../agent/credential_sources.js'
|
|
3
|
+
export async function login(provider, key) {
|
|
4
|
+
const env = (provider.toUpperCase() + '_API_KEY')
|
|
5
|
+
await getAuthStore().setCredential(env, key)
|
|
6
|
+
return { provider, stored: env }
|
|
7
|
+
}
|
|
8
|
+
export async function logout(provider) {
|
|
9
|
+
const env = (provider.toUpperCase() + '_API_KEY')
|
|
10
|
+
await getAuthStore().deleteCredential(env)
|
|
11
|
+
return { provider, removed: env }
|
|
12
|
+
}
|
|
13
|
+
export async function status() {
|
|
14
|
+
const out = []
|
|
15
|
+
for (const p of listProviders()) { const k = await resolveKey(p); out.push({ provider: p, source: k.source, hasKey: k.value != null }) }
|
|
16
|
+
return out
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function isAzureBaseUrl(url) {
|
|
2
|
+
if (!url) return false
|
|
3
|
+
return /\.openai\.azure\.com\b/i.test(String(url)) || /azure-api\.net\b/i.test(String(url))
|
|
4
|
+
}
|
|
5
|
+
export function detectFromEnv() {
|
|
6
|
+
const url = process.env.AZURE_OPENAI_ENDPOINT || process.env.OPENAI_BASE_URL
|
|
7
|
+
if (!url) return { azure: false }
|
|
8
|
+
return { azure: isAzureBaseUrl(url), endpoint: url, deployment: process.env.AZURE_OPENAI_DEPLOYMENT, apiVersion: process.env.AZURE_OPENAI_API_VERSION || '2024-08-01-preview' }
|
|
9
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { getFophHome } from '../home.js'
|
|
4
|
+
export async function createBackup({ outFile } = {}) {
|
|
5
|
+
const home = getFophHome()
|
|
6
|
+
const out = outFile || path.join(home, 'backups', 'freddie-' + new Date().toISOString().replace(/[:.]/g, '-') + '.tar.gz')
|
|
7
|
+
fs.mkdirSync(path.dirname(out), { recursive: true })
|
|
8
|
+
const { spawnSync } = await import('node:child_process')
|
|
9
|
+
const r = spawnSync('tar', ['-czf', out, '-C', path.dirname(home), path.basename(home)], { encoding: 'utf8' })
|
|
10
|
+
if (r.status === 0) return { ok: true, file: out, size: fs.statSync(out).size }
|
|
11
|
+
return { ok: false, stderr: r.stderr, hint: 'tar may be missing on Windows; install GNU tar or use a different archiver.' }
|
|
12
|
+
}
|
|
13
|
+
export function listBackups() {
|
|
14
|
+
const dir = path.join(getFophHome(), 'backups')
|
|
15
|
+
if (!fs.existsSync(dir)) return []
|
|
16
|
+
return fs.readdirSync(dir).filter(f => f.endsWith('.tar.gz')).map(f => ({ name: f, file: path.join(dir, f), mtime: fs.statSync(path.join(dir, f)).mtimeMs }))
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { getActiveSkin } from '../skin/engine.js'
|
|
2
|
+
const ART = [
|
|
3
|
+
' _____ _ _ _ ',
|
|
4
|
+
' |_ _| |__ ___ | |_| |__ ',
|
|
5
|
+
' | | | _ \\ / _ \\| __| _ \\ ',
|
|
6
|
+
' | | | | | | (_)| |_| | | |',
|
|
7
|
+
' |_| |_| |_|\\___/ \\__|_| |_|',
|
|
8
|
+
]
|
|
9
|
+
export function renderBanner() {
|
|
10
|
+
const skin = getActiveSkin()
|
|
11
|
+
return ART.join('\n') + '\n' + skin.branding.welcome
|
|
12
|
+
}
|
|
13
|
+
export function printBanner(out = process.stdout) { out.write(renderBanner() + '\n') }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
let _puppeteer = null
|
|
2
|
+
async function probe() { if (_puppeteer !== null) return _puppeteer; try { _puppeteer = (await import('puppeteer-core')).default } catch { _puppeteer = false } return _puppeteer }
|
|
3
|
+
export async function connectToBrowser({ wsEndpoint, browserURL } = {}) {
|
|
4
|
+
const p = await probe()
|
|
5
|
+
if (!p) return { error: 'puppeteer-core not installed' }
|
|
6
|
+
const browser = wsEndpoint ? await p.connect({ browserWSEndpoint: wsEndpoint }) : await p.connect({ browserURL })
|
|
7
|
+
return { browser, pages: await browser.pages() }
|
|
8
|
+
}
|
|
9
|
+
export async function attachExisting(port = 9222) {
|
|
10
|
+
return await connectToBrowser({ browserURL: 'http://127.0.0.1:' + port })
|
|
11
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
const _callbacks = new Map()
|
|
2
|
+
export function on(event, fn) { if (!_callbacks.has(event)) _callbacks.set(event, new Set()); _callbacks.get(event).add(fn); return () => _callbacks.get(event)?.delete(fn) }
|
|
3
|
+
export async function emit(event, payload) { const out = []; for (const fn of (_callbacks.get(event) || [])) try { out.push(await fn(payload)) } catch (e) { out.push({ error: String(e?.message || e) }) } return out }
|
|
4
|
+
export function listEvents() { return [..._callbacks.keys()] }
|
|
5
|
+
export function clearAll() { _callbacks.clear() }
|
package/src/cli/claw.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { paste } from './clipboard.js'
|
|
2
|
+
const HEAVY_THRESHOLD = 4000
|
|
3
|
+
export async function clawIntoMessage({ minLength = 50 } = {}) {
|
|
4
|
+
const text = (await paste()) || ''
|
|
5
|
+
if (text.length < minLength) return null
|
|
6
|
+
const trimmed = text.length > HEAVY_THRESHOLD ? text.slice(0, HEAVY_THRESHOLD) + '\n…[' + (text.length - HEAVY_THRESHOLD) + ' chars truncated]' : text
|
|
7
|
+
return { content: '[pasted]\n\n' + trimmed, originalLength: text.length, truncated: text.length > HEAVY_THRESHOLD }
|
|
8
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getActiveSkin } from '../skin/engine.js'
|
|
2
|
+
import { hex } from './colors.js'
|
|
3
|
+
export function info(msg) { return hex(getActiveSkin().colors.banner_text, msg) }
|
|
4
|
+
export function success(msg) { return hex('#22c55e', '✓ ' + msg) }
|
|
5
|
+
export function warning(msg) { return hex('#fbbf24', '! ' + msg) }
|
|
6
|
+
export function error(msg) { return hex('#ef4444', '✗ ' + msg) }
|
|
7
|
+
export function box(title, body) {
|
|
8
|
+
const line = '─'.repeat(Math.max(20, title.length + 4))
|
|
9
|
+
return '┌' + line + '┐\n│ ' + title + ' '.repeat(line.length - title.length - 1) + '│\n├' + line + '┤\n' + body.split('\n').map(l => '│ ' + l + ' '.repeat(Math.max(0, line.length - l.length - 1)) + '│').join('\n') + '\n└' + line + '┘'
|
|
10
|
+
}
|
|
11
|
+
export function table(rows) {
|
|
12
|
+
if (!rows.length) return ''
|
|
13
|
+
const keys = Object.keys(rows[0])
|
|
14
|
+
const widths = keys.map(k => Math.max(k.length, ...rows.map(r => String(r[k] ?? '').length)))
|
|
15
|
+
const sep = '+-' + widths.map(w => '-'.repeat(w)).join('-+-') + '-+'
|
|
16
|
+
const head = '| ' + keys.map((k, i) => k.padEnd(widths[i])).join(' | ') + ' |'
|
|
17
|
+
const body = rows.map(r => '| ' + keys.map((k, i) => String(r[k] ?? '').padEnd(widths[i])).join(' | ') + ' |').join('\n')
|
|
18
|
+
return [sep, head, sep, body, sep].join('\n')
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
function run(cmd, input = null) {
|
|
3
|
+
return new Promise((resolve) => {
|
|
4
|
+
const child = spawn(cmd[0], cmd.slice(1))
|
|
5
|
+
let stdout = ''
|
|
6
|
+
child.stdout?.on('data', d => stdout += d.toString())
|
|
7
|
+
child.on('close', code => resolve({ code, stdout }))
|
|
8
|
+
child.on('error', () => resolve({ code: -1, stdout: '' }))
|
|
9
|
+
if (input != null) { child.stdin?.write(input); child.stdin?.end() }
|
|
10
|
+
})
|
|
11
|
+
}
|
|
12
|
+
export async function copy(text) {
|
|
13
|
+
if (process.platform === 'win32') return run(['clip'], text)
|
|
14
|
+
if (process.platform === 'darwin') return run(['pbcopy'], text)
|
|
15
|
+
const xclip = await run(['xclip', '-selection', 'clipboard'], text)
|
|
16
|
+
if (xclip.code === 0) return xclip
|
|
17
|
+
return run(['xsel', '--clipboard', '--input'], text)
|
|
18
|
+
}
|
|
19
|
+
export async function paste() {
|
|
20
|
+
if (process.platform === 'win32') return (await run(['powershell', '-NoProfile', '-Command', 'Get-Clipboard'])).stdout
|
|
21
|
+
if (process.platform === 'darwin') return (await run(['pbpaste'])).stdout
|
|
22
|
+
const x = await run(['xclip', '-selection', 'clipboard', '-o'])
|
|
23
|
+
return x.code === 0 ? x.stdout : (await run(['xsel', '--clipboard', '--output'])).stdout
|
|
24
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const CODEX_MODELS = ['o3', 'o3-mini', 'o1', 'o1-mini', 'gpt-5', 'gpt-5-mini']
|
|
2
|
+
export function isCodexModel(id) {
|
|
3
|
+
if (!id) return false
|
|
4
|
+
return CODEX_MODELS.some(m => id === m || id.startsWith(m))
|
|
5
|
+
}
|
|
6
|
+
export function recommendCodexModel(scenario) {
|
|
7
|
+
return ({ reasoning: 'o3', fast: 'o3-mini', flagship: 'gpt-5', cheap: 'gpt-5-mini' })[scenario] || 'o3-mini'
|
|
8
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const RESET = '\x1b[0m'
|
|
2
|
+
const FG = { red: 31, green: 32, yellow: 33, blue: 34, magenta: 35, cyan: 36, white: 37, gray: 90 }
|
|
3
|
+
const BG = { red: 41, green: 42, yellow: 43, blue: 44, magenta: 45, cyan: 46, white: 47 }
|
|
4
|
+
const ATTR = { bold: 1, dim: 2, italic: 3, underline: 4 }
|
|
5
|
+
function wrap(code, s) { return '\x1b[' + code + 'm' + s + RESET }
|
|
6
|
+
export const fg = Object.fromEntries(Object.entries(FG).map(([k, c]) => [k, (s) => wrap(c, s)]))
|
|
7
|
+
export const bg = Object.fromEntries(Object.entries(BG).map(([k, c]) => [k, (s) => wrap(c, s)]))
|
|
8
|
+
export const attr = Object.fromEntries(Object.entries(ATTR).map(([k, c]) => [k, (s) => wrap(c, s)]))
|
|
9
|
+
export function hex(h, s) {
|
|
10
|
+
const r = parseInt(h.slice(1, 3), 16), g = parseInt(h.slice(3, 5), 16), b = parseInt(h.slice(5, 7), 16)
|
|
11
|
+
return '\x1b[38;2;' + r + ';' + g + ';' + b + 'm' + s + RESET
|
|
12
|
+
}
|
|
13
|
+
export function strip(s) { return String(s).replace(/\x1b\[[0-9;]*m/g, '') }
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { COMMAND_REGISTRY } from '../commands/registry.js'
|
|
4
|
+
|
|
5
|
+
export class SlashCommandCompleter {
|
|
6
|
+
constructor({ commands = COMMAND_REGISTRY } = {}) {
|
|
7
|
+
this.commands = commands
|
|
8
|
+
this.names = []
|
|
9
|
+
for (const c of commands) { this.names.push(c.name); for (const a of c.aliases || []) this.names.push(a) }
|
|
10
|
+
}
|
|
11
|
+
suggest(line) {
|
|
12
|
+
if (!line.startsWith('/')) return []
|
|
13
|
+
const stripped = line.slice(1)
|
|
14
|
+
const space = stripped.indexOf(' ')
|
|
15
|
+
if (space === -1) {
|
|
16
|
+
return this.names.filter(n => n.startsWith(stripped)).map(n => ({ value: '/' + n, kind: 'command', display: '/' + n + this._argHint(n) }))
|
|
17
|
+
}
|
|
18
|
+
const cmd = stripped.slice(0, space)
|
|
19
|
+
const def = this.commands.find(c => c.name === cmd || (c.aliases || []).includes(cmd))
|
|
20
|
+
if (!def) return []
|
|
21
|
+
return [{ value: line, kind: 'args', display: '/' + def.name + ' ' + (def.args_hint || ''), description: def.description }]
|
|
22
|
+
}
|
|
23
|
+
_argHint(name) {
|
|
24
|
+
const def = this.commands.find(c => c.name === name || (c.aliases || []).includes(name))
|
|
25
|
+
return def?.args_hint ? ' ' + def.args_hint : ''
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class PathCompleter {
|
|
30
|
+
constructor({ cwd = process.cwd(), maxResults = 20, includeHidden = false } = {}) {
|
|
31
|
+
this.cwd = cwd
|
|
32
|
+
this.maxResults = maxResults
|
|
33
|
+
this.includeHidden = includeHidden
|
|
34
|
+
}
|
|
35
|
+
suggest(input) {
|
|
36
|
+
try {
|
|
37
|
+
const abs = path.isAbsolute(input) ? input : path.resolve(this.cwd, input)
|
|
38
|
+
const dir = input.endsWith('/') || input.endsWith(path.sep) ? abs : path.dirname(abs)
|
|
39
|
+
const stem = input.endsWith('/') || input.endsWith(path.sep) ? '' : path.basename(abs)
|
|
40
|
+
if (!fs.existsSync(dir)) return []
|
|
41
|
+
const ents = fs.readdirSync(dir, { withFileTypes: true })
|
|
42
|
+
const out = []
|
|
43
|
+
for (const e of ents) {
|
|
44
|
+
if (!this.includeHidden && e.name.startsWith('.')) continue
|
|
45
|
+
if (!e.name.toLowerCase().startsWith(stem.toLowerCase())) continue
|
|
46
|
+
const isDir = e.isDirectory()
|
|
47
|
+
const value = path.join(dir, e.name) + (isDir ? path.sep : '')
|
|
48
|
+
out.push({ value, kind: isDir ? 'dir' : 'file', display: e.name + (isDir ? '/' : '') })
|
|
49
|
+
if (out.length >= this.maxResults) break
|
|
50
|
+
}
|
|
51
|
+
return out
|
|
52
|
+
} catch { return [] }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class FuzzyMatcher {
|
|
57
|
+
constructor(items, { keyFn = (x) => String(x) } = {}) {
|
|
58
|
+
this.items = items
|
|
59
|
+
this.keyFn = keyFn
|
|
60
|
+
}
|
|
61
|
+
score(query, target) {
|
|
62
|
+
const q = query.toLowerCase()
|
|
63
|
+
const t = target.toLowerCase()
|
|
64
|
+
if (!q) return 1
|
|
65
|
+
if (t === q) return 1000
|
|
66
|
+
if (t.startsWith(q)) return 500 - (t.length - q.length)
|
|
67
|
+
if (t.includes(q)) return 250 - (t.length - q.length)
|
|
68
|
+
let qi = 0, score = 0, prev = -1
|
|
69
|
+
for (let i = 0; i < t.length && qi < q.length; i++) {
|
|
70
|
+
if (t[i] === q[qi]) { score += (i - prev === 1 ? 5 : 1); prev = i; qi++ }
|
|
71
|
+
}
|
|
72
|
+
return qi === q.length ? score : 0
|
|
73
|
+
}
|
|
74
|
+
match(query, { limit = 10 } = {}) {
|
|
75
|
+
const scored = []
|
|
76
|
+
for (const it of this.items) {
|
|
77
|
+
const s = this.score(query, this.keyFn(it))
|
|
78
|
+
if (s > 0) scored.push({ item: it, score: s })
|
|
79
|
+
}
|
|
80
|
+
scored.sort((a, b) => b.score - a.score)
|
|
81
|
+
return scored.slice(0, limit).map(s => s.item)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createCompleter({ cwd } = {}) {
|
|
86
|
+
const slash = new SlashCommandCompleter()
|
|
87
|
+
const file = new PathCompleter({ cwd })
|
|
88
|
+
return {
|
|
89
|
+
suggest(line, cursor = line.length) {
|
|
90
|
+
const before = line.slice(0, cursor)
|
|
91
|
+
if (before.startsWith('/')) return slash.suggest(before)
|
|
92
|
+
const lastSpace = before.lastIndexOf(' ')
|
|
93
|
+
const tok = before.slice(lastSpace + 1)
|
|
94
|
+
if (tok.startsWith('./') || tok.startsWith('/') || tok.startsWith('~') || tok.includes('/')) return file.suggest(tok)
|
|
95
|
+
return []
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
}
|