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
|
+
import fs from 'node:fs'
|
|
2
|
+
import { registry } from './registry.js'
|
|
3
|
+
|
|
4
|
+
registry.register({
|
|
5
|
+
name: 'edit',
|
|
6
|
+
toolset: 'core',
|
|
7
|
+
schema: {
|
|
8
|
+
name: 'edit',
|
|
9
|
+
description: 'Replace exact string in file. Fails if old_string occurs zero or multiple times unless replace_all.',
|
|
10
|
+
parameters: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
path: { type: 'string' },
|
|
14
|
+
old_string: { type: 'string' },
|
|
15
|
+
new_string: { type: 'string' },
|
|
16
|
+
replace_all: { type: 'boolean', default: false },
|
|
17
|
+
},
|
|
18
|
+
required: ['path', 'old_string', 'new_string'],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
handler: async ({ path: p, old_string, new_string, replace_all = false }) => {
|
|
22
|
+
if (!fs.existsSync(p)) return { error: `not found: ${p}` }
|
|
23
|
+
const src = fs.readFileSync(p, 'utf8')
|
|
24
|
+
const occurrences = src.split(old_string).length - 1
|
|
25
|
+
if (occurrences === 0) return { error: 'old_string not found' }
|
|
26
|
+
if (occurrences > 1 && !replace_all) return { error: `old_string matches ${occurrences} times; pass replace_all=true` }
|
|
27
|
+
const out = replace_all ? src.split(old_string).join(new_string) : src.replace(old_string, new_string)
|
|
28
|
+
fs.writeFileSync(p, out, 'utf8')
|
|
29
|
+
return { path: p, replacements: replace_all ? occurrences : 1 }
|
|
30
|
+
},
|
|
31
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
import { getConfigValue } from '../config.js'
|
|
3
|
+
|
|
4
|
+
export function buildBashEnv() {
|
|
5
|
+
const allow = getConfigValue('terminal.env_passthrough', ['HOME', 'USER', 'LANG', 'PATH', 'SHELL']) || []
|
|
6
|
+
const out = {}
|
|
7
|
+
for (const k of allow) if (process.env[k]) out[k] = process.env[k]
|
|
8
|
+
return out
|
|
9
|
+
}
|
|
10
|
+
registry.register({
|
|
11
|
+
name: 'env_passthrough',
|
|
12
|
+
toolset: 'core',
|
|
13
|
+
schema: { name: 'env_passthrough', description: 'Compute the env-var subset that should be passed through to spawned shells.', parameters: { type: 'object', properties: {} } },
|
|
14
|
+
handler: async () => ({ env: buildBashEnv() }),
|
|
15
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class BaseEnvironment {
|
|
2
|
+
constructor(opts = {}) { this.opts = opts; this.cwd = opts.cwd || '/workspace'; this.name = 'base' }
|
|
3
|
+
async run(_cmd, _o) { throw new Error(this.name + '.run() not implemented') }
|
|
4
|
+
async put(_l, _r) { throw new Error(this.name + '.put() not implemented') }
|
|
5
|
+
async get(_r, _l) { throw new Error(this.name + '.get() not implemented') }
|
|
6
|
+
async shutdown() {}
|
|
7
|
+
async ready() { return true }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function fetchJson(url, { method = 'GET', headers = {}, body, timeoutMs = 60000 } = {}) {
|
|
11
|
+
const ctrl = new AbortController()
|
|
12
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs)
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(url, { method, headers: { 'content-type': 'application/json', ...headers }, body: body ? JSON.stringify(body) : undefined, signal: ctrl.signal })
|
|
15
|
+
const txt = await res.text()
|
|
16
|
+
let json = null
|
|
17
|
+
try { json = txt ? JSON.parse(txt) : null } catch { json = { raw: txt } }
|
|
18
|
+
return { ok: res.ok, status: res.status, json, text: txt }
|
|
19
|
+
} finally { clearTimeout(t) }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function requireEnv(name) {
|
|
23
|
+
const v = process.env[name]
|
|
24
|
+
if (!v) throw new Error('missing env: ' + name)
|
|
25
|
+
return v
|
|
26
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { BaseEnvironment, fetchJson, requireEnv } from './base.js'
|
|
2
|
+
|
|
3
|
+
export class DaytonaEnvironment extends BaseEnvironment {
|
|
4
|
+
constructor(opts = {}) {
|
|
5
|
+
super(opts)
|
|
6
|
+
this.name = 'daytona'
|
|
7
|
+
this.apiUrl = opts.apiUrl || process.env.DAYTONA_API_URL || 'https://app.daytona.io/api'
|
|
8
|
+
this.apiKey = opts.apiKey || process.env.DAYTONA_API_KEY
|
|
9
|
+
this.target = opts.target || process.env.DAYTONA_TARGET || 'us'
|
|
10
|
+
this.workspaceId = opts.workspaceId || null
|
|
11
|
+
this.cwd = opts.cwd || '/workspace'
|
|
12
|
+
}
|
|
13
|
+
headers() { return { authorization: 'Bearer ' + (this.apiKey || requireEnv('DAYTONA_API_KEY')) } }
|
|
14
|
+
async ensureWorkspace() {
|
|
15
|
+
if (this.workspaceId) return this.workspaceId
|
|
16
|
+
const r = await fetchJson(this.apiUrl + '/workspace', { method: 'POST', headers: this.headers(), body: { target: this.target, image: this.opts.image || 'ubuntu:22.04' } })
|
|
17
|
+
if (!r.ok) throw new Error('daytona create: ' + r.status + ' ' + r.text)
|
|
18
|
+
this.workspaceId = r.json.id
|
|
19
|
+
return this.workspaceId
|
|
20
|
+
}
|
|
21
|
+
async run(cmd, { timeoutMs = 120000 } = {}) {
|
|
22
|
+
try {
|
|
23
|
+
const id = await this.ensureWorkspace()
|
|
24
|
+
const r = await fetchJson(this.apiUrl + '/workspace/' + id + '/exec', { method: 'POST', headers: this.headers(), body: { command: cmd, cwd: this.cwd }, timeoutMs })
|
|
25
|
+
return { exitCode: r.json?.exit_code ?? (r.ok ? 0 : -1), stdout: r.json?.stdout || '', stderr: r.json?.stderr || (!r.ok ? r.text : '') }
|
|
26
|
+
} catch (e) { return { exitCode: -1, stdout: '', stderr: e.message } }
|
|
27
|
+
}
|
|
28
|
+
async put(localPath, remotePath) {
|
|
29
|
+
const fs = await import('node:fs')
|
|
30
|
+
const id = await this.ensureWorkspace()
|
|
31
|
+
const buf = fs.readFileSync(localPath)
|
|
32
|
+
const r = await fetchJson(this.apiUrl + '/workspace/' + id + '/files', { method: 'POST', headers: this.headers(), body: { path: remotePath, content: buf.toString('base64'), encoding: 'base64' } })
|
|
33
|
+
return { copied: remotePath, ok: r.ok }
|
|
34
|
+
}
|
|
35
|
+
async get(remotePath, localPath) {
|
|
36
|
+
const fs = await import('node:fs')
|
|
37
|
+
const id = await this.ensureWorkspace()
|
|
38
|
+
const r = await fetchJson(this.apiUrl + '/workspace/' + id + '/files?path=' + encodeURIComponent(remotePath), { headers: this.headers() })
|
|
39
|
+
if (!r.ok) return { error: r.text }
|
|
40
|
+
fs.writeFileSync(localPath, Buffer.from(r.json?.content || '', 'base64'))
|
|
41
|
+
return { copied: localPath }
|
|
42
|
+
}
|
|
43
|
+
async shutdown() {
|
|
44
|
+
if (!this.workspaceId) return
|
|
45
|
+
await fetchJson(this.apiUrl + '/workspace/' + this.workspaceId, { method: 'DELETE', headers: this.headers() })
|
|
46
|
+
this.workspaceId = null
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class DockerEnvironment {
|
|
2
|
+
constructor(opts = {}) { this.image = opts.image || 'ubuntu:latest'; this.name = 'docker' }
|
|
3
|
+
async run(_cmd) { throw new Error('DockerEnvironment: install dockerode and replace this method') }
|
|
4
|
+
async put() { throw new Error('DockerEnvironment: install dockerode') }
|
|
5
|
+
async get() { throw new Error('DockerEnvironment: install dockerode') }
|
|
6
|
+
async shutdown() {}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let _dockerodeAvailable = null
|
|
10
|
+
export async function probeDockerode() {
|
|
11
|
+
if (_dockerodeAvailable !== null) return _dockerodeAvailable
|
|
12
|
+
try { await import('dockerode'); _dockerodeAvailable = true } catch { _dockerodeAvailable = false }
|
|
13
|
+
return _dockerodeAvailable
|
|
14
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
function sha256(buf) { return crypto.createHash('sha256').update(buf).digest('hex') }
|
|
6
|
+
|
|
7
|
+
function walk(root) {
|
|
8
|
+
const out = []
|
|
9
|
+
function rec(dir, rel) {
|
|
10
|
+
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
11
|
+
const abs = path.join(dir, ent.name)
|
|
12
|
+
const r = path.posix.join(rel, ent.name)
|
|
13
|
+
if (ent.isDirectory()) {
|
|
14
|
+
if (ent.name === '.git' || ent.name === 'node_modules') continue
|
|
15
|
+
rec(abs, r)
|
|
16
|
+
} else if (ent.isFile()) {
|
|
17
|
+
const buf = fs.readFileSync(abs)
|
|
18
|
+
out.push({ rel: r, abs, hash: sha256(buf), size: buf.length })
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
rec(root, '')
|
|
23
|
+
return out
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function syncTo(env, localRoot, remoteRoot, { onProgress } = {}) {
|
|
27
|
+
const files = walk(localRoot)
|
|
28
|
+
let transferred = 0
|
|
29
|
+
for (const f of files) {
|
|
30
|
+
const target = path.posix.join(remoteRoot, f.rel)
|
|
31
|
+
const r = await env.put(f.abs, target)
|
|
32
|
+
transferred++
|
|
33
|
+
if (onProgress) onProgress({ rel: f.rel, transferred, total: files.length, status: r.error ? 'error' : 'ok', error: r.error })
|
|
34
|
+
}
|
|
35
|
+
return { files: files.length, transferred }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function syncFrom(env, remoteRoot, localRoot, manifest = []) {
|
|
39
|
+
fs.mkdirSync(localRoot, { recursive: true })
|
|
40
|
+
let transferred = 0
|
|
41
|
+
for (const rel of manifest) {
|
|
42
|
+
const remote = path.posix.join(remoteRoot, rel)
|
|
43
|
+
const local = path.join(localRoot, rel)
|
|
44
|
+
fs.mkdirSync(path.dirname(local), { recursive: true })
|
|
45
|
+
const r = await env.get(remote, local)
|
|
46
|
+
if (!r.error) transferred++
|
|
47
|
+
}
|
|
48
|
+
return { transferred, total: manifest.length }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function diffManifest(localRoot, remoteManifest = []) {
|
|
52
|
+
const local = walk(localRoot)
|
|
53
|
+
const localByRel = new Map(local.map(f => [f.rel, f]))
|
|
54
|
+
const remoteByRel = new Map(remoteManifest.map(f => [f.rel, f]))
|
|
55
|
+
const toUpload = local.filter(f => remoteByRel.get(f.rel)?.hash !== f.hash).map(f => f.rel)
|
|
56
|
+
const toDelete = remoteManifest.filter(f => !localByRel.has(f.rel)).map(f => f.rel)
|
|
57
|
+
return { toUpload, toDelete }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { walk, sha256 }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { LocalEnvironment } from './local.js'
|
|
2
|
+
import { DockerEnvironment } from './docker.js'
|
|
3
|
+
import { SshEnvironment } from './ssh.js'
|
|
4
|
+
import { ModalEnvironment, ManagedModalEnvironment } from './modal.js'
|
|
5
|
+
import { DaytonaEnvironment } from './daytona.js'
|
|
6
|
+
import { SingularityEnvironment } from './singularity.js'
|
|
7
|
+
import { VercelSandboxEnvironment } from './vercel_sandbox.js'
|
|
8
|
+
import { BaseEnvironment } from './base.js'
|
|
9
|
+
import { syncTo, syncFrom, diffManifest } from './file_sync.js'
|
|
10
|
+
import { getConfigValue } from '../../config.js'
|
|
11
|
+
|
|
12
|
+
const FACTORIES = {
|
|
13
|
+
local: (opts) => new LocalEnvironment(opts),
|
|
14
|
+
docker: (opts) => new DockerEnvironment(opts),
|
|
15
|
+
ssh: (opts) => new SshEnvironment(opts),
|
|
16
|
+
modal: (opts) => new ModalEnvironment(opts),
|
|
17
|
+
managed_modal: (opts) => new ManagedModalEnvironment(opts),
|
|
18
|
+
daytona: (opts) => new DaytonaEnvironment(opts),
|
|
19
|
+
singularity: (opts) => new SingularityEnvironment(opts),
|
|
20
|
+
vercel_sandbox: (opts) => new VercelSandboxEnvironment(opts),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function listEnvironments() { return Object.keys(FACTORIES) }
|
|
24
|
+
|
|
25
|
+
export function createEnvironment(name, opts = {}) {
|
|
26
|
+
const f = FACTORIES[name]
|
|
27
|
+
if (!f) throw new Error('unknown environment: ' + name + ' (available: ' + Object.keys(FACTORIES).join(', ') + ')')
|
|
28
|
+
return f(opts)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function defaultEnvironment(opts = {}) {
|
|
32
|
+
const name = getConfigValue('terminal.environment', 'local')
|
|
33
|
+
return createEnvironment(name, opts)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { LocalEnvironment, DockerEnvironment, SshEnvironment, ModalEnvironment, ManagedModalEnvironment, DaytonaEnvironment, SingularityEnvironment, VercelSandboxEnvironment, BaseEnvironment, syncTo, syncFrom, diffManifest }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
export class LocalEnvironment {
|
|
6
|
+
constructor(opts = {}) { this.cwd = opts.cwd || process.cwd(); this.name = 'local' }
|
|
7
|
+
async run(cmd, { timeoutMs = 60000 } = {}) {
|
|
8
|
+
return new Promise(resolve => {
|
|
9
|
+
const sh = process.platform === 'win32' ? 'cmd' : 'sh'
|
|
10
|
+
const flag = process.platform === 'win32' ? '/c' : '-c'
|
|
11
|
+
const child = spawn(sh, [flag, cmd], { cwd: this.cwd, env: process.env })
|
|
12
|
+
let stdout = '', stderr = ''
|
|
13
|
+
const t = setTimeout(() => { try { child.kill('SIGKILL') } catch {} resolve({ exitCode: -1, stdout, stderr: stderr + '\n[timeout]' }) }, timeoutMs)
|
|
14
|
+
child.stdout?.on('data', d => stdout += d.toString())
|
|
15
|
+
child.stderr?.on('data', d => stderr += d.toString())
|
|
16
|
+
child.on('close', code => { clearTimeout(t); resolve({ exitCode: code, stdout, stderr }) })
|
|
17
|
+
child.on('error', e => { clearTimeout(t); resolve({ exitCode: -1, stdout, stderr: stderr + '\n' + e.message }) })
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
async put(localPath, remotePath) {
|
|
21
|
+
fs.mkdirSync(path.dirname(remotePath), { recursive: true })
|
|
22
|
+
fs.copyFileSync(localPath, remotePath)
|
|
23
|
+
return { copied: remotePath }
|
|
24
|
+
}
|
|
25
|
+
async get(remotePath, localPath) {
|
|
26
|
+
fs.mkdirSync(path.dirname(localPath), { recursive: true })
|
|
27
|
+
fs.copyFileSync(remotePath, localPath)
|
|
28
|
+
return { copied: localPath }
|
|
29
|
+
}
|
|
30
|
+
async shutdown() {}
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { BaseEnvironment } from './base.js'
|
|
3
|
+
|
|
4
|
+
export class ModalEnvironment extends BaseEnvironment {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
super(opts)
|
|
7
|
+
this.name = 'modal'
|
|
8
|
+
this.app = opts.app || 'freddie-sandbox'
|
|
9
|
+
this.image = opts.image || 'python:3.11'
|
|
10
|
+
this.cwd = opts.cwd || '/sandbox'
|
|
11
|
+
this.token = process.env.MODAL_TOKEN_ID
|
|
12
|
+
this.secret = process.env.MODAL_TOKEN_SECRET
|
|
13
|
+
}
|
|
14
|
+
async run(cmd, { timeoutMs = 120000 } = {}) {
|
|
15
|
+
if (!this.token) return { exitCode: -1, stdout: '', stderr: 'MODAL_TOKEN_ID required' }
|
|
16
|
+
return new Promise(resolve => {
|
|
17
|
+
const env = { ...process.env, MODAL_TOKEN_ID: this.token, MODAL_TOKEN_SECRET: this.secret }
|
|
18
|
+
const child = spawn('modal', ['run', '--detach=false', '-q', '-', cmd], { env, shell: process.platform === 'win32' })
|
|
19
|
+
let stdout = '', stderr = ''
|
|
20
|
+
const t = setTimeout(() => { try { child.kill('SIGKILL') } catch {} resolve({ exitCode: -1, stdout, stderr: stderr + '\n[timeout]' }) }, timeoutMs)
|
|
21
|
+
child.stdout?.on('data', d => stdout += d.toString())
|
|
22
|
+
child.stderr?.on('data', d => stderr += d.toString())
|
|
23
|
+
child.on('close', code => { clearTimeout(t); resolve({ exitCode: code, stdout, stderr }) })
|
|
24
|
+
child.on('error', e => { clearTimeout(t); resolve({ exitCode: -1, stdout, stderr: stderr + '\n' + e.message }) })
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
async put(localPath, remotePath) { return { error: 'modal put requires modal volume put: ' + localPath + ' -> ' + remotePath } }
|
|
28
|
+
async get(remotePath, localPath) { return { error: 'modal get requires modal volume get: ' + remotePath + ' -> ' + localPath } }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class ManagedModalEnvironment extends ModalEnvironment {
|
|
32
|
+
constructor(opts = {}) { super({ ...opts, app: opts.app || 'freddie-managed' }); this.name = 'managed_modal' }
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { BaseEnvironment } from './base.js'
|
|
3
|
+
|
|
4
|
+
export class SingularityEnvironment extends BaseEnvironment {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
super(opts)
|
|
7
|
+
this.name = 'singularity'
|
|
8
|
+
this.image = opts.image || 'docker://ubuntu:22.04'
|
|
9
|
+
this.binary = opts.binary || process.env.SINGULARITY_BIN || 'singularity'
|
|
10
|
+
this.binds = opts.binds || []
|
|
11
|
+
this.cwd = opts.cwd || '/workspace'
|
|
12
|
+
}
|
|
13
|
+
_bindArgs() {
|
|
14
|
+
const args = []
|
|
15
|
+
for (const b of this.binds) args.push('--bind', b)
|
|
16
|
+
return args
|
|
17
|
+
}
|
|
18
|
+
async run(cmd, { timeoutMs = 120000 } = {}) {
|
|
19
|
+
return new Promise(resolve => {
|
|
20
|
+
const args = ['exec', '--pwd', this.cwd, ...this._bindArgs(), this.image, 'sh', '-c', cmd]
|
|
21
|
+
const child = spawn(this.binary, args, { env: process.env })
|
|
22
|
+
let stdout = '', stderr = ''
|
|
23
|
+
const t = setTimeout(() => { try { child.kill('SIGKILL') } catch {} resolve({ exitCode: -1, stdout, stderr: stderr + '\n[timeout]' }) }, timeoutMs)
|
|
24
|
+
child.stdout?.on('data', d => stdout += d.toString())
|
|
25
|
+
child.stderr?.on('data', d => stderr += d.toString())
|
|
26
|
+
child.on('close', code => { clearTimeout(t); resolve({ exitCode: code, stdout, stderr }) })
|
|
27
|
+
child.on('error', e => { clearTimeout(t); resolve({ exitCode: -1, stdout, stderr: stderr + '\n' + e.message }) })
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
async put(localPath, remotePath) {
|
|
31
|
+
const r = await this.run('mkdir -p "$(dirname \'' + remotePath + '\')" && cp "' + localPath + '" "' + remotePath + '"')
|
|
32
|
+
return r.exitCode === 0 ? { copied: remotePath } : { error: r.stderr }
|
|
33
|
+
}
|
|
34
|
+
async get(remotePath, localPath) {
|
|
35
|
+
const r = await this.run('cp "' + remotePath + '" "' + localPath + '"')
|
|
36
|
+
return r.exitCode === 0 ? { copied: localPath } : { error: r.stderr }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class SshEnvironment {
|
|
2
|
+
constructor(opts = {}) { this.host = opts.host; this.user = opts.user; this.name = 'ssh' }
|
|
3
|
+
async run(_cmd) { throw new Error('SshEnvironment: install ssh2 and replace this method') }
|
|
4
|
+
async put() { throw new Error('SshEnvironment: install ssh2') }
|
|
5
|
+
async get() { throw new Error('SshEnvironment: install ssh2') }
|
|
6
|
+
async shutdown() {}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let _ssh2Available = null
|
|
10
|
+
export async function probeSsh2() {
|
|
11
|
+
if (_ssh2Available !== null) return _ssh2Available
|
|
12
|
+
try { await import('ssh2'); _ssh2Available = true } catch { _ssh2Available = false }
|
|
13
|
+
return _ssh2Available
|
|
14
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { BaseEnvironment, fetchJson, requireEnv } from './base.js'
|
|
2
|
+
|
|
3
|
+
export class VercelSandboxEnvironment extends BaseEnvironment {
|
|
4
|
+
constructor(opts = {}) {
|
|
5
|
+
super(opts)
|
|
6
|
+
this.name = 'vercel_sandbox'
|
|
7
|
+
this.apiUrl = opts.apiUrl || process.env.VERCEL_SANDBOX_URL || 'https://api.vercel.com/v1/sandbox'
|
|
8
|
+
this.token = opts.token || process.env.VERCEL_TOKEN
|
|
9
|
+
this.runtime = opts.runtime || 'node22'
|
|
10
|
+
this.sandboxId = null
|
|
11
|
+
this.cwd = opts.cwd || '/vercel/sandbox'
|
|
12
|
+
}
|
|
13
|
+
headers() { return { authorization: 'Bearer ' + (this.token || requireEnv('VERCEL_TOKEN')) } }
|
|
14
|
+
async ensureSandbox() {
|
|
15
|
+
if (this.sandboxId) return this.sandboxId
|
|
16
|
+
const r = await fetchJson(this.apiUrl, { method: 'POST', headers: this.headers(), body: { runtime: this.runtime, timeout: this.opts.timeoutSec || 600 } })
|
|
17
|
+
if (!r.ok) throw new Error('vercel sandbox create: ' + r.status + ' ' + r.text)
|
|
18
|
+
this.sandboxId = r.json.id
|
|
19
|
+
return this.sandboxId
|
|
20
|
+
}
|
|
21
|
+
async run(cmd, { timeoutMs = 120000 } = {}) {
|
|
22
|
+
try {
|
|
23
|
+
const id = await this.ensureSandbox()
|
|
24
|
+
const r = await fetchJson(this.apiUrl + '/' + id + '/exec', { method: 'POST', headers: this.headers(), body: { cmd: ['sh', '-c', cmd], cwd: this.cwd }, timeoutMs })
|
|
25
|
+
return { exitCode: r.json?.exitCode ?? (r.ok ? 0 : -1), stdout: r.json?.stdout || '', stderr: r.json?.stderr || (!r.ok ? r.text : '') }
|
|
26
|
+
} catch (e) { return { exitCode: -1, stdout: '', stderr: e.message } }
|
|
27
|
+
}
|
|
28
|
+
async put(localPath, remotePath) {
|
|
29
|
+
const fs = await import('node:fs')
|
|
30
|
+
const id = await this.ensureSandbox()
|
|
31
|
+
const r = await fetchJson(this.apiUrl + '/' + id + '/files', { method: 'PUT', headers: this.headers(), body: { path: remotePath, content: fs.readFileSync(localPath).toString('base64'), encoding: 'base64' } })
|
|
32
|
+
return r.ok ? { copied: remotePath } : { error: r.text }
|
|
33
|
+
}
|
|
34
|
+
async get(remotePath, localPath) {
|
|
35
|
+
const fs = await import('node:fs')
|
|
36
|
+
const id = await this.ensureSandbox()
|
|
37
|
+
const r = await fetchJson(this.apiUrl + '/' + id + '/files?path=' + encodeURIComponent(remotePath), { headers: this.headers() })
|
|
38
|
+
if (!r.ok) return { error: r.text }
|
|
39
|
+
fs.writeFileSync(localPath, Buffer.from(r.json?.content || '', 'base64'))
|
|
40
|
+
return { copied: localPath }
|
|
41
|
+
}
|
|
42
|
+
async shutdown() {
|
|
43
|
+
if (!this.sandboxId) return
|
|
44
|
+
await fetchJson(this.apiUrl + '/' + this.sandboxId, { method: 'DELETE', headers: this.headers() })
|
|
45
|
+
this.sandboxId = null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
registry.register({
|
|
3
|
+
name: 'feishu_doc',
|
|
4
|
+
toolset: 'core',
|
|
5
|
+
schema: { name: 'feishu_doc', description: 'Read or update a Feishu doc by token.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['get', 'patch'] }, doc_token: { type: 'string' }, content: {} }, required: ['action', 'doc_token'] } },
|
|
6
|
+
requiresEnv: ['FEISHU_APP_TOKEN'],
|
|
7
|
+
checkFn: () => Boolean(process.env.FEISHU_APP_TOKEN),
|
|
8
|
+
handler: async ({ action, doc_token, content }) => {
|
|
9
|
+
const auth = { authorization: `Bearer ${process.env.FEISHU_APP_TOKEN}` }
|
|
10
|
+
const base = 'https://open.feishu.cn/open-apis/docx/v1/documents/' + doc_token
|
|
11
|
+
if (action === 'get') return await (await fetch(base, { headers: auth })).json()
|
|
12
|
+
if (action === 'patch') return await (await fetch(base + '/blocks', { method: 'PATCH', headers: { ...auth, 'content-type': 'application/json' }, body: JSON.stringify(content) })).json()
|
|
13
|
+
return { error: 'unknown action' }
|
|
14
|
+
},
|
|
15
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
registry.register({
|
|
3
|
+
name: 'feishu_drive',
|
|
4
|
+
toolset: 'core',
|
|
5
|
+
schema: { name: 'feishu_drive', description: 'List or download Feishu Drive files.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'download'] }, folder_token: { type: 'string' }, file_token: { type: 'string' } }, required: ['action'] } },
|
|
6
|
+
requiresEnv: ['FEISHU_APP_TOKEN'],
|
|
7
|
+
checkFn: () => Boolean(process.env.FEISHU_APP_TOKEN),
|
|
8
|
+
handler: async ({ action, folder_token, file_token }) => {
|
|
9
|
+
const auth = { authorization: `Bearer ${process.env.FEISHU_APP_TOKEN}` }
|
|
10
|
+
if (action === 'list') return await (await fetch('https://open.feishu.cn/open-apis/drive/v1/files?folder_token=' + (folder_token || ''), { headers: auth })).json()
|
|
11
|
+
if (action === 'download') return await (await fetch(`https://open.feishu.cn/open-apis/drive/v1/files/${file_token}/download`, { headers: auth })).json()
|
|
12
|
+
return { error: 'unknown action' }
|
|
13
|
+
},
|
|
14
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { registry } from './registry.js'
|
|
4
|
+
|
|
5
|
+
const ACTIONS = {
|
|
6
|
+
move: ({ src, dest }) => { fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.renameSync(src, dest); return { moved: dest } },
|
|
7
|
+
copy: ({ src, dest }) => { fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.copyFileSync(src, dest); return { copied: dest } },
|
|
8
|
+
delete: ({ path: p }) => { if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true }); return { deleted: p } },
|
|
9
|
+
mkdir: ({ path: p }) => { fs.mkdirSync(p, { recursive: true }); return { created: p } },
|
|
10
|
+
stat: ({ path: p }) => { const s = fs.statSync(p); return { size: s.size, mtime: s.mtimeMs, isDir: s.isDirectory() } },
|
|
11
|
+
}
|
|
12
|
+
registry.register({
|
|
13
|
+
name: 'file_operations',
|
|
14
|
+
toolset: 'core',
|
|
15
|
+
schema: { name: 'file_operations', description: 'move/copy/delete/mkdir/stat — atomic file ops.', parameters: { type: 'object', properties: { action: { type: 'string', enum: Object.keys(ACTIONS) }, src: { type: 'string' }, dest: { type: 'string' }, path: { type: 'string' } }, required: ['action'] } },
|
|
16
|
+
handler: async (a) => { const fn = ACTIONS[a.action]; try { return fn ? fn(a) : { error: 'unknown action' } } catch (e) { return { error: String(e.message || e) } } },
|
|
17
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { db } from '../db.js'
|
|
2
|
+
import { registry } from './registry.js'
|
|
3
|
+
|
|
4
|
+
async function init() { const d = await db(); d.exec(`CREATE TABLE IF NOT EXISTS file_state (id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, file_path TEXT NOT NULL, action TEXT NOT NULL, ts INTEGER NOT NULL)`); return d }
|
|
5
|
+
registry.register({
|
|
6
|
+
name: 'file_state',
|
|
7
|
+
toolset: 'core',
|
|
8
|
+
schema: { name: 'file_state', description: 'Track files modified in this session (read|write|edit|delete) for diff-summary purposes.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['record', 'list', 'changed_in_session'] }, session_id: { type: 'string' }, file_path: { type: 'string' }, op: { type: 'string' } }, required: ['action'] } },
|
|
9
|
+
handler: async ({ action, session_id, file_path, op }) => {
|
|
10
|
+
const d = await init()
|
|
11
|
+
if (action === 'record') { d.prepare('INSERT INTO file_state (session_id, file_path, action, ts) VALUES (?, ?, ?, ?)').run(session_id, file_path, op, Date.now()); return { recorded: true } }
|
|
12
|
+
if (action === 'list') return { items: d.prepare('SELECT * FROM file_state WHERE session_id = ? ORDER BY id DESC').all(session_id) }
|
|
13
|
+
if (action === 'changed_in_session') return { files: [...new Set(d.prepare('SELECT file_path FROM file_state WHERE session_id = ?').all(session_id).map(r => r.file_path))] }
|
|
14
|
+
return { error: 'unknown action' }
|
|
15
|
+
},
|
|
16
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { registry } from './registry.js'
|
|
4
|
+
|
|
5
|
+
function walk(dir, out, skip) {
|
|
6
|
+
let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
7
|
+
for (const e of entries) {
|
|
8
|
+
if (skip.has(e.name)) continue
|
|
9
|
+
const full = path.join(dir, e.name)
|
|
10
|
+
if (e.isDirectory()) walk(full, out, skip)
|
|
11
|
+
else out.push(full)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
registry.register({
|
|
15
|
+
name: 'file_tools',
|
|
16
|
+
toolset: 'core',
|
|
17
|
+
schema: { name: 'file_tools', description: 'list/glob files (recursive walk skipping node_modules, .git, dist).', parameters: { type: 'object', properties: { dir: { type: 'string', default: '.' }, ext: { type: 'string' }, limit: { type: 'number', default: 1000 } } } },
|
|
18
|
+
handler: async ({ dir = '.', ext, limit = 1000 }) => {
|
|
19
|
+
const out = []; walk(dir, out, new Set(['node_modules', '.git', 'dist', '.cache', 'build']))
|
|
20
|
+
const filtered = ext ? out.filter(f => f.endsWith(ext)) : out
|
|
21
|
+
return { files: filtered.slice(0, limit), total: filtered.length, truncated: filtered.length > limit }
|
|
22
|
+
},
|
|
23
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { fuzzyMatch as helper } from '../utils.js'
|
|
2
|
+
import { registry } from './registry.js'
|
|
3
|
+
registry.register({
|
|
4
|
+
name: 'fuzzy_match',
|
|
5
|
+
toolset: 'core',
|
|
6
|
+
schema: { name: 'fuzzy_match', description: 'Score a candidate string against a needle. Returns 0 for no match, higher for better match.', parameters: { type: 'object', properties: { needle: { type: 'string' }, haystack: { type: 'string' } }, required: ['needle', 'haystack'] } },
|
|
7
|
+
handler: async ({ needle, haystack }) => ({ score: helper(needle, haystack) }),
|
|
8
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { registry } from './registry.js'
|
|
4
|
+
|
|
5
|
+
registry.register({
|
|
6
|
+
name: 'grep',
|
|
7
|
+
toolset: 'core',
|
|
8
|
+
schema: {
|
|
9
|
+
name: 'grep',
|
|
10
|
+
description: 'Recursive regex search across files. Returns file:line:content matches.',
|
|
11
|
+
parameters: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
pattern: { type: 'string' },
|
|
15
|
+
path: { type: 'string', default: '.' },
|
|
16
|
+
glob: { type: 'string' },
|
|
17
|
+
head_limit: { type: 'number', default: 200 },
|
|
18
|
+
ignore_case: { type: 'boolean', default: false },
|
|
19
|
+
},
|
|
20
|
+
required: ['pattern'],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
handler: async ({ pattern, path: root = '.', head_limit = 200, ignore_case = false, glob }) => {
|
|
24
|
+
const re = new RegExp(pattern, ignore_case ? 'i' : '')
|
|
25
|
+
const out = []
|
|
26
|
+
const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', '.cache'])
|
|
27
|
+
const walk = (d) => {
|
|
28
|
+
if (out.length >= head_limit) return
|
|
29
|
+
let entries
|
|
30
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }) } catch { return }
|
|
31
|
+
for (const e of entries) {
|
|
32
|
+
if (out.length >= head_limit) return
|
|
33
|
+
const full = path.join(d, e.name)
|
|
34
|
+
if (e.isDirectory()) { if (!skipDirs.has(e.name)) walk(full); continue }
|
|
35
|
+
if (glob && !matchGlob(e.name, glob)) continue
|
|
36
|
+
let content
|
|
37
|
+
try { content = fs.readFileSync(full, 'utf8') } catch { continue }
|
|
38
|
+
content.split('\n').forEach((line, i) => {
|
|
39
|
+
if (out.length < head_limit && re.test(line)) out.push(`${full}:${i + 1}:${line.slice(0, 200)}`)
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
walk(root)
|
|
44
|
+
return { matches: out, total: out.length, truncated: out.length >= head_limit }
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
function matchGlob(name, glob) {
|
|
49
|
+
const re = new RegExp('^' + glob.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i')
|
|
50
|
+
return re.test(name)
|
|
51
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
registry.register({
|
|
3
|
+
name: 'homeassistant_tool',
|
|
4
|
+
toolset: 'core',
|
|
5
|
+
schema: { name: 'homeassistant_tool', description: 'Read state or call a service on Home Assistant.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['state', 'service'] }, entity_id: { type: 'string' }, domain: { type: 'string' }, service: { type: 'string' }, data: {} }, required: ['action'] } },
|
|
6
|
+
requiresEnv: ['HASS_TOKEN', 'HASS_URL'],
|
|
7
|
+
checkFn: () => Boolean(process.env.HASS_TOKEN),
|
|
8
|
+
handler: async ({ action, entity_id, domain, service, data = {} }) => {
|
|
9
|
+
const url = process.env.HASS_URL || 'http://homeassistant.local:8123'
|
|
10
|
+
const auth = { authorization: `Bearer ${process.env.HASS_TOKEN}` }
|
|
11
|
+
if (action === 'state') return await (await fetch(`${url}/api/states/${entity_id}`, { headers: auth })).json()
|
|
12
|
+
if (action === 'service') return await (await fetch(`${url}/api/services/${domain}/${service}`, { method: 'POST', headers: { ...auth, 'content-type': 'application/json' }, body: JSON.stringify({ entity_id, ...data }) })).json()
|
|
13
|
+
return { error: 'unknown action' }
|
|
14
|
+
},
|
|
15
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
|
|
3
|
+
registry.register({
|
|
4
|
+
name: 'image_gen',
|
|
5
|
+
toolset: 'creative',
|
|
6
|
+
schema: {
|
|
7
|
+
name: 'image_gen',
|
|
8
|
+
description: 'Generate an image from a prompt. Provider via config.image_gen.provider (openai|replicate).',
|
|
9
|
+
parameters: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
prompt: { type: 'string' },
|
|
13
|
+
provider: { type: 'string', enum: ['openai', 'replicate'] },
|
|
14
|
+
size: { type: 'string', default: '1024x1024' },
|
|
15
|
+
model: { type: 'string' },
|
|
16
|
+
},
|
|
17
|
+
required: ['prompt'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
checkFn: () => Boolean(process.env.OPENAI_API_KEY || process.env.REPLICATE_API_TOKEN),
|
|
21
|
+
requiresEnv: ['OPENAI_API_KEY or REPLICATE_API_TOKEN'],
|
|
22
|
+
handler: async ({ prompt, provider, size = '1024x1024', model }) => {
|
|
23
|
+
const which = provider || (process.env.OPENAI_API_KEY ? 'openai' : 'replicate')
|
|
24
|
+
if (which === 'openai') {
|
|
25
|
+
if (!process.env.OPENAI_API_KEY) return { error: 'OPENAI_API_KEY required' }
|
|
26
|
+
const res = await fetch('https://api.openai.com/v1/images/generations', { method: 'POST', headers: { authorization: `Bearer ${process.env.OPENAI_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ model: model || 'gpt-image-1', prompt, size }) })
|
|
27
|
+
return await res.json()
|
|
28
|
+
}
|
|
29
|
+
if (!process.env.REPLICATE_API_TOKEN) return { error: 'REPLICATE_API_TOKEN required' }
|
|
30
|
+
const res = await fetch('https://api.replicate.com/v1/predictions', { method: 'POST', headers: { authorization: `Token ${process.env.REPLICATE_API_TOKEN}`, 'content-type': 'application/json' }, body: JSON.stringify({ version: model || 'black-forest-labs/flux-schnell', input: { prompt } }) })
|
|
31
|
+
return await res.json()
|
|
32
|
+
},
|
|
33
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { registry } from './registry.js'
|
|
2
|
+
|
|
3
|
+
const _flags = new Map()
|
|
4
|
+
export function setInterrupt(sessionId) { _flags.set(sessionId, true) }
|
|
5
|
+
export function isInterrupted(sessionId) { return _flags.get(sessionId) === true }
|
|
6
|
+
export function clearInterrupt(sessionId) { _flags.delete(sessionId) }
|
|
7
|
+
|
|
8
|
+
registry.register({
|
|
9
|
+
name: 'interrupt',
|
|
10
|
+
toolset: 'core',
|
|
11
|
+
schema: { name: 'interrupt', description: 'Set/clear/check interrupt flag for a session — agent loop polls and exits early.', parameters: { type: 'object', properties: { action: { type: 'string', enum: ['set', 'clear', 'check'] }, session_id: { type: 'string' } }, required: ['action', 'session_id'] } },
|
|
12
|
+
handler: async ({ action, session_id }) => {
|
|
13
|
+
if (action === 'set') { setInterrupt(session_id); return { interrupted: true } }
|
|
14
|
+
if (action === 'clear') { clearInterrupt(session_id); return { cleared: true } }
|
|
15
|
+
if (action === 'check') return { interrupted: isInterrupted(session_id) }
|
|
16
|
+
return { error: 'unknown action' }
|
|
17
|
+
},
|
|
18
|
+
})
|