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,29 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { registry } from './registry.js'
|
|
3
|
+
|
|
4
|
+
const _sessions = new Map()
|
|
5
|
+
|
|
6
|
+
registry.register({
|
|
7
|
+
name: 'terminal',
|
|
8
|
+
toolset: 'core',
|
|
9
|
+
schema: { name: 'terminal', description: 'Open a long-lived shell session, send input lines, capture output. Actions: open, send, read, close.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['open', 'send', 'read', 'close', 'list'] }, id: { type: 'string' }, input: { type: 'string' }, cwd: { type: 'string' } }, required: ['action'] } },
|
|
10
|
+
handler: async ({ action, id, input, cwd }) => {
|
|
11
|
+
if (action === 'open') {
|
|
12
|
+
const sid = 'term-' + Date.now()
|
|
13
|
+
const sh = process.platform === 'win32' ? 'cmd' : 'sh'
|
|
14
|
+
const child = spawn(sh, [], { cwd: cwd || process.cwd(), env: process.env })
|
|
15
|
+
const buf = { stdout: '', stderr: '' }
|
|
16
|
+
child.stdout?.on('data', d => buf.stdout += d.toString())
|
|
17
|
+
child.stderr?.on('data', d => buf.stderr += d.toString())
|
|
18
|
+
_sessions.set(sid, { child, buf })
|
|
19
|
+
return { id: sid, opened: true }
|
|
20
|
+
}
|
|
21
|
+
const s = _sessions.get(id)
|
|
22
|
+
if (!s) return { error: 'unknown terminal id: ' + id }
|
|
23
|
+
if (action === 'send') { s.child.stdin?.write(input + '\n'); return { sent: true } }
|
|
24
|
+
if (action === 'read') { const out = { ...s.buf }; s.buf.stdout = ''; s.buf.stderr = ''; return out }
|
|
25
|
+
if (action === 'close') { try { s.child.kill('SIGTERM') } catch {} _sessions.delete(id); return { closed: id } }
|
|
26
|
+
if (action === 'list') return { sessions: [..._sessions.keys()] }
|
|
27
|
+
return { error: 'unknown action' }
|
|
28
|
+
},
|
|
29
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { getFophHome } from '../home.js'
|
|
4
|
+
import { registry } from './registry.js'
|
|
5
|
+
|
|
6
|
+
function policyPath() { return path.join(getFophHome(), 'policy.json') }
|
|
7
|
+
function loadPolicy() { try { return JSON.parse(fs.readFileSync(policyPath(), 'utf8')) } catch { return { tools: {}, hosts: { allow: [], deny: [] } } } }
|
|
8
|
+
|
|
9
|
+
registry.register({
|
|
10
|
+
name: 'tirith_security',
|
|
11
|
+
toolset: 'core',
|
|
12
|
+
schema: { name: 'tirith_security', description: 'Evaluate a candidate action against ~/.freddie/policy.json. Returns allow|deny|ask.', parameters: { type: 'object', properties: { kind: { type: 'string' }, target: { type: 'string' } }, required: ['kind', 'target'] } },
|
|
13
|
+
handler: async ({ kind, target }) => {
|
|
14
|
+
const p = loadPolicy()
|
|
15
|
+
if (kind === 'tool') {
|
|
16
|
+
const t = p.tools?.[target]
|
|
17
|
+
if (t === 'allow' || t === 'deny') return { decision: t }
|
|
18
|
+
}
|
|
19
|
+
if (kind === 'host') {
|
|
20
|
+
if (p.hosts?.deny?.some(d => target.includes(d))) return { decision: 'deny' }
|
|
21
|
+
if (p.hosts?.allow?.some(d => target.includes(d))) return { decision: 'allow' }
|
|
22
|
+
}
|
|
23
|
+
return { decision: 'ask' }
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { db } from '../db.js'
|
|
2
|
+
import { registry } from './registry.js'
|
|
3
|
+
|
|
4
|
+
async function init() {
|
|
5
|
+
const d = await db()
|
|
6
|
+
d.exec(`CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, content TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created INTEGER NOT NULL, updated INTEGER NOT NULL)`)
|
|
7
|
+
return d
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ACTIONS = {
|
|
11
|
+
add: async ({ session_id = null, content }) => {
|
|
12
|
+
if (!content) return { error: 'content required' }
|
|
13
|
+
const d = await init(); const now = Date.now()
|
|
14
|
+
const info = d.prepare(`INSERT INTO todos (session_id, content, status, created, updated) VALUES (?, ?, 'pending', ?, ?)`).run(session_id, content, now, now)
|
|
15
|
+
return { id: info.lastInsertRowid, content, status: 'pending' }
|
|
16
|
+
},
|
|
17
|
+
list: async ({ session_id = null }) => {
|
|
18
|
+
const d = await init()
|
|
19
|
+
const rows = session_id ? d.prepare(`SELECT * FROM todos WHERE session_id = ? ORDER BY id DESC`).all(session_id) : d.prepare(`SELECT * FROM todos ORDER BY id DESC`).all()
|
|
20
|
+
return { todos: rows }
|
|
21
|
+
},
|
|
22
|
+
update: async ({ id, status }) => {
|
|
23
|
+
if (!id) return { error: 'id required' }
|
|
24
|
+
(await init()).prepare(`UPDATE todos SET status = ?, updated = ? WHERE id = ?`).run(status, Date.now(), id)
|
|
25
|
+
return { id, status }
|
|
26
|
+
},
|
|
27
|
+
complete: async ({ id }) => ACTIONS.update({ id, status: 'completed' }),
|
|
28
|
+
delete: async ({ id }) => { (await init()).prepare(`DELETE FROM todos WHERE id = ?`).run(id); return { id, deleted: true } },
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
registry.register({
|
|
32
|
+
name: 'todo',
|
|
33
|
+
toolset: 'core',
|
|
34
|
+
schema: {
|
|
35
|
+
name: 'todo',
|
|
36
|
+
description: 'Manage per-session todos. Actions: add, list, update, complete, delete.',
|
|
37
|
+
parameters: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
action: { type: 'string', enum: Object.keys(ACTIONS) },
|
|
41
|
+
content: { type: 'string' },
|
|
42
|
+
id: { type: 'number' },
|
|
43
|
+
status: { type: 'string' },
|
|
44
|
+
session_id: { type: 'string' },
|
|
45
|
+
},
|
|
46
|
+
required: ['action'],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
handler: async (args) => {
|
|
50
|
+
const fn = ACTIONS[args.action]
|
|
51
|
+
if (!fn) return { error: 'unknown action: ' + args.action }
|
|
52
|
+
return fn(args)
|
|
53
|
+
},
|
|
54
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
|
|
3
|
+
export function shapeArgs(schema, args) {
|
|
4
|
+
if (!schema?.properties) return args
|
|
5
|
+
const out = {}
|
|
6
|
+
for (const [k, def] of Object.entries(schema.properties)) {
|
|
7
|
+
if (k in args) out[k] = args[k]
|
|
8
|
+
else if ('default' in def) out[k] = def.default
|
|
9
|
+
}
|
|
10
|
+
return out
|
|
11
|
+
}
|
|
12
|
+
export function describeTools(filter = null) {
|
|
13
|
+
let list = registry.list()
|
|
14
|
+
if (filter) list = list.filter(t => t.toolset === filter)
|
|
15
|
+
return list.map(t => ({ name: t.name, description: t.schema.description, toolset: t.toolset }))
|
|
16
|
+
}
|
|
17
|
+
registry.register({
|
|
18
|
+
name: 'tool_backend_helpers',
|
|
19
|
+
toolset: 'core',
|
|
20
|
+
schema: { name: 'tool_backend_helpers', description: 'Helper meta-tool: describeTools(filter), shapeArgs(schema, args).', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['describe', 'shape'] }, filter: { type: 'string' }, schema: {}, args: {} }, required: ['action'] } },
|
|
21
|
+
handler: async ({ action, filter, schema, args }) => {
|
|
22
|
+
if (action === 'describe') return { tools: describeTools(filter) }
|
|
23
|
+
if (action === 'shape') return { args: shapeArgs(schema, args || {}) }
|
|
24
|
+
return { error: 'unknown action' }
|
|
25
|
+
},
|
|
26
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
import { getConfigValue } from '../config.js'
|
|
3
|
+
|
|
4
|
+
export function truncate(s, max = null) {
|
|
5
|
+
const limit = max ?? getConfigValue('tool.output_limit', 100_000)
|
|
6
|
+
const t = String(s)
|
|
7
|
+
if (t.length <= limit) return t
|
|
8
|
+
return t.slice(0, limit) + `\n…[truncated ${t.length - limit} chars]`
|
|
9
|
+
}
|
|
10
|
+
registry.register({
|
|
11
|
+
name: 'tool_output_limits',
|
|
12
|
+
toolset: 'core',
|
|
13
|
+
schema: { name: 'tool_output_limits', description: 'Apply the configured output truncation cap to a string.', parameters: { type: 'object', properties: { text: { type: 'string' }, max: { type: 'number' } }, required: ['text'] } },
|
|
14
|
+
handler: async ({ text, max }) => ({ text: truncate(text, max) }),
|
|
15
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
import { getFophHome } from '../home.js'
|
|
5
|
+
import { registry } from './registry.js'
|
|
6
|
+
|
|
7
|
+
function dir() { const d = path.join(getFophHome(), 'tool-results'); fs.mkdirSync(d, { recursive: true }); return d }
|
|
8
|
+
|
|
9
|
+
registry.register({
|
|
10
|
+
name: 'tool_result_storage',
|
|
11
|
+
toolset: 'core',
|
|
12
|
+
schema: { name: 'tool_result_storage', description: 'Persist a large tool result to disk; return reference token. Actions: store, fetch, list, delete.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['store', 'fetch', 'list', 'delete'] }, content: { type: 'string' }, token: { type: 'string' } }, required: ['action'] } },
|
|
13
|
+
handler: async ({ action, content, token }) => {
|
|
14
|
+
if (action === 'store') { const t = crypto.randomBytes(8).toString('hex'); fs.writeFileSync(path.join(dir(), t + '.txt'), content || '', 'utf8'); return { token: t, bytes: (content || '').length } }
|
|
15
|
+
if (action === 'fetch') { const f = path.join(dir(), token + '.txt'); return fs.existsSync(f) ? { content: fs.readFileSync(f, 'utf8') } : { error: 'not found' } }
|
|
16
|
+
if (action === 'list') return { tokens: fs.readdirSync(dir()).filter(f => f.endsWith('.txt')).map(f => f.replace(/\.txt$/, '')) }
|
|
17
|
+
if (action === 'delete') { const f = path.join(dir(), token + '.txt'); if (fs.existsSync(f)) fs.unlinkSync(f); return { deleted: token } }
|
|
18
|
+
return { error: 'unknown action' }
|
|
19
|
+
},
|
|
20
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import { registry } from './registry.js'
|
|
3
|
+
registry.register({
|
|
4
|
+
name: 'transcription',
|
|
5
|
+
toolset: 'creative',
|
|
6
|
+
schema: { name: 'transcription', description: 'Transcribe audio with OpenAI Whisper.', parameters: { type: 'object', properties: { file_path: { type: 'string' }, model: { type: 'string', default: 'whisper-1' } }, required: ['file_path'] } },
|
|
7
|
+
requiresEnv: ['OPENAI_API_KEY'],
|
|
8
|
+
checkFn: () => Boolean(process.env.OPENAI_API_KEY),
|
|
9
|
+
handler: async ({ file_path, model = 'whisper-1' }) => {
|
|
10
|
+
if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
|
|
11
|
+
if (!fs.existsSync(file_path)) return { error: 'file not found: ' + file_path }
|
|
12
|
+
const blob = new Blob([fs.readFileSync(file_path)])
|
|
13
|
+
const fd = new FormData()
|
|
14
|
+
fd.append('file', blob, file_path.split(/[\\/]/).pop())
|
|
15
|
+
fd.append('model', model)
|
|
16
|
+
const r = await fetch('https://api.openai.com/v1/audio/transcriptions', { method: 'POST', headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}` }, body: fd })
|
|
17
|
+
return await r.json()
|
|
18
|
+
},
|
|
19
|
+
})
|
package/src/tools/tts.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
registry.register({
|
|
3
|
+
name: 'tts',
|
|
4
|
+
toolset: 'creative',
|
|
5
|
+
schema: { name: 'tts', description: 'Synthesize speech (OpenAI tts-1 or ElevenLabs).', parameters: { type: 'object', properties: { text: { type: 'string' }, provider: { type: 'string', enum: ['openai', 'elevenlabs'], default: 'openai' }, voice: { type: 'string' } }, required: ['text'] } },
|
|
6
|
+
requiresEnv: ['OPENAI_API_KEY or ELEVENLABS_API_KEY'],
|
|
7
|
+
checkFn: () => Boolean(process.env.OPENAI_API_KEY || process.env.ELEVENLABS_API_KEY),
|
|
8
|
+
handler: async ({ text, provider = 'openai', voice = 'alloy' }) => {
|
|
9
|
+
if (provider === 'elevenlabs') {
|
|
10
|
+
if (!process.env.ELEVENLABS_API_KEY) return { error: 'ELEVENLABS_API_KEY required' }
|
|
11
|
+
const v = voice || '21m00Tcm4TlvDq8ikWAM'
|
|
12
|
+
const r = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${v}`, { method: 'POST', headers: { 'xi-api-key': process.env.ELEVENLABS_API_KEY, 'content-type': 'application/json' }, body: JSON.stringify({ text }) })
|
|
13
|
+
return { status: r.status, contentType: r.headers.get('content-type'), bytes: (await r.arrayBuffer()).byteLength }
|
|
14
|
+
}
|
|
15
|
+
if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
|
|
16
|
+
const r = await fetch('https://api.openai.com/v1/audio/speech', { method: 'POST', headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ model: 'tts-1', input: text, voice }) })
|
|
17
|
+
return { status: r.status, bytes: (await r.arrayBuffer()).byteLength }
|
|
18
|
+
},
|
|
19
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
const SUSPICIOUS = ['phish', 'malware', '.onion']
|
|
3
|
+
const PRIVATE_RANGES = [/^10\./, /^192\.168\./, /^172\.(1[6-9]|2\d|3[01])\./, /^127\./, /^0\./, /^169\.254\./]
|
|
4
|
+
registry.register({
|
|
5
|
+
name: 'url_safety',
|
|
6
|
+
toolset: 'core',
|
|
7
|
+
schema: { name: 'url_safety', description: 'Heuristic URL safety check (private IPs, known-bad TLDs, scheme).', parameters: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] } },
|
|
8
|
+
handler: async ({ url }) => {
|
|
9
|
+
let u; try { u = new URL(url) } catch { return { safe: false, reason: 'invalid URL' } }
|
|
10
|
+
if (!['http:', 'https:'].includes(u.protocol)) return { safe: false, reason: 'unsupported scheme: ' + u.protocol }
|
|
11
|
+
if (PRIVATE_RANGES.some(re => re.test(u.hostname))) return { safe: false, reason: 'private IP host' }
|
|
12
|
+
for (const s of SUSPICIOUS) if (u.hostname.includes(s)) return { safe: false, reason: 'suspicious token: ' + s }
|
|
13
|
+
return { safe: true, host: u.hostname }
|
|
14
|
+
},
|
|
15
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
registry.register({
|
|
3
|
+
name: 'vision',
|
|
4
|
+
toolset: 'creative',
|
|
5
|
+
schema: { name: 'vision', description: 'Describe an image (URL or base64) using a vision-capable LLM.', parameters: { type: 'object', properties: { image_url: { type: 'string' }, prompt: { type: 'string', default: 'Describe this image.' }, provider: { type: 'string', enum: ['openai', 'anthropic'], default: 'openai' } }, required: ['image_url'] } },
|
|
6
|
+
requiresEnv: ['OPENAI_API_KEY or ANTHROPIC_API_KEY'],
|
|
7
|
+
checkFn: () => Boolean(process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY),
|
|
8
|
+
handler: async ({ image_url, prompt = 'Describe this image.', provider = 'openai' }) => {
|
|
9
|
+
if (provider === 'anthropic') {
|
|
10
|
+
if (!process.env.ANTHROPIC_API_KEY) return { error: 'ANTHROPIC_API_KEY required' }
|
|
11
|
+
const r = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'x-api-key': process.env.ANTHROPIC_API_KEY, 'anthropic-version': '2023-06-01', 'content-type': 'application/json' }, body: JSON.stringify({ model: 'claude-sonnet-4-6', max_tokens: 1024, messages: [{ role: 'user', content: [{ type: 'image', source: { type: 'url', url: image_url } }, { type: 'text', text: prompt }] }] }) })
|
|
12
|
+
return await r.json()
|
|
13
|
+
}
|
|
14
|
+
if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
|
|
15
|
+
const r = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: [{ type: 'text', text: prompt }, { type: 'image_url', image_url: { url: image_url } }] }] }) })
|
|
16
|
+
return await r.json()
|
|
17
|
+
},
|
|
18
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
registry.register({
|
|
3
|
+
name: 'voice_mode',
|
|
4
|
+
toolset: 'creative',
|
|
5
|
+
schema: { name: 'voice_mode', description: 'Toggle full-duplex voice (transcription in + tts out) for the active session. Returns the new state.', parameters: { type: 'object', properties: { enabled: { type: 'boolean' } } } },
|
|
6
|
+
handler: async ({ enabled }, ctx = {}) => {
|
|
7
|
+
if (typeof ctx.setVoiceMode === 'function') return await ctx.setVoiceMode(Boolean(enabled))
|
|
8
|
+
return { enabled: Boolean(enabled), note: 'voice mode toggled in-process; bind ctx.setVoiceMode to wire to a real audio loop' }
|
|
9
|
+
},
|
|
10
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
|
|
3
|
+
registry.register({
|
|
4
|
+
name: 'web_search',
|
|
5
|
+
toolset: 'browse',
|
|
6
|
+
schema: {
|
|
7
|
+
name: 'web_search',
|
|
8
|
+
description: 'Search the web (DuckDuckGo HTML or SerpAPI). Returns title/url/snippet list.',
|
|
9
|
+
parameters: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
query: { type: 'string' },
|
|
13
|
+
limit: { type: 'number', default: 5 },
|
|
14
|
+
},
|
|
15
|
+
required: ['query'],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
checkFn: () => true,
|
|
19
|
+
requiresEnv: ['SERPAPI_KEY (optional, falls back to DDG)'],
|
|
20
|
+
handler: async ({ query, limit = 5 }) => {
|
|
21
|
+
if (process.env.SERPAPI_KEY) {
|
|
22
|
+
const url = `https://serpapi.com/search.json?q=${encodeURIComponent(query)}&api_key=${process.env.SERPAPI_KEY}`
|
|
23
|
+
const data = await fetch(url).then(r => r.json())
|
|
24
|
+
const results = (data.organic_results || []).slice(0, limit).map(r => ({ title: r.title, url: r.link, snippet: r.snippet }))
|
|
25
|
+
return { results }
|
|
26
|
+
}
|
|
27
|
+
const fetchFn = globalThis.__fophFetch || fetch
|
|
28
|
+
const html = await fetchFn(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`).then(r => r.text())
|
|
29
|
+
const results = []
|
|
30
|
+
const re = /<a class="result__a"[^>]*href="([^"]+)"[^>]*>([^<]+)<\/a>[\s\S]*?<a class="result__snippet"[^>]*>([^<]+)<\/a>/g
|
|
31
|
+
let m
|
|
32
|
+
while ((m = re.exec(html)) && results.length < limit) {
|
|
33
|
+
results.push({ url: m[1], title: m[2].replace(/&/g, '&'), snippet: m[3].replace(/<\/?b>/g, '') })
|
|
34
|
+
}
|
|
35
|
+
return { results }
|
|
36
|
+
},
|
|
37
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
registry.register({
|
|
3
|
+
name: 'web_fetch',
|
|
4
|
+
toolset: 'browse',
|
|
5
|
+
schema: { name: 'web_fetch', description: 'Fetch a URL and return text/json/headers.', parameters: { type: 'object', properties: { url: { type: 'string' }, method: { type: 'string', default: 'GET' }, headers: {}, body: { type: 'string' }, parse: { type: 'string', enum: ['text', 'json'] } }, required: ['url'] } },
|
|
6
|
+
handler: async ({ url, method = 'GET', headers = {}, body, parse = 'text' }) => {
|
|
7
|
+
const r = await fetch(url, { method, headers, body })
|
|
8
|
+
const ct = r.headers.get('content-type')
|
|
9
|
+
const out = parse === 'json' ? await r.json().catch(() => null) : await r.text()
|
|
10
|
+
return { status: r.status, contentType: ct, body: out }
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
registry.register({
|
|
14
|
+
name: 'web_extract',
|
|
15
|
+
toolset: 'browse',
|
|
16
|
+
schema: { name: 'web_extract', description: 'Strip tags from HTML to plain text.', parameters: { type: 'object', properties: { html: { type: 'string' } }, required: ['html'] } },
|
|
17
|
+
handler: async ({ html }) => ({ text: String(html).replace(/<script[\s\S]*?<\/script>/gi, '').replace(/<style[\s\S]*?<\/style>/gi, '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim() }),
|
|
18
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
import { getConfigValue } from '../config.js'
|
|
3
|
+
registry.register({
|
|
4
|
+
name: 'website_policy',
|
|
5
|
+
toolset: 'core',
|
|
6
|
+
schema: { name: 'website_policy', description: 'Per-host fetch policy (rate limit, allow/deny). Reads config.website_policy.', parameters: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] } },
|
|
7
|
+
handler: async ({ url }) => {
|
|
8
|
+
const policy = getConfigValue('website_policy', { allow: [], deny: [], ratelimit_ms: 1000 }) || {}
|
|
9
|
+
const u = new URL(url)
|
|
10
|
+
if (policy.deny?.some(d => u.hostname.includes(d))) return { decision: 'deny' }
|
|
11
|
+
if (policy.allow?.length && !policy.allow.some(a => u.hostname.includes(a))) return { decision: 'deny', reason: 'not in allow list' }
|
|
12
|
+
return { decision: 'allow', ratelimit_ms: policy.ratelimit_ms || 1000 }
|
|
13
|
+
},
|
|
14
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { registry } from './registry.js'
|
|
4
|
+
|
|
5
|
+
registry.register({
|
|
6
|
+
name: 'write',
|
|
7
|
+
toolset: 'core',
|
|
8
|
+
schema: {
|
|
9
|
+
name: 'write',
|
|
10
|
+
description: 'Write content to a file (overwrites).',
|
|
11
|
+
parameters: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
path: { type: 'string' },
|
|
15
|
+
content: { type: 'string' },
|
|
16
|
+
},
|
|
17
|
+
required: ['path', 'content'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
handler: async ({ path: p, content }) => {
|
|
21
|
+
fs.mkdirSync(path.dirname(p), { recursive: true })
|
|
22
|
+
fs.writeFileSync(p, content, 'utf8')
|
|
23
|
+
return { path: p, bytes: Buffer.byteLength(content, 'utf8') }
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
registry.register({
|
|
3
|
+
name: 'xai_grok',
|
|
4
|
+
toolset: 'core',
|
|
5
|
+
schema: { name: 'xai_grok', description: 'Chat completion via xAI Grok.', parameters: { type: 'object', properties: { prompt: { type: 'string' }, model: { type: 'string', default: 'grok-3' } }, required: ['prompt'] } },
|
|
6
|
+
requiresEnv: ['XAI_API_KEY'],
|
|
7
|
+
checkFn: () => Boolean(process.env.XAI_API_KEY),
|
|
8
|
+
handler: async ({ prompt, model = 'grok-3' }) => {
|
|
9
|
+
if (!process.env.XAI_API_KEY) return { error: 'XAI_API_KEY required' }
|
|
10
|
+
const r = await fetch('https://api.x.ai/v1/chat/completions', { method: 'POST', headers: { authorization: `Bearer ${process.env.XAI_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ model, messages: [{ role: 'user', content: prompt }] }) })
|
|
11
|
+
return await r.json()
|
|
12
|
+
},
|
|
13
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
registry.register({
|
|
3
|
+
name: 'yuanbao_tools',
|
|
4
|
+
toolset: 'core',
|
|
5
|
+
schema: { name: 'yuanbao_tools', description: 'Tencent Yuanbao chat completion (Hunyuan).', parameters: { type: 'object', properties: { prompt: { type: 'string' }, model: { type: 'string', default: 'hunyuan-pro' } }, required: ['prompt'] } },
|
|
6
|
+
requiresEnv: ['YUANBAO_API_KEY'],
|
|
7
|
+
checkFn: () => Boolean(process.env.YUANBAO_API_KEY),
|
|
8
|
+
handler: async ({ prompt, model = 'hunyuan-pro' }) => {
|
|
9
|
+
if (!process.env.YUANBAO_API_KEY) return { error: 'YUANBAO_API_KEY required' }
|
|
10
|
+
const r = await fetch('https://api.hunyuan.cloud.tencent.com/v1/chat/completions', { method: 'POST', headers: { authorization: `Bearer ${process.env.YUANBAO_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ model, messages: [{ role: 'user', content: prompt }] }) })
|
|
11
|
+
return await r.json()
|
|
12
|
+
},
|
|
13
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { saveConfigValue } from './config.js'
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_DISTRIBUTIONS = {
|
|
4
|
+
coder: { enabledToolsets: ['core', 'browse'], disabledToolsets: [] },
|
|
5
|
+
researcher: { enabledToolsets: ['core', 'browse', 'creative'], disabledToolsets: [] },
|
|
6
|
+
ops: { enabledToolsets: ['core'], disabledToolsets: ['creative', 'browse'] },
|
|
7
|
+
minimal: { enabledToolsets: ['core'], disabledToolsets: ['browse', 'creative'] },
|
|
8
|
+
full: { enabledToolsets: ['core', 'browse', 'creative'], disabledToolsets: [] },
|
|
9
|
+
}
|
|
10
|
+
export function listDistributions() { return Object.keys(DEFAULT_DISTRIBUTIONS) }
|
|
11
|
+
export function getDistribution(name) { return DEFAULT_DISTRIBUTIONS[name] || null }
|
|
12
|
+
export function applyDistribution(name) {
|
|
13
|
+
const d = getDistribution(name)
|
|
14
|
+
if (!d) throw new Error('unknown distribution: ' + name)
|
|
15
|
+
saveConfigValue('toolsets.enabled', d.enabledToolsets)
|
|
16
|
+
saveConfigValue('toolsets.disabled', d.disabledToolsets)
|
|
17
|
+
return d
|
|
18
|
+
}
|
package/src/toolsets.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { registry, discoverBuiltinTools } from './tools/registry.js'
|
|
2
|
+
|
|
3
|
+
export const _FREDDIE_CORE_TOOLS = ['bash', 'read', 'write', 'edit', 'grep']
|
|
4
|
+
|
|
5
|
+
export async function getEnabledToolSchemas(enabled = ['core'], disabled = []) {
|
|
6
|
+
await discoverBuiltinTools()
|
|
7
|
+
const all = registry.available()
|
|
8
|
+
const enabledSet = new Set(enabled)
|
|
9
|
+
const disabledSet = new Set(disabled)
|
|
10
|
+
return all.filter(t => enabledSet.has(t.toolset) && !disabledSet.has(t.name)).map(t => t.schema)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function getEnabledToolNames(enabled = ['core'], disabled = []) {
|
|
14
|
+
await discoverBuiltinTools()
|
|
15
|
+
const all = registry.available()
|
|
16
|
+
const enabledSet = new Set(enabled)
|
|
17
|
+
const disabledSet = new Set(disabled)
|
|
18
|
+
return all.filter(t => enabledSet.has(t.toolset) && !disabledSet.has(t.name)).map(t => t.name)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function getAvailableToolsets() {
|
|
22
|
+
await discoverBuiltinTools()
|
|
23
|
+
const ts = new Set()
|
|
24
|
+
for (const t of registry.list()) ts.add(t.toolset)
|
|
25
|
+
return [...ts]
|
|
26
|
+
}
|
package/src/tui/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { interactive } from '../cli/interactive.js'
|
|
2
|
+
import { logger } from '../observability/log.js'
|
|
3
|
+
|
|
4
|
+
const log = logger('tui')
|
|
5
|
+
|
|
6
|
+
let _piTui = null
|
|
7
|
+
async function probePiTui() {
|
|
8
|
+
if (_piTui !== null) return _piTui
|
|
9
|
+
try { _piTui = await import('@mariozechner/pi-tui') } catch { _piTui = false }
|
|
10
|
+
return _piTui
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function launchTui({ output = process.stdout, callLLM = null } = {}) {
|
|
14
|
+
const tui = await probePiTui()
|
|
15
|
+
if (!tui) {
|
|
16
|
+
log.info('pi-tui unavailable, falling back to readline cli')
|
|
17
|
+
return interactive({ output, callLLM })
|
|
18
|
+
}
|
|
19
|
+
if (!process.stdout.isTTY) {
|
|
20
|
+
log.info('non-tty, falling back to readline cli')
|
|
21
|
+
return interactive({ output, callLLM })
|
|
22
|
+
}
|
|
23
|
+
if (typeof tui.InteractiveMode === 'function') return new tui.InteractiveMode({ callLLM })
|
|
24
|
+
log.info('pi-tui shape unfamiliar, falling back')
|
|
25
|
+
return interactive({ output, callLLM })
|
|
26
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
export const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v))
|
|
3
|
+
export const sleep = (ms) => new Promise(r => setTimeout(r, ms))
|
|
4
|
+
export async function retry({ fn, attempts = 3, backoff = 200, factor = 2 }) {
|
|
5
|
+
let last
|
|
6
|
+
for (let i = 0; i < attempts; i++) {
|
|
7
|
+
try { return await fn(i) } catch (e) { last = e; if (i < attempts - 1) await sleep(backoff * Math.pow(factor, i)) }
|
|
8
|
+
}
|
|
9
|
+
throw last
|
|
10
|
+
}
|
|
11
|
+
export function debounce(fn, ms) {
|
|
12
|
+
let t = null
|
|
13
|
+
return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms) }
|
|
14
|
+
}
|
|
15
|
+
export function sha256(s) { return crypto.createHash('sha256').update(s).digest('hex') }
|
|
16
|
+
export function randomId(len = 12) { return crypto.randomBytes(len).toString('hex') }
|
|
17
|
+
export function deepClone(o) { return JSON.parse(JSON.stringify(o)) }
|
|
18
|
+
export function deepMerge(t, s) {
|
|
19
|
+
if (!s || typeof s !== 'object') return t
|
|
20
|
+
for (const k of Object.keys(s)) {
|
|
21
|
+
if (s[k] && typeof s[k] === 'object' && !Array.isArray(s[k]) && t[k] && typeof t[k] === 'object' && !Array.isArray(t[k])) deepMerge(t[k], s[k])
|
|
22
|
+
else t[k] = s[k]
|
|
23
|
+
}
|
|
24
|
+
return t
|
|
25
|
+
}
|
|
26
|
+
const SECRET_PATTERNS = [
|
|
27
|
+
/sk-[A-Za-z0-9-_]{20,}/g,
|
|
28
|
+
/ghp_[A-Za-z0-9]{36}/g,
|
|
29
|
+
/xox[baprs]-[A-Za-z0-9-]{10,}/g,
|
|
30
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
31
|
+
/[a-zA-Z0-9._%+-]+:[^@\s]+@[a-zA-Z0-9.-]+/g,
|
|
32
|
+
/Bearer\s+[A-Za-z0-9._-]+/gi,
|
|
33
|
+
]
|
|
34
|
+
export function redactSecret(s) {
|
|
35
|
+
let out = String(s)
|
|
36
|
+
for (const re of SECRET_PATTERNS) out = out.replace(re, '[REDACTED]')
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
export function ansiStrip(s) {
|
|
40
|
+
return String(s).replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '').replace(/\x1b\][^\x07]*\x07/g, '')
|
|
41
|
+
}
|
|
42
|
+
export function fuzzyMatch(needle, hay) {
|
|
43
|
+
needle = String(needle).toLowerCase(); hay = String(hay).toLowerCase()
|
|
44
|
+
let i = 0, score = 0
|
|
45
|
+
for (const c of hay) {
|
|
46
|
+
if (i < needle.length && c === needle[i]) { score += 2 + (score & 1); i++ } else { score = score & ~1 }
|
|
47
|
+
}
|
|
48
|
+
return i === needle.length ? score : 0
|
|
49
|
+
}
|
|
50
|
+
export function parseEnvKeyList(name) {
|
|
51
|
+
const v = process.env[name]
|
|
52
|
+
if (!v) return []
|
|
53
|
+
return v.split(',').map(x => x.trim()).filter(Boolean)
|
|
54
|
+
}
|