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 express from 'express'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
export class MattermostAdapter extends EventEmitter {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
super()
|
|
7
|
+
this.platform = 'mattermost'
|
|
8
|
+
this.url = opts.url || process.env.MATTERMOST_URL
|
|
9
|
+
this.token = opts.token || process.env.MATTERMOST_TOKEN
|
|
10
|
+
this.port = opts.port || 0
|
|
11
|
+
this._server = null
|
|
12
|
+
}
|
|
13
|
+
getRequiredEnv() { return ['MATTERMOST_URL', 'MATTERMOST_TOKEN'] }
|
|
14
|
+
async start() {
|
|
15
|
+
if (!this.url || !this.token) throw new Error('MattermostAdapter: MATTERMOST_URL + MATTERMOST_TOKEN required')
|
|
16
|
+
const app = express()
|
|
17
|
+
app.use(express.urlencoded({ extended: true }))
|
|
18
|
+
app.post('/hook', (req, res) => {
|
|
19
|
+
this.emit('message', { from: req.body.channel_id, text: req.body.text || '', user: req.body.user_id, raw: req.body })
|
|
20
|
+
res.json({})
|
|
21
|
+
})
|
|
22
|
+
await new Promise(r => { this._server = app.listen(this.port, () => r()) })
|
|
23
|
+
this.port = this._server.address().port
|
|
24
|
+
}
|
|
25
|
+
async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
|
|
26
|
+
async send(reply) {
|
|
27
|
+
return fetch(`${this.url}/api/v4/posts`, { method: 'POST', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ channel_id: reply.to, message: reply.text }) }).then(r => r.json())
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
export class QqbotAdapter extends EventEmitter {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
super()
|
|
7
|
+
this.platform = 'qqbot'
|
|
8
|
+
this.token = opts.token || process.env.QQBOT_TOKEN
|
|
9
|
+
this.port = opts.port || 0
|
|
10
|
+
this.api = opts.api || "https://api.sgroup.qq.com/channels/messages"
|
|
11
|
+
this._server = null
|
|
12
|
+
}
|
|
13
|
+
getRequiredEnv() { return ["QQBOT_TOKEN"] }
|
|
14
|
+
async start() {
|
|
15
|
+
if (!this.token) throw new Error('QqbotAdapter: ' + this.getRequiredEnv().join(', ') + ' required')
|
|
16
|
+
const app = express()
|
|
17
|
+
app.use(express.json())
|
|
18
|
+
app.post('/webhook', (req, res) => {
|
|
19
|
+
const text = req.body?.text || req.body?.message?.text || req.body?.content || ''
|
|
20
|
+
const from = req.body?.from || req.body?.user_id || req.body?.sender_id || ''
|
|
21
|
+
this.emit('message', { from: String(from), text, raw: req.body })
|
|
22
|
+
res.json({ ok: true })
|
|
23
|
+
})
|
|
24
|
+
await new Promise(r => { this._server = app.listen(this.port, () => r()) })
|
|
25
|
+
this.port = this._server.address().port
|
|
26
|
+
}
|
|
27
|
+
async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
|
|
28
|
+
async send(reply) {
|
|
29
|
+
if (!this.token) throw new Error('QqbotAdapter: token required')
|
|
30
|
+
return fetch(this.api, { method: 'POST', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ to: reply.to, text: reply.text }) }).then(r => r.json())
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
|
|
3
|
+
export class SignalAdapter extends EventEmitter {
|
|
4
|
+
constructor(opts = {}) {
|
|
5
|
+
super()
|
|
6
|
+
this.platform = 'signal'
|
|
7
|
+
this.api = opts.api || process.env.SIGNAL_CLI_URL || 'http://127.0.0.1:8080'
|
|
8
|
+
this.number = opts.number || process.env.SIGNAL_NUMBER
|
|
9
|
+
this._running = false
|
|
10
|
+
}
|
|
11
|
+
getRequiredEnv() { return ['SIGNAL_CLI_URL', 'SIGNAL_NUMBER'] }
|
|
12
|
+
async start() {
|
|
13
|
+
if (!this.number) throw new Error('SignalAdapter: SIGNAL_NUMBER required')
|
|
14
|
+
this._running = true
|
|
15
|
+
this._loop()
|
|
16
|
+
}
|
|
17
|
+
async stop() { this._running = false }
|
|
18
|
+
async _loop() {
|
|
19
|
+
while (this._running) {
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(`${this.api}/v1/receive/${this.number}`)
|
|
22
|
+
const items = await res.json()
|
|
23
|
+
for (const it of items) {
|
|
24
|
+
const msg = it?.envelope?.dataMessage
|
|
25
|
+
if (msg) this.emit('message', { from: it.envelope.source, text: msg.message || '', raw: it })
|
|
26
|
+
}
|
|
27
|
+
} catch (e) { await new Promise(r => setTimeout(r, 2000)) }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async send(reply) {
|
|
31
|
+
return fetch(`${this.api}/v2/send`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ number: this.number, recipients: [reply.to], message: reply.text }) }).then(r => r.json())
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
export class SlackAdapter extends EventEmitter {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
super()
|
|
7
|
+
this.platform = 'slack'
|
|
8
|
+
this.token = opts.token || process.env.SLACK_BOT_TOKEN
|
|
9
|
+
this.signingSecret = opts.signingSecret || process.env.SLACK_SIGNING_SECRET
|
|
10
|
+
this.port = opts.port || 0
|
|
11
|
+
this.path = opts.path || '/slack/events'
|
|
12
|
+
this.api = opts.api || 'https://slack.com/api'
|
|
13
|
+
this._server = null
|
|
14
|
+
}
|
|
15
|
+
getRequiredEnv() { return ['SLACK_BOT_TOKEN', 'SLACK_SIGNING_SECRET'] }
|
|
16
|
+
async start() {
|
|
17
|
+
if (!this.token) throw new Error('SlackAdapter: SLACK_BOT_TOKEN required')
|
|
18
|
+
const app = express()
|
|
19
|
+
app.use(express.json())
|
|
20
|
+
app.post(this.path, (req, res) => {
|
|
21
|
+
if (req.body?.type === 'url_verification') return res.json({ challenge: req.body.challenge })
|
|
22
|
+
const ev = req.body?.event
|
|
23
|
+
if (ev?.type === 'message' && !ev.bot_id) this.emit('message', { from: ev.channel, text: ev.text || '', user: ev.user, raw: req.body })
|
|
24
|
+
res.json({ ok: true })
|
|
25
|
+
})
|
|
26
|
+
await new Promise(r => { this._server = app.listen(this.port, () => r()) })
|
|
27
|
+
this.port = this._server.address().port
|
|
28
|
+
}
|
|
29
|
+
async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
|
|
30
|
+
async send(reply) {
|
|
31
|
+
if (!this.token) throw new Error('SlackAdapter: token required')
|
|
32
|
+
return fetch(`${this.api}/chat.postMessage`, { method: 'POST', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ channel: reply.to, text: reply.text }) }).then(r => r.json())
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
export class SmsAdapter extends EventEmitter {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
super()
|
|
7
|
+
this.platform = 'sms'
|
|
8
|
+
this.sid = opts.sid || process.env.TWILIO_SID
|
|
9
|
+
this.token = opts.token || process.env.TWILIO_TOKEN
|
|
10
|
+
this.from = opts.from || process.env.TWILIO_FROM
|
|
11
|
+
this.port = opts.port || 0
|
|
12
|
+
this._server = null
|
|
13
|
+
}
|
|
14
|
+
getRequiredEnv() { return ['TWILIO_SID', 'TWILIO_TOKEN', 'TWILIO_FROM'] }
|
|
15
|
+
async start() {
|
|
16
|
+
if (!this.sid || !this.token || !this.from) throw new Error('SmsAdapter: TWILIO_SID/TOKEN/FROM required')
|
|
17
|
+
const app = express()
|
|
18
|
+
app.use(express.urlencoded({ extended: true }))
|
|
19
|
+
app.post('/sms', (req, res) => {
|
|
20
|
+
this.emit('message', { from: req.body.From, text: req.body.Body || '', raw: req.body })
|
|
21
|
+
res.set('content-type', 'text/xml')
|
|
22
|
+
res.send('<Response/>')
|
|
23
|
+
})
|
|
24
|
+
await new Promise(r => { this._server = app.listen(this.port, () => r()) })
|
|
25
|
+
this.port = this._server.address().port
|
|
26
|
+
}
|
|
27
|
+
async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
|
|
28
|
+
async send(reply) {
|
|
29
|
+
const url = `https://api.twilio.com/2010-04-01/Accounts/${this.sid}/Messages.json`
|
|
30
|
+
const body = new URLSearchParams({ To: reply.to, From: this.from, Body: reply.text }).toString()
|
|
31
|
+
const auth = 'Basic ' + Buffer.from(`${this.sid}:${this.token}`).toString('base64')
|
|
32
|
+
return fetch(url, { method: 'POST', headers: { authorization: auth, 'content-type': 'application/x-www-form-urlencoded' }, body }).then(r => r.json())
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
|
|
3
|
+
export class TelegramAdapter extends EventEmitter {
|
|
4
|
+
constructor(opts = {}) {
|
|
5
|
+
super()
|
|
6
|
+
this.platform = 'telegram'
|
|
7
|
+
this.token = opts.token || process.env.TELEGRAM_BOT_TOKEN
|
|
8
|
+
this.api = opts.api || 'https://api.telegram.org'
|
|
9
|
+
this.offset = 0
|
|
10
|
+
this._running = false
|
|
11
|
+
this._poll = null
|
|
12
|
+
}
|
|
13
|
+
getRequiredEnv() { return ['TELEGRAM_BOT_TOKEN'] }
|
|
14
|
+
async start() {
|
|
15
|
+
if (!this.token) throw new Error('TelegramAdapter: TELEGRAM_BOT_TOKEN required')
|
|
16
|
+
this._running = true
|
|
17
|
+
this._loop()
|
|
18
|
+
}
|
|
19
|
+
async stop() { this._running = false; if (this._poll) clearTimeout(this._poll) }
|
|
20
|
+
async _loop() {
|
|
21
|
+
while (this._running) {
|
|
22
|
+
try {
|
|
23
|
+
const url = `${this.api}/bot${this.token}/getUpdates?timeout=25&offset=${this.offset + 1}`
|
|
24
|
+
const res = await fetch(url)
|
|
25
|
+
const data = await res.json()
|
|
26
|
+
if (data.ok) for (const u of data.result) {
|
|
27
|
+
this.offset = Math.max(this.offset, u.update_id)
|
|
28
|
+
if (u.message) this.emit('message', { from: String(u.message.from?.id || ''), text: u.message.text || '', raw: u })
|
|
29
|
+
}
|
|
30
|
+
} catch (e) { await new Promise(r => setTimeout(r, 5000)) }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async send(reply) {
|
|
34
|
+
if (!this.token) throw new Error('TelegramAdapter: token required')
|
|
35
|
+
const url = `${this.api}/bot${this.token}/sendMessage`
|
|
36
|
+
return fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ chat_id: reply.to, text: reply.text }) }).then(r => r.json())
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function tgFloodWait(error) {
|
|
2
|
+
const m = String(error?.message || '').match(/FLOOD_WAIT_(\d+)/)
|
|
3
|
+
return m ? Number(m[1]) * 1000 : null
|
|
4
|
+
}
|
|
5
|
+
export async function tgWithRetry(fn, { attempts = 3 } = {}) {
|
|
6
|
+
let last
|
|
7
|
+
for (let i = 0; i < attempts; i++) {
|
|
8
|
+
try { return await fn() } catch (e) {
|
|
9
|
+
last = e
|
|
10
|
+
const wait = tgFloodWait(e)
|
|
11
|
+
if (wait != null) await new Promise(r => setTimeout(r, wait + 100))
|
|
12
|
+
else if (i < attempts - 1) await new Promise(r => setTimeout(r, 500 * (i + 1)))
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
throw last
|
|
16
|
+
}
|
|
17
|
+
export function withProxy(fetchOpts, proxy) { return proxy ? { ...fetchOpts, proxy } : fetchOpts }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
export class WebhookAdapter extends EventEmitter {
|
|
5
|
+
constructor({ port = 0, path = '/webhook' } = {}) { super(); this.port = port; this.path = path; this._server = null; this.outbox = [] }
|
|
6
|
+
async start() {
|
|
7
|
+
const app = express()
|
|
8
|
+
app.use(express.json())
|
|
9
|
+
app.post(this.path, (req, res) => {
|
|
10
|
+
const m = { from: req.body?.from || 'webhook', text: req.body?.text || '', raw: req.body }
|
|
11
|
+
this.emit('message', m)
|
|
12
|
+
res.json({ ok: true })
|
|
13
|
+
})
|
|
14
|
+
await new Promise(resolve => { this._server = app.listen(this.port, () => resolve()) })
|
|
15
|
+
this.port = this._server.address().port
|
|
16
|
+
}
|
|
17
|
+
async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
|
|
18
|
+
async send(reply) { this.outbox.push(reply) }
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
export class WecomAdapter extends EventEmitter {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
super()
|
|
7
|
+
this.platform = 'wecom'
|
|
8
|
+
this.token = opts.token || process.env.WECOM_WEBHOOK_KEY
|
|
9
|
+
this.port = opts.port || 0
|
|
10
|
+
this.api = opts.api || "https://qyapi.weixin.qq.com/cgi-bin/webhook/send"
|
|
11
|
+
this._server = null
|
|
12
|
+
}
|
|
13
|
+
getRequiredEnv() { return ["WECOM_WEBHOOK_KEY"] }
|
|
14
|
+
async start() {
|
|
15
|
+
if (!this.token) throw new Error('WecomAdapter: ' + this.getRequiredEnv().join(', ') + ' required')
|
|
16
|
+
const app = express()
|
|
17
|
+
app.use(express.json())
|
|
18
|
+
app.post('/webhook', (req, res) => {
|
|
19
|
+
const text = req.body?.text || req.body?.message?.text || req.body?.content || ''
|
|
20
|
+
const from = req.body?.from || req.body?.user_id || req.body?.sender_id || ''
|
|
21
|
+
this.emit('message', { from: String(from), text, raw: req.body })
|
|
22
|
+
res.json({ ok: true })
|
|
23
|
+
})
|
|
24
|
+
await new Promise(r => { this._server = app.listen(this.port, () => r()) })
|
|
25
|
+
this.port = this._server.address().port
|
|
26
|
+
}
|
|
27
|
+
async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
|
|
28
|
+
async send(reply) {
|
|
29
|
+
if (!this.token) throw new Error('WecomAdapter: token required')
|
|
30
|
+
return fetch(this.api, { method: 'POST', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ to: reply.to, text: reply.text }) }).then(r => r.json())
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
export class WecomCallbackAdapter extends EventEmitter {
|
|
4
|
+
constructor(opts = {}) { super(); this.platform = 'wecom_callback'; this.token = opts.token || process.env.WECOM_CALLBACK_TOKEN; this.aesKey = opts.aesKey || process.env.WECOM_ENCODING_AES_KEY; this.port = opts.port || 0 }
|
|
5
|
+
getRequiredEnv() { return ['WECOM_CALLBACK_TOKEN'] }
|
|
6
|
+
async start() {
|
|
7
|
+
if (!this.token) throw new Error('WECOM_CALLBACK_TOKEN required')
|
|
8
|
+
const app = express(); app.use(express.text({ type: '*/*' }))
|
|
9
|
+
app.post('/wecom/callback', (req, res) => { this.emit('message', { from: 'wecom', text: req.body || '', raw: req.body }); res.send('') })
|
|
10
|
+
this._server = await new Promise(r => { const s = app.listen(this.port, () => r(s)) })
|
|
11
|
+
this.port = this._server.address().port
|
|
12
|
+
}
|
|
13
|
+
async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
|
|
14
|
+
async send() { return { error: 'wecom_callback is receive-only; use wecom adapter for outbound' } }
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
export function sha1(...parts) { return crypto.createHash('sha1').update(parts.sort().join('')).digest('hex') }
|
|
3
|
+
export function verifyMsgSignature({ token, timestamp, nonce, encrypt, signature }) {
|
|
4
|
+
return sha1(token, timestamp, nonce, encrypt) === signature
|
|
5
|
+
}
|
|
6
|
+
export function decryptMsg(encryptedB64, encodingAesKey) {
|
|
7
|
+
const aesKey = Buffer.from(encodingAesKey + '=', 'base64')
|
|
8
|
+
const iv = aesKey.slice(0, 16)
|
|
9
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, iv)
|
|
10
|
+
decipher.setAutoPadding(false)
|
|
11
|
+
const decrypted = Buffer.concat([decipher.update(Buffer.from(encryptedB64, 'base64')), decipher.final()])
|
|
12
|
+
const padLen = decrypted[decrypted.length - 1]
|
|
13
|
+
const trimmed = decrypted.slice(16, decrypted.length - padLen)
|
|
14
|
+
const xmlLen = trimmed.readUInt32BE(0)
|
|
15
|
+
return { xml: trimmed.slice(4, 4 + xmlLen).toString('utf8'), corpId: trimmed.slice(4 + xmlLen).toString('utf8') }
|
|
16
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
export class WeixinAdapter extends EventEmitter {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
super()
|
|
7
|
+
this.platform = 'weixin'
|
|
8
|
+
this.token = opts.token || process.env.WEIXIN_TOKEN
|
|
9
|
+
this.port = opts.port || 0
|
|
10
|
+
this.api = opts.api || "https://api.weixin.qq.com/cgi-bin/message/send"
|
|
11
|
+
this._server = null
|
|
12
|
+
}
|
|
13
|
+
getRequiredEnv() { return ["WEIXIN_TOKEN"] }
|
|
14
|
+
async start() {
|
|
15
|
+
if (!this.token) throw new Error('WeixinAdapter: ' + this.getRequiredEnv().join(', ') + ' required')
|
|
16
|
+
const app = express()
|
|
17
|
+
app.use(express.json())
|
|
18
|
+
app.post('/webhook', (req, res) => {
|
|
19
|
+
const text = req.body?.text || req.body?.message?.text || req.body?.content || ''
|
|
20
|
+
const from = req.body?.from || req.body?.user_id || req.body?.sender_id || ''
|
|
21
|
+
this.emit('message', { from: String(from), text, raw: req.body })
|
|
22
|
+
res.json({ ok: true })
|
|
23
|
+
})
|
|
24
|
+
await new Promise(r => { this._server = app.listen(this.port, () => r()) })
|
|
25
|
+
this.port = this._server.address().port
|
|
26
|
+
}
|
|
27
|
+
async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
|
|
28
|
+
async send(reply) {
|
|
29
|
+
if (!this.token) throw new Error('WeixinAdapter: token required')
|
|
30
|
+
return fetch(this.api, { method: 'POST', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ to: reply.to, text: reply.text }) }).then(r => r.json())
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
export class WhatsappAdapter extends EventEmitter {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
super()
|
|
7
|
+
this.platform = 'whatsapp'
|
|
8
|
+
this.token = opts.token || process.env.WHATSAPP_API_TOKEN
|
|
9
|
+
this.phoneId = opts.phoneId || process.env.WHATSAPP_PHONE_NUMBER_ID
|
|
10
|
+
this.verifyToken = opts.verifyToken || process.env.WHATSAPP_VERIFY_TOKEN || 'freddie'
|
|
11
|
+
this.port = opts.port || 0
|
|
12
|
+
this.api = opts.api || 'https://graph.facebook.com/v20.0'
|
|
13
|
+
this._server = null
|
|
14
|
+
}
|
|
15
|
+
getRequiredEnv() { return ['WHATSAPP_API_TOKEN', 'WHATSAPP_PHONE_NUMBER_ID'] }
|
|
16
|
+
async start() {
|
|
17
|
+
if (!this.token || !this.phoneId) throw new Error('WhatsappAdapter: WHATSAPP_API_TOKEN + WHATSAPP_PHONE_NUMBER_ID required')
|
|
18
|
+
const app = express()
|
|
19
|
+
app.use(express.json())
|
|
20
|
+
app.get('/webhook', (req, res) => {
|
|
21
|
+
if (req.query['hub.verify_token'] === this.verifyToken) return res.send(req.query['hub.challenge'])
|
|
22
|
+
res.sendStatus(403)
|
|
23
|
+
})
|
|
24
|
+
app.post('/webhook', (req, res) => {
|
|
25
|
+
const entries = req.body?.entry || []
|
|
26
|
+
for (const e of entries) for (const c of (e.changes || [])) {
|
|
27
|
+
const msgs = c.value?.messages || []
|
|
28
|
+
for (const m of msgs) this.emit('message', { from: m.from, text: m.text?.body || '', raw: m })
|
|
29
|
+
}
|
|
30
|
+
res.json({ ok: true })
|
|
31
|
+
})
|
|
32
|
+
await new Promise(r => { this._server = app.listen(this.port, () => r()) })
|
|
33
|
+
this.port = this._server.address().port
|
|
34
|
+
}
|
|
35
|
+
async stop() { if (this._server) await new Promise(r => this._server.close(() => r())) }
|
|
36
|
+
async send(reply) {
|
|
37
|
+
if (!this.token) throw new Error('WhatsappAdapter: token required')
|
|
38
|
+
return fetch(`${this.api}/${this.phoneId}/messages`, { method: 'POST', headers: { authorization: `Bearer ${this.token}`, 'content-type': 'application/json' }, body: JSON.stringify({ messaging_product: 'whatsapp', to: reply.to, text: { body: reply.text } }) }).then(r => r.json())
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BasePlatformAdapter } from '../base.js'
|
|
2
|
+
export class YuanbaoAdapter extends BasePlatformAdapter {
|
|
3
|
+
constructor(opts = {}) { super({ ...opts, platform: 'yuanbao' }); this.token = opts.token || process.env.YUANBAO_API_KEY; this.api = opts.api || 'https://api.hunyuan.cloud.tencent.com/v1' }
|
|
4
|
+
getRequiredEnv() { return ['YUANBAO_API_KEY'] }
|
|
5
|
+
async send(reply) {
|
|
6
|
+
const r = await fetch(this.api + '/chat/completions', { method: 'POST', headers: { authorization: 'Bearer ' + this.token, 'content-type': 'application/json' }, body: JSON.stringify({ model: 'hunyuan-pro', messages: [{ role: 'user', content: reply.text }] }) })
|
|
7
|
+
return await r.json()
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export async function uploadMedia({ token, file, mime = 'image/png' }) {
|
|
2
|
+
if (!token) throw new Error('yuanbao token required')
|
|
3
|
+
const r = await fetch('https://api.hunyuan.cloud.tencent.com/v1/files', { method: 'POST', headers: { authorization: 'Bearer ' + token, 'content-type': mime }, body: file })
|
|
4
|
+
return await r.json()
|
|
5
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function buildChatPayload({ model = 'hunyuan-pro', messages, stream = false, tools = [] }) {
|
|
2
|
+
return { model, messages, stream, ...(tools.length ? { tools: tools.map(t => ({ type: 'function', function: t })) } : {}) }
|
|
3
|
+
}
|
|
4
|
+
export function parseChatResponse(json) {
|
|
5
|
+
const choice = json?.choices?.[0]
|
|
6
|
+
if (!choice) return { content: '', tool_calls: [] }
|
|
7
|
+
const tc = (choice.message?.tool_calls || []).map(c => ({ id: c.id, name: c.function?.name, arguments: c.function?.arguments ? JSON.parse(c.function.arguments) : {} }))
|
|
8
|
+
return { content: choice.message?.content || '', tool_calls: tc }
|
|
9
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const STICKER_PACK = ['cute', 'work', 'celebration', 'thinking', 'thumbs-up']
|
|
2
|
+
export function buildStickerMessage({ to, sticker }) {
|
|
3
|
+
if (!STICKER_PACK.includes(sticker)) throw new Error('unknown sticker: ' + sticker)
|
|
4
|
+
return { to, type: 'sticker', sticker }
|
|
5
|
+
}
|
|
6
|
+
export function listStickers() { return STICKER_PACK }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { logger } from '../observability/log.js'
|
|
2
|
+
import { runTurn } from '../agent/machine.js'
|
|
3
|
+
|
|
4
|
+
const log = logger('gateway')
|
|
5
|
+
|
|
6
|
+
export class Gateway {
|
|
7
|
+
constructor({ platforms = {}, callLLM = null } = {}) {
|
|
8
|
+
this.platforms = new Map()
|
|
9
|
+
this.callLLM = callLLM
|
|
10
|
+
this.hooks = { inbound: [], outbound: [] }
|
|
11
|
+
for (const [name, adapter] of Object.entries(platforms)) this.register(name, adapter)
|
|
12
|
+
}
|
|
13
|
+
register(name, adapter) {
|
|
14
|
+
this.platforms.set(name, adapter)
|
|
15
|
+
adapter.on?.('message', (m) => this.handleInbound(name, m))
|
|
16
|
+
}
|
|
17
|
+
addHook(stage, fn) { this.hooks[stage].push(fn) }
|
|
18
|
+
async start() {
|
|
19
|
+
for (const a of this.platforms.values()) await a.start?.()
|
|
20
|
+
log.info('gateway started', { platforms: [...this.platforms.keys()] })
|
|
21
|
+
}
|
|
22
|
+
async stop() {
|
|
23
|
+
for (const a of this.platforms.values()) await a.stop?.()
|
|
24
|
+
log.info('gateway stopped')
|
|
25
|
+
}
|
|
26
|
+
async handleInbound(platform, msg) {
|
|
27
|
+
log.info('inbound', { platform, from: msg.from, len: (msg.text || '').length })
|
|
28
|
+
let cur = { ...msg, platform }
|
|
29
|
+
for (const h of this.hooks.inbound) cur = (await h(cur)) || cur
|
|
30
|
+
const result = await runTurn({ prompt: cur.text || '', callLLM: this.callLLM })
|
|
31
|
+
let reply = { to: msg.from, text: result.result || result.error || '', platform, result }
|
|
32
|
+
for (const h of this.hooks.outbound) reply = (await h(reply)) || reply
|
|
33
|
+
const adapter = this.platforms.get(platform)
|
|
34
|
+
await adapter.send?.(reply)
|
|
35
|
+
return reply
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const bootMdHook = async (m) => {
|
|
40
|
+
if ((m.text || '').startsWith('/boot')) m.text = (m.text || '').replace(/^\/boot\s*/, '').trim() || 'hello'
|
|
41
|
+
return m
|
|
42
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
5
|
+
|
|
6
|
+
const PLAT = process.platform
|
|
7
|
+
|
|
8
|
+
function profileSuffix() { return process.env.FREDDIE_PROFILE ? '-' + process.env.FREDDIE_PROFILE : '' }
|
|
9
|
+
|
|
10
|
+
export function serviceName() { return 'freddie-gateway' + profileSuffix() }
|
|
11
|
+
|
|
12
|
+
export function isLinux() { return PLAT === 'linux' }
|
|
13
|
+
export function isMacos() { return PLAT === 'darwin' }
|
|
14
|
+
export function isWindows() { return PLAT === 'win32' }
|
|
15
|
+
|
|
16
|
+
export function supportsSystemdServices() {
|
|
17
|
+
if (!isLinux()) return false
|
|
18
|
+
if (!fs.existsSync('/run/systemd/system')) return false
|
|
19
|
+
return true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function userSystemdUnitDir() {
|
|
23
|
+
return path.join(os.homedir(), '.config', 'systemd', 'user')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getSystemdUnitPath() {
|
|
27
|
+
return path.join(userSystemdUnitDir(), serviceName() + '.service')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function renderSystemdUnit({ execStart, workingDir = process.cwd(), envVars = {} } = {}) {
|
|
31
|
+
const env = Object.entries(envVars).map(([k, v]) => `Environment=${k}=${v}`).join('\n')
|
|
32
|
+
return `[Unit]
|
|
33
|
+
Description=Freddie Gateway${profileSuffix()}
|
|
34
|
+
After=network-online.target
|
|
35
|
+
|
|
36
|
+
[Service]
|
|
37
|
+
Type=simple
|
|
38
|
+
WorkingDirectory=${workingDir}
|
|
39
|
+
ExecStart=${execStart}
|
|
40
|
+
Restart=on-failure
|
|
41
|
+
RestartSec=5
|
|
42
|
+
${env}
|
|
43
|
+
|
|
44
|
+
[Install]
|
|
45
|
+
WantedBy=default.target
|
|
46
|
+
`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function installSystemdUnit({ execStart, workingDir, envVars } = {}) {
|
|
50
|
+
if (!supportsSystemdServices()) throw new Error('systemd user services not available')
|
|
51
|
+
fs.mkdirSync(userSystemdUnitDir(), { recursive: true })
|
|
52
|
+
fs.writeFileSync(getSystemdUnitPath(), renderSystemdUnit({ execStart, workingDir, envVars }))
|
|
53
|
+
spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore' })
|
|
54
|
+
return getSystemdUnitPath()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function controlSystemd(verb) {
|
|
58
|
+
if (!supportsSystemdServices()) throw new Error('systemd user services not available')
|
|
59
|
+
const r = spawnSync('systemctl', ['--user', verb, serviceName()], { encoding: 'utf8' })
|
|
60
|
+
return { ok: r.status === 0, stdout: r.stdout || '', stderr: r.stderr || '', code: r.status }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getLaunchdPlistPath() {
|
|
64
|
+
return path.join(os.homedir(), 'Library', 'LaunchAgents', 'co.freddie.gateway' + profileSuffix() + '.plist')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function renderLaunchdPlist({ execStart, workingDir = process.cwd(), envVars = {} } = {}) {
|
|
68
|
+
const args = execStart.split(/\s+/)
|
|
69
|
+
const argXml = args.map(a => `<string>${a.replace(/&/g, '&').replace(/</g, '<')}</string>`).join('\n ')
|
|
70
|
+
const envXml = Object.entries(envVars).map(([k, v]) => `<key>${k}</key><string>${v}</string>`).join('\n ')
|
|
71
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
72
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
73
|
+
<plist version="1.0">
|
|
74
|
+
<dict>
|
|
75
|
+
<key>Label</key><string>co.freddie.gateway${profileSuffix()}</string>
|
|
76
|
+
<key>WorkingDirectory</key><string>${workingDir}</string>
|
|
77
|
+
<key>ProgramArguments</key>
|
|
78
|
+
<array>
|
|
79
|
+
${argXml}
|
|
80
|
+
</array>
|
|
81
|
+
<key>EnvironmentVariables</key>
|
|
82
|
+
<dict>
|
|
83
|
+
${envXml}
|
|
84
|
+
</dict>
|
|
85
|
+
<key>RunAtLoad</key><true/>
|
|
86
|
+
<key>KeepAlive</key><true/>
|
|
87
|
+
</dict>
|
|
88
|
+
</plist>
|
|
89
|
+
`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function installLaunchdPlist({ execStart, workingDir, envVars } = {}) {
|
|
93
|
+
if (!isMacos()) throw new Error('launchd is macOS-only')
|
|
94
|
+
fs.mkdirSync(path.dirname(getLaunchdPlistPath()), { recursive: true })
|
|
95
|
+
fs.writeFileSync(getLaunchdPlistPath(), renderLaunchdPlist({ execStart, workingDir, envVars }))
|
|
96
|
+
return getLaunchdPlistPath()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function controlLaunchd(verb) {
|
|
100
|
+
if (!isMacos()) throw new Error('launchd is macOS-only')
|
|
101
|
+
const map = { start: 'load', stop: 'unload', restart: 'kickstart' }
|
|
102
|
+
const arg = map[verb] || verb
|
|
103
|
+
const r = spawnSync('launchctl', [arg, getLaunchdPlistPath()], { encoding: 'utf8' })
|
|
104
|
+
return { ok: r.status === 0, stdout: r.stdout || '', stderr: r.stderr || '', code: r.status }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function findGatewayPids() {
|
|
108
|
+
if (isWindows()) {
|
|
109
|
+
const r = spawnSync('wmic', ['process', 'get', 'ProcessId,CommandLine', '/format:csv'], { encoding: 'utf8' })
|
|
110
|
+
if (r.status !== 0) return []
|
|
111
|
+
const lines = (r.stdout || '').split(/\r?\n/).filter(l => /freddie.*gateway/i.test(l))
|
|
112
|
+
return lines.map(l => parseInt(l.split(',').pop(), 10)).filter(n => Number.isFinite(n))
|
|
113
|
+
}
|
|
114
|
+
const r = spawnSync('pgrep', ['-f', 'freddie.*gateway'], { encoding: 'utf8' })
|
|
115
|
+
if (r.status !== 0) return []
|
|
116
|
+
return (r.stdout || '').trim().split('\n').filter(Boolean).map(n => parseInt(n, 10))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function killGatewayProcesses({ signal = 'SIGTERM' } = {}) {
|
|
120
|
+
const pids = findGatewayPids()
|
|
121
|
+
let killed = 0
|
|
122
|
+
for (const pid of pids) {
|
|
123
|
+
try { process.kill(pid, signal); killed++ } catch {}
|
|
124
|
+
}
|
|
125
|
+
return { pids, killed }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getRuntimeSnapshot() {
|
|
129
|
+
return {
|
|
130
|
+
platform: PLAT,
|
|
131
|
+
serviceName: serviceName(),
|
|
132
|
+
systemd: { available: supportsSystemdServices(), unitPath: getSystemdUnitPath() },
|
|
133
|
+
launchd: { available: isMacos(), plistPath: getLaunchdPlistPath() },
|
|
134
|
+
pids: findGatewayPids(),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function startDetached({ execStart, workingDir = process.cwd(), envVars = {} } = {}) {
|
|
139
|
+
const args = execStart.split(/\s+/)
|
|
140
|
+
const child = spawn(args[0], args.slice(1), { cwd: workingDir, env: { ...process.env, ...envVars }, detached: true, stdio: 'ignore' })
|
|
141
|
+
child.unref()
|
|
142
|
+
return child.pid
|
|
143
|
+
}
|
package/src/home.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
|
|
5
|
+
let _cached = null
|
|
6
|
+
|
|
7
|
+
export function getFophHome() {
|
|
8
|
+
if (_cached) return _cached
|
|
9
|
+
const env = process.env.FREDDIE_HOME
|
|
10
|
+
if (env) { _cached = env; ensure(env); return env }
|
|
11
|
+
const profile = process.env.FREDDIE_PROFILE
|
|
12
|
+
const root = path.join(os.homedir(), '.freddie')
|
|
13
|
+
const home = profile ? path.join(root, 'profiles', profile) : root
|
|
14
|
+
_cached = home
|
|
15
|
+
ensure(home)
|
|
16
|
+
return home
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function displayFophHome() {
|
|
20
|
+
const profile = process.env.FREDDIE_PROFILE
|
|
21
|
+
return profile ? `~/.freddie/profiles/${profile}` : '~/.freddie'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function applyProfileOverride(name) {
|
|
25
|
+
if (!name || name === 'default') { delete process.env.FREDDIE_PROFILE; _cached = null; return }
|
|
26
|
+
process.env.FREDDIE_PROFILE = name
|
|
27
|
+
_cached = null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getProfilesRoot() {
|
|
31
|
+
if (process.env.FREDDIE_PROFILES_ROOT) return process.env.FREDDIE_PROFILES_ROOT
|
|
32
|
+
if (process.env.FREDDIE_HOME) return path.join(process.env.FREDDIE_HOME, 'profiles')
|
|
33
|
+
return path.join(os.homedir(), '.freddie', 'profiles')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function listProfiles() {
|
|
37
|
+
const root = getProfilesRoot()
|
|
38
|
+
if (!fs.existsSync(root)) return []
|
|
39
|
+
return fs.readdirSync(root).filter(n => fs.statSync(path.join(root, n)).isDirectory())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resetCacheForTests() { _cached = null }
|
|
43
|
+
|
|
44
|
+
function ensure(p) { try { fs.mkdirSync(p, { recursive: true }) } catch {} }
|