sanook-cli 0.5.1 → 0.5.2
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/.env.example +161 -3
- package/CHANGELOG.md +57 -8
- package/README.md +240 -23
- package/README.th.md +87 -6
- package/dist/approval.js +6 -0
- package/dist/bin.js +3026 -196
- package/dist/brain-context.js +223 -0
- package/dist/brain-doctor.js +318 -0
- package/dist/brain-eval.js +186 -0
- package/dist/brain-final.js +371 -0
- package/dist/brain-review.js +382 -0
- package/dist/brain.js +12 -1
- package/dist/brand.js +1 -1
- package/dist/cli-args.js +152 -0
- package/dist/cli-option-values.js +16 -0
- package/dist/commands.js +172 -13
- package/dist/compaction.js +96 -11
- package/dist/config.js +118 -28
- package/dist/context-compression.js +191 -0
- package/dist/cost.js +49 -15
- package/dist/first-run.js +21 -0
- package/dist/gateway/auth.js +37 -8
- package/dist/gateway/bluebubbles.js +205 -0
- package/dist/gateway/config.js +929 -0
- package/dist/gateway/deliver.js +357 -0
- package/dist/gateway/discord.js +124 -0
- package/dist/gateway/email.js +472 -0
- package/dist/gateway/googlechat.js +207 -0
- package/dist/gateway/homeassistant.js +256 -0
- package/dist/gateway/ledger.js +18 -0
- package/dist/gateway/line.js +171 -0
- package/dist/gateway/lock.js +3 -1
- package/dist/gateway/matrix.js +366 -0
- package/dist/gateway/mattermost.js +322 -0
- package/dist/gateway/ntfy.js +218 -0
- package/dist/gateway/schedule.js +31 -4
- package/dist/gateway/serve.js +267 -7
- package/dist/gateway/server.js +253 -19
- package/dist/gateway/service.js +224 -0
- package/dist/gateway/session.js +343 -0
- package/dist/gateway/signal.js +351 -0
- package/dist/gateway/slack.js +124 -0
- package/dist/gateway/sms.js +169 -0
- package/dist/gateway/targets.js +576 -0
- package/dist/gateway/teams.js +106 -0
- package/dist/gateway/telegram.js +38 -15
- package/dist/gateway/webhooks.js +220 -0
- package/dist/gateway/whatsapp.js +230 -0
- package/dist/hooks.js +13 -2
- package/dist/insights-args.js +35 -0
- package/dist/insights.js +86 -0
- package/dist/loop.js +123 -24
- package/dist/lsp/index.js +23 -5
- package/dist/mcp-registry.js +350 -0
- package/dist/mcp-server.js +1 -1
- package/dist/mcp.js +44 -6
- package/dist/memory.js +100 -33
- package/dist/orchestrate.js +49 -19
- package/dist/personality.js +58 -0
- package/dist/providers/codex.js +70 -36
- package/dist/providers/keys.js +1 -1
- package/dist/providers/models.js +1 -1
- package/dist/providers/registry.js +14 -47
- package/dist/search/chunk.js +7 -8
- package/dist/search/cli.js +75 -0
- package/dist/search/embed-store.js +3 -0
- package/dist/search/indexer.js +44 -1
- package/dist/search/store.js +23 -1
- package/dist/session.js +93 -7
- package/dist/skill-install.js +29 -12
- package/dist/support-dump.js +175 -0
- package/dist/tools/edit.js +45 -15
- package/dist/tools/git.js +10 -5
- package/dist/tools/homeassistant.js +106 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/list.js +19 -6
- package/dist/tools/permission.js +923 -9
- package/dist/tools/read.js +16 -4
- package/dist/tools/schedule.js +19 -3
- package/dist/tools/search.js +217 -13
- package/dist/tools/task.js +18 -7
- package/dist/tools/timeout.js +21 -3
- package/dist/trust.js +11 -1
- package/dist/ui/app.js +48 -8
- package/dist/ui/history.js +37 -5
- package/dist/ui/mentions.js +3 -2
- package/dist/ui/setup.js +17 -4
- package/dist/update.js +24 -11
- package/dist/worktree.js +175 -4
- package/package.json +4 -4
- package/second-brain/AGENTS.md +6 -4
- package/second-brain/CLAUDE.md +7 -1
- package/second-brain/Evals/_Index.md +10 -2
- package/second-brain/Evals/quality-ledger.md +9 -1
- package/second-brain/Evals/second-brain-benchmarks.md +62 -0
- package/second-brain/GEMINI.md +5 -4
- package/second-brain/Home.md +1 -1
- package/second-brain/Projects/_Index.md +3 -1
- package/second-brain/Projects/sanook-cli/_Index.md +26 -0
- package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +156 -0
- package/second-brain/README.md +1 -1
- package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
- package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
- package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
- package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
- package/second-brain/Research/_Index.md +6 -1
- package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
- package/second-brain/Reviews/_Index.md +1 -1
- package/second-brain/Runbooks/_Index.md +6 -1
- package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
- package/second-brain/SANOOK.md +45 -0
- package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
- package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
- package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
- package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
- package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
- package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
- package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
- package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
- package/second-brain/Sessions/_Index.md +15 -1
- package/second-brain/Shared/AI-Context-Index.md +22 -0
- package/second-brain/Shared/Context-Packs/_Index.md +9 -1
- package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
- package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
- package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
- package/second-brain/Shared/Operating-State/current-state.md +22 -3
- package/second-brain/Shared/Scripts/_Index.md +3 -1
- package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +4 -1
- package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
- package/second-brain/Shared/User-Memory/_Index.md +4 -1
- package/second-brain/Shared/User-Memory/response-examples.md +98 -0
- package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
- package/second-brain/Templates/_Index.md +9 -0
- package/second-brain/Templates/final-lite.md +111 -0
- package/second-brain/Templates/final.md +231 -0
- package/second-brain/Vault Structure Map.md +2 -1
- package/skills/structured-output-llm/SKILL.md +1 -1
package/dist/bin.js
CHANGED
|
@@ -1,60 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runAgent } from './loop.js';
|
|
3
|
-
import { redactKey } from './providers/keys.js';
|
|
4
|
-
import { specKey, parseSpec, PROVIDERS, consoleUrl, detectEnvProvider
|
|
3
|
+
import { assertDirectApiKey, redactKey } from './providers/keys.js';
|
|
4
|
+
import { specKey, parseSpec, PROVIDERS, consoleUrl, detectEnvProvider } from './providers/registry.js';
|
|
5
5
|
import { resolveKeyFromEnv } from './providers/keys.js';
|
|
6
6
|
import { hasPricingForKey } from './cost.js';
|
|
7
7
|
import { loadConfig, isFirstRun, loadKeysIntoEnv, parsePricingOverride } from './config.js';
|
|
8
|
-
import { saveSession, latestSession, newSessionId } from './session.js';
|
|
8
|
+
import { saveSession, latestSession, newSessionId, listSessions, loadSession, removeSession, pruneSessions, renameSession, sanitizeSessionForExport, sessionStorePath, } from './session.js';
|
|
9
9
|
import { closeMcp, isValidMcpServerName } from './mcp.js';
|
|
10
10
|
import { readFileSync } from 'node:fs';
|
|
11
11
|
import { homedir } from 'node:os';
|
|
12
|
-
import { join, dirname } from 'node:path';
|
|
12
|
+
import { join, dirname, resolve } from 'node:path';
|
|
13
13
|
import { chmod, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
14
14
|
import { createInterface } from 'node:readline/promises';
|
|
15
15
|
import { appHomePath, BRAND, BRAND_ENV, envFlag } from './brand.js';
|
|
16
|
+
import { hasContinueAnyRequest, hasContinueRequest, hasResumeRequest, hasServeCommandRequest, parseArgs, parseBudgetUsd, parseServeArgs, parseThinkingConfigValue, } from './cli-args.js';
|
|
16
17
|
// สี: เคารพ NO_COLOR + auto-plain เมื่อ pipe/redirect (legacy Windows cmd ก็ไม่เห็น garbage ANSI); FORCE_COLOR บังคับได้
|
|
17
18
|
const useColor = !process.env.NO_COLOR && (Boolean(process.env.FORCE_COLOR) || process.stdout.isTTY === true);
|
|
18
19
|
const DIM = useColor ? '\x1b[2m' : '';
|
|
19
20
|
const RESET = useColor ? '\x1b[0m' : '';
|
|
20
|
-
function parseArgs(argv) {
|
|
21
|
-
let model;
|
|
22
|
-
let budget;
|
|
23
|
-
let json = false;
|
|
24
|
-
let quiet = false;
|
|
25
|
-
let planMode = false;
|
|
26
|
-
let yes = false;
|
|
27
|
-
const rest = [];
|
|
28
|
-
for (let i = 0; i < argv.length; i++) {
|
|
29
|
-
const a = argv[i];
|
|
30
|
-
if (a === '--model' || a === '-m')
|
|
31
|
-
model = argv[++i];
|
|
32
|
-
else if (a === '--budget' || a === '-b')
|
|
33
|
-
budget = Number.parseFloat(argv[++i] ?? '');
|
|
34
|
-
else if (a === '--json')
|
|
35
|
-
json = true;
|
|
36
|
-
else if (a === '-q' || a === '--quiet')
|
|
37
|
-
quiet = true;
|
|
38
|
-
else if (a === '--output-format') {
|
|
39
|
-
const v = argv[++i];
|
|
40
|
-
if (v === 'json')
|
|
41
|
-
json = true;
|
|
42
|
-
else if (v === 'final' || v === 'quiet')
|
|
43
|
-
quiet = true;
|
|
44
|
-
/* 'text' = default */
|
|
45
|
-
}
|
|
46
|
-
else if (a === '--plan')
|
|
47
|
-
planMode = true;
|
|
48
|
-
else if (a === '--yes' || a === '-y')
|
|
49
|
-
yes = true;
|
|
50
|
-
else if (a === '-p' || a === '--print' || a === '-c' || a === '--continue') {
|
|
51
|
-
/* -p headless flag · -c/--continue resume (handled in main) */
|
|
52
|
-
}
|
|
53
|
-
else
|
|
54
|
-
rest.push(a);
|
|
55
|
-
}
|
|
56
|
-
return { model, budget, json, quiet, prompt: rest.join(' ').trim(), planMode, yes };
|
|
57
|
-
}
|
|
58
21
|
async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, planMode = false, permissionMode = 'ask', quiet = false, fallbackModel) {
|
|
59
22
|
const controller = new AbortController();
|
|
60
23
|
process.on('SIGINT', () => {
|
|
@@ -106,105 +69,2560 @@ async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, pl
|
|
|
106
69
|
await appendBrainWorklog(brain, { prompt, summary: cost.summary(), model, today: now.slice(0, 10) }).catch(() => { });
|
|
107
70
|
}
|
|
108
71
|
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
const msg = redactKey(err.message);
|
|
72
|
+
catch (err) {
|
|
73
|
+
const msg = redactKey(err.message);
|
|
74
|
+
if (json)
|
|
75
|
+
process.stdout.write(`${JSON.stringify({ type: 'error', message: msg })}\n`);
|
|
76
|
+
else
|
|
77
|
+
console.error(`\nERROR: ${msg}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// อ่านจาก package.json (single source of truth) — กัน version constant drift
|
|
82
|
+
const PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
83
|
+
const VERSION = PACKAGE.version;
|
|
84
|
+
const PACKAGE_NAME = PACKAGE.name;
|
|
85
|
+
const HELP = `${BRAND.productName} — a terminal AI coding agent (BYOK)
|
|
86
|
+
|
|
87
|
+
usage:
|
|
88
|
+
${BRAND.cliName} "<task>" run one task (headless)
|
|
89
|
+
${BRAND.cliName} -z "<task>" one-shot final output (script-friendly)
|
|
90
|
+
${BRAND.cliName} chat -q "<query>" direct one-shot query
|
|
91
|
+
${BRAND.cliName} interactive REPL
|
|
92
|
+
${BRAND.cliName} setup [section] setup wizard (model | gateway | tools | agent | brain)
|
|
93
|
+
${BRAND.cliName} model choose provider + model
|
|
94
|
+
${BRAND.cliName} --json "<task>" headless, JSONL output (for CI/scripts)
|
|
95
|
+
${BRAND.cliName} sessions list/resume-audit saved conversation sessions
|
|
96
|
+
${BRAND.cliName} insights local usage/session insights
|
|
97
|
+
${BRAND.cliName} dump [--show-keys] support snapshot (secrets redacted)
|
|
98
|
+
${BRAND.cliName} update update ${BRAND.cliName} to the latest npm release
|
|
99
|
+
${BRAND.cliName} doctor ตรวจการติดตั้ง + วิธีแก้ PATH (เมื่อพิมพ์ "${BRAND.cliName}" แล้วไม่เจอ)
|
|
100
|
+
|
|
101
|
+
gateway (อยู่ยาว 24/7 — HTTP loopback + cron):
|
|
102
|
+
${BRAND.cliName} gateway setup telegram ตั้งค่า Telegram token + allowlist
|
|
103
|
+
${BRAND.cliName} gateway setup discord ตั้งค่า Discord bot token + channel allowlist
|
|
104
|
+
${BRAND.cliName} gateway setup slack ตั้งค่า Slack bot/app token + channel allowlist
|
|
105
|
+
${BRAND.cliName} gateway setup mattermost ตั้งค่า Mattermost token + user/channel allowlist
|
|
106
|
+
${BRAND.cliName} gateway setup homeassistant ตั้งค่า Home Assistant token + state-change filters
|
|
107
|
+
${BRAND.cliName} gateway setup email ตั้งค่า Email IMAP/SMTP + allowed senders
|
|
108
|
+
${BRAND.cliName} gateway setup line ตั้งค่า LINE Messaging API push target
|
|
109
|
+
${BRAND.cliName} gateway setup sms ตั้งค่า Twilio SMS webhook + allowlist
|
|
110
|
+
${BRAND.cliName} gateway setup ntfy ตั้งค่า ntfy topic push + subscribe
|
|
111
|
+
${BRAND.cliName} gateway setup signal ตั้งค่า Signal ผ่าน signal-cli HTTP daemon
|
|
112
|
+
${BRAND.cliName} gateway setup whatsapp ตั้งค่า WhatsApp Cloud API webhook + send
|
|
113
|
+
${BRAND.cliName} gateway setup matrix ตั้งค่า Matrix homeserver sync + send
|
|
114
|
+
${BRAND.cliName} gateway setup googlechat ตั้งค่า Google Chat bot send
|
|
115
|
+
${BRAND.cliName} gateway setup bluebubbles ตั้งค่า BlueBubbles/iMessage send
|
|
116
|
+
${BRAND.cliName} gateway setup teams ตั้งค่า Microsoft Teams delivery
|
|
117
|
+
${BRAND.cliName} gateway setup webhooks เปิด generic webhook routes + HMAC
|
|
118
|
+
${BRAND.cliName} gateway run [--port 8787] [--model spec] เปิด gateway (เหมือน serve)
|
|
119
|
+
${BRAND.cliName} gateway start [--port 8787] เปิด gateway เป็น background process
|
|
120
|
+
${BRAND.cliName} gateway stop|restart|install จัดการ gateway service
|
|
121
|
+
${BRAND.cliName} gateway status ดู config/status gateway
|
|
122
|
+
${BRAND.cliName} send --to telegram|discord|slack|mattermost|homeassistant|email|line|sms|ntfy|signal|whatsapp|matrix|googlechat|bluebubbles|teams[:target] "msg" ส่งข้อความออก platform โดยไม่เรียก LLM
|
|
123
|
+
${BRAND.cliName} webhook subscribe <route> [--prompt "..."] [--to telegram|slack|mattermost|homeassistant|sms|ntfy|signal|whatsapp|matrix|googlechat|bluebubbles|teams]
|
|
124
|
+
รับ event จาก GitHub/GitLab/Jira/Stripe แล้ว trigger agent/delivery
|
|
125
|
+
${BRAND.cliName} send --list [platform] ดู messaging targets ที่ตั้งค่าไว้
|
|
126
|
+
${BRAND.cliName} serve [--port 8787] [--model spec] เปิด gateway (OpenAI-compat /v1/chat/completions + scheduler)
|
|
127
|
+
${BRAND.cliName} cron add "<when>" "<task>" [--to <target>] [--model <model>]
|
|
128
|
+
ตั้งงานล่วงหน้า + ส่งผลลัพธ์กลับ messaging target ได้
|
|
129
|
+
${BRAND.cliName} cron list ดู task ทั้งหมด
|
|
130
|
+
${BRAND.cliName} cron rm <id> ลบ task
|
|
131
|
+
|
|
132
|
+
skills (built-in + ติดตั้งเพิ่มได้):
|
|
133
|
+
${BRAND.cliName} skill list ดู skill ทั้งหมด
|
|
134
|
+
${BRAND.cliName} skill add <user/repo|url|path> ติดตั้ง skill จาก GitHub / URL / local
|
|
135
|
+
${BRAND.cliName} skill remove <name> ลบ skill ที่ติดตั้ง
|
|
136
|
+
${BRAND.cliName} models [provider] ดู/verify model id (เทียบ provider จริงถ้ามี key)
|
|
137
|
+
|
|
138
|
+
second brain (Obsidian workspace สำหรับจัดเก็บงาน + ความจำ AI):
|
|
139
|
+
${BRAND.cliName} brain init [path] สร้างโครงสร้าง second-brain ที่ path (ไม่ใส่ = ถาม)
|
|
140
|
+
${BRAND.cliName} brain doctor ตรวจ health ของ second-brain ที่ config.brainPath
|
|
141
|
+
${BRAND.cliName} brain context [--task "..."] แสดง context ที่ Sanook จะ inject + retrieval hits ต่อ task
|
|
142
|
+
${BRAND.cliName} brain eval รัน second-brain benchmark sanity checks
|
|
143
|
+
${BRAND.cliName} brain review curator review: inbox, packs, sessions, evals, note hygiene
|
|
144
|
+
${BRAND.cliName} brain final --task "..." สร้าง final gate note ใน Sessions พร้อม evidence scaffold
|
|
145
|
+
|
|
146
|
+
search (BM25 + optional BYOK semantic เหนือ vault + memory + sessions + skills):
|
|
147
|
+
${BRAND.cliName} index (re)index vault+memory แบบ incremental (O(delta))
|
|
148
|
+
${BRAND.cliName} search "<query>" [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,memory]
|
|
149
|
+
${BRAND.cliName} mcp serve expose brain เป็น MCP server (stdio) ให้ Claude Desktop/Cursor
|
|
150
|
+
|
|
151
|
+
config & mcp:
|
|
152
|
+
${BRAND.cliName} status ดู provider/key/brain/gateway status แบบ redacted
|
|
153
|
+
${BRAND.cliName} auth [list|status|add|remove] จัดการ API keys ของ providers (BYOK, redacted)
|
|
154
|
+
${BRAND.cliName} sessions [list|latest|show|rm] จัดการ saved sessions
|
|
155
|
+
${BRAND.cliName} insights [--days N] [--all] ดู usage/session insights ในเครื่อง
|
|
156
|
+
${BRAND.cliName} dump [--show-keys] diagnostic/support dump แบบไม่โชว์ raw secret
|
|
157
|
+
${BRAND.cliName} tools ดู tool surface ที่ agent ใช้ได้
|
|
158
|
+
${BRAND.cliName} config [get|set <k> <v>] ดู/แก้ ${appHomePath('config.json')} (model/budgetUsd/permissionMode/cacheTtl/compaction/contextCompression/thinking/embeddingModel)
|
|
159
|
+
${BRAND.cliName} mcp [search|info|install|test|doctor|preset|list|add|remove] จัดการ MCP servers
|
|
160
|
+
${BRAND.cliName} trust [status|add|remove] อนุญาต/ยกเลิก project .sanook mcp/hooks/skills/commands
|
|
161
|
+
|
|
162
|
+
flags:
|
|
163
|
+
-m, --model <spec> sonnet/opus/haiku/fable · gpt/codex · gemini · grok · mistral · groq · ollama/lmstudio
|
|
164
|
+
or "provider:model-id" (e.g. openai:gpt-5.3-codex, groq:fast, google:gemini-2.5-flash)
|
|
165
|
+
-b, --budget <usd> stop when estimated cost exceeds this
|
|
166
|
+
-c, --continue resume the latest session ของ project นี้
|
|
167
|
+
-r, --resume <id> resume a specific saved session
|
|
168
|
+
--continue-any resume latest session ข้าม project (explicit)
|
|
169
|
+
--plan plan mode — สำรวจ+วางแผนเท่านั้น ไม่แก้ไฟล์ (read-only)
|
|
170
|
+
-y, --yes อนุมัติ tool อัตโนมัติ (ข้าม ask-mode permission)
|
|
171
|
+
--yolo alias ของ --yes (compat)
|
|
172
|
+
--json machine-readable JSONL output
|
|
173
|
+
-v, --version
|
|
174
|
+
-h, --help
|
|
175
|
+
|
|
176
|
+
env (BYOK — direct API key only):
|
|
177
|
+
ANTHROPIC_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / OPENAI_API_KEY
|
|
178
|
+
${BRAND_ENV.disableUpdateCheck}=1 disable interactive update prompts`;
|
|
179
|
+
/** sanook serve [--port N] [--model spec] — เปิด gateway (HTTP loopback + cron scheduler) อยู่ยาว */
|
|
180
|
+
async function runServe(args) {
|
|
181
|
+
const parsed = parseServeArgs(args);
|
|
182
|
+
if (parsed.portError) {
|
|
183
|
+
console.error(`port ไม่ถูกต้อง: ${parsed.portError}`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
const config = await loadConfig({ model: parsed.model });
|
|
187
|
+
const { startGateway } = await import('./gateway/serve.js');
|
|
188
|
+
process.stdout.write(`${DIM}${BRAND.productName} gateway — model: ${config.model}${RESET}\n`);
|
|
189
|
+
const stop = await startGateway({
|
|
190
|
+
port: parsed.port,
|
|
191
|
+
model: config.model,
|
|
192
|
+
budgetUsd: config.budgetUsd,
|
|
193
|
+
permissionMode: envFlag(BRAND_ENV.gatewayAllowWrite) ? 'auto' : config.permissionMode,
|
|
194
|
+
onLog: (m) => process.stdout.write(`${DIM}[gateway] ${m}${RESET}\n`),
|
|
195
|
+
});
|
|
196
|
+
const shutdown = () => {
|
|
197
|
+
stop();
|
|
198
|
+
process.stdout.write('\n[gateway] หยุดแล้ว\n');
|
|
199
|
+
process.exit(0);
|
|
200
|
+
};
|
|
201
|
+
process.on('SIGINT', shutdown);
|
|
202
|
+
process.on('SIGTERM', shutdown);
|
|
203
|
+
// server + scheduler interval ถือ event loop ไว้ → process อยู่ยาวจนกด Ctrl-C
|
|
204
|
+
}
|
|
205
|
+
async function startModelSetup() {
|
|
206
|
+
const config = await loadConfig({});
|
|
207
|
+
const { startApp } = await import('./ui/render.js');
|
|
208
|
+
startApp({
|
|
209
|
+
needsSetup: true,
|
|
210
|
+
appProps: {
|
|
211
|
+
initialModel: config.model,
|
|
212
|
+
fallbackModel: config.fallbackModel,
|
|
213
|
+
budgetUsd: config.budgetUsd,
|
|
214
|
+
permissionMode: config.permissionMode,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async function runTools(_args = []) {
|
|
219
|
+
const { tools } = await import('./tools/index.js');
|
|
220
|
+
const names = Object.keys(tools).sort();
|
|
221
|
+
console.log(`${BRAND.productName} tools (${names.length})`);
|
|
222
|
+
console.log(names.map((n) => ` ${n}`).join('\n'));
|
|
223
|
+
console.log(`\nจัดการ MCP เพิ่มเติม: ${BRAND.cliName} mcp add <name> <command> [args...]`);
|
|
224
|
+
}
|
|
225
|
+
async function runAgentSetupSummary() {
|
|
226
|
+
const cfg = await loadConfig({});
|
|
227
|
+
console.log(`${BRAND.productName} agent settings`);
|
|
228
|
+
console.log(` model: ${cfg.model}`);
|
|
229
|
+
console.log(` fallbackModel: ${cfg.fallbackModel ?? '(not set)'}`);
|
|
230
|
+
console.log(` personality: ${cfg.personality ?? '(none)'}`);
|
|
231
|
+
console.log(` permissionMode: ${cfg.permissionMode}`);
|
|
232
|
+
console.log(` maxSteps: ${cfg.maxSteps}`);
|
|
233
|
+
console.log(` budgetUsd: ${cfg.budgetUsd ?? '(not set)'}`);
|
|
234
|
+
console.log(` brainPath: ${cfg.brainPath ?? '(not set)'}`);
|
|
235
|
+
console.log(` insights: ${BRAND.cliName} insights [--days N]`);
|
|
236
|
+
console.log('\nแก้ค่าได้ด้วย:');
|
|
237
|
+
console.log(` ${BRAND.cliName} config set personality concise`);
|
|
238
|
+
console.log(` ${BRAND.cliName} config set permissionMode ask`);
|
|
239
|
+
console.log(` ${BRAND.cliName} config set budgetUsd 0.25`);
|
|
240
|
+
console.log(` ${BRAND.cliName} config set fallbackModel haiku`);
|
|
241
|
+
}
|
|
242
|
+
async function runGatewayStatus() {
|
|
243
|
+
const { readGatewayConfig, redactGatewayConfig, resolveBlueBubblesConfig, resolveDiscordConfig, resolveEmailConfig, resolveGoogleChatConfig, resolveHomeAssistantConfig, resolveLineConfig, resolveMattermostConfig, resolveMatrixConfig, resolveNtfyConfig, resolveSignalConfig, resolveSlackConfig, resolveSmsConfig, resolveTelegramConfig, resolveTeamsConfig, resolveWhatsAppConfig, resolveWebhookConfig, gatewayConfigPath, } = await import('./gateway/config.js');
|
|
244
|
+
const cfg = await readGatewayConfig();
|
|
245
|
+
const telegram = resolveTelegramConfig(cfg);
|
|
246
|
+
const discord = resolveDiscordConfig(cfg);
|
|
247
|
+
const slack = resolveSlackConfig(cfg);
|
|
248
|
+
const email = resolveEmailConfig(cfg);
|
|
249
|
+
const homeassistant = resolveHomeAssistantConfig(cfg);
|
|
250
|
+
const line = resolveLineConfig(cfg);
|
|
251
|
+
const mattermost = resolveMattermostConfig(cfg);
|
|
252
|
+
const sms = resolveSmsConfig(cfg);
|
|
253
|
+
const ntfy = resolveNtfyConfig(cfg);
|
|
254
|
+
const signal = resolveSignalConfig(cfg);
|
|
255
|
+
const whatsapp = resolveWhatsAppConfig(cfg);
|
|
256
|
+
const matrix = resolveMatrixConfig(cfg);
|
|
257
|
+
const googleChat = resolveGoogleChatConfig(cfg);
|
|
258
|
+
const bluebubbles = resolveBlueBubblesConfig(cfg);
|
|
259
|
+
const teams = resolveTeamsConfig(cfg);
|
|
260
|
+
const webhooks = resolveWebhookConfig(cfg);
|
|
261
|
+
const { redactSignalId } = await import('./gateway/signal.js');
|
|
262
|
+
const { redactWhatsAppId } = await import('./gateway/whatsapp.js');
|
|
263
|
+
console.log(`${BRAND.productName} gateway`);
|
|
264
|
+
console.log(` config: ${gatewayConfigPath()}`);
|
|
265
|
+
console.log(` token: ${appHomePath('gateway', 'token')} (HTTP bearer, auto-created on run)`);
|
|
266
|
+
const { gatewayServiceStatus } = await import('./gateway/service.js');
|
|
267
|
+
const service = await gatewayServiceStatus();
|
|
268
|
+
console.log(` service: ${service.running ? `running (pid ${service.state?.pid})` : service.state ? `stopped (last pid ${service.state.pid})` : 'not started'}`);
|
|
269
|
+
console.log(` log: ${service.logPath}`);
|
|
270
|
+
console.log(` telegram: ${telegram.token ? `configured via ${telegram.source}` : 'not configured'}`);
|
|
271
|
+
if (telegram.token) {
|
|
272
|
+
console.log(` enabled: ${telegram.enabled}`);
|
|
273
|
+
console.log(` allowed chats: ${telegram.allowedChatIds.length ? telegram.allowedChatIds.join(', ') : '(none — fail closed)'}`);
|
|
274
|
+
console.log(` allow write: ${telegram.allowWrite ? 'yes' : 'no'}`);
|
|
275
|
+
}
|
|
276
|
+
console.log(` discord: ${discord.token ? `configured via ${discord.source}` : 'not configured'}`);
|
|
277
|
+
if (discord.token) {
|
|
278
|
+
console.log(` default channel: ${discord.defaultChannelId ?? '(not set)'}`);
|
|
279
|
+
console.log(` allowed channels: ${discord.allowedChannelIds.length ? discord.allowedChannelIds.join(', ') : '(none)'}`);
|
|
280
|
+
}
|
|
281
|
+
console.log(` slack: ${slack.botToken ? `configured via ${slack.source}` : 'not configured'}`);
|
|
282
|
+
if (slack.botToken) {
|
|
283
|
+
console.log(` app token: ${slack.appToken ? 'set' : '(not set — needed for future Socket Mode gateway)'}`);
|
|
284
|
+
console.log(` default channel: ${slack.defaultChannelId ?? '(not set)'}`);
|
|
285
|
+
console.log(` allowed channels: ${slack.allowedChannelIds.length ? slack.allowedChannelIds.join(', ') : '(none)'}`);
|
|
286
|
+
}
|
|
287
|
+
console.log(` mattermost: ${mattermost.serverUrl || mattermost.token ? `configured via ${mattermost.source}` : 'not configured'}`);
|
|
288
|
+
if (mattermost.serverUrl || mattermost.token) {
|
|
289
|
+
console.log(` server url: ${mattermost.serverUrl ?? '(not set)'}`);
|
|
290
|
+
console.log(` token: ${mattermost.token ? 'set' : '(not set)'}`);
|
|
291
|
+
console.log(` home channel: ${mattermost.homeChannel ?? '(not set)'}`);
|
|
292
|
+
console.log(` allowed users: ${mattermost.allowedUsers.length ? mattermost.allowedUsers.join(', ') : mattermost.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
293
|
+
console.log(` allowed channels: ${mattermost.allowedChannels.length ? mattermost.allowedChannels.join(', ') : '(none)'}`);
|
|
294
|
+
console.log(` free channels: ${mattermost.freeResponseChannels.length ? mattermost.freeResponseChannels.join(', ') : '(none)'}`);
|
|
295
|
+
console.log(` require mention: ${mattermost.requireMention ? 'yes' : 'no'}`);
|
|
296
|
+
console.log(` reply mode: ${mattermost.replyMode}`);
|
|
297
|
+
}
|
|
298
|
+
console.log(` homeassistant: ${homeassistant.token || homeassistant.url !== 'http://homeassistant.local:8123' ? `configured via ${homeassistant.source}` : 'not configured'}`);
|
|
299
|
+
if (homeassistant.token || homeassistant.url !== 'http://homeassistant.local:8123') {
|
|
300
|
+
console.log(` url: ${homeassistant.url}`);
|
|
301
|
+
console.log(` token: ${homeassistant.token ? 'set' : '(not set)'}`);
|
|
302
|
+
console.log(` home channel: ${homeassistant.homeChannel ?? '(not set)'}`);
|
|
303
|
+
console.log(` watch domains: ${homeassistant.watchDomains.length ? homeassistant.watchDomains.join(', ') : '(none)'}`);
|
|
304
|
+
console.log(` watch entities: ${homeassistant.watchEntities.length ? homeassistant.watchEntities.join(', ') : '(none)'}`);
|
|
305
|
+
console.log(` ignore entities: ${homeassistant.ignoreEntities.length ? homeassistant.ignoreEntities.join(', ') : '(none)'}`);
|
|
306
|
+
console.log(` watch all: ${homeassistant.watchAll ? 'yes' : 'no'}`);
|
|
307
|
+
console.log(` cooldown: ${homeassistant.cooldownSeconds}s`);
|
|
308
|
+
}
|
|
309
|
+
console.log(` email: ${email.address ? `configured via ${email.source}` : 'not configured'}`);
|
|
310
|
+
if (email.address) {
|
|
311
|
+
console.log(` address: ${email.address}`);
|
|
312
|
+
console.log(` smtp: ${email.smtpHost ?? '(not set)'}:${email.smtpPort}`);
|
|
313
|
+
console.log(` imap: ${email.imapHost ?? '(not set)'}:${email.imapPort}`);
|
|
314
|
+
console.log(` home address: ${email.homeAddress ?? '(not set)'}`);
|
|
315
|
+
console.log(` allowed senders: ${email.allowedUsers.length ? email.allowedUsers.join(', ') : email.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
316
|
+
}
|
|
317
|
+
console.log(` line: ${line.channelAccessToken ? `configured via ${line.source}` : 'not configured'}`);
|
|
318
|
+
if (line.channelAccessToken) {
|
|
319
|
+
console.log(` channel secret: ${line.channelSecret ? 'set' : '(not set — needed for webhook replies)'}`);
|
|
320
|
+
console.log(` home channel: ${line.homeChannel ?? '(not set)'}`);
|
|
321
|
+
console.log(` allowed users: ${line.allowedUsers.length ? line.allowedUsers.join(', ') : line.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
322
|
+
console.log(` allowed groups: ${line.allowedGroups.length ? line.allowedGroups.join(', ') : '(none)'}`);
|
|
323
|
+
console.log(` allowed rooms: ${line.allowedRooms.length ? line.allowedRooms.join(', ') : '(none)'}`);
|
|
324
|
+
console.log(` public url: ${line.publicUrl ?? '(not set)'}`);
|
|
325
|
+
}
|
|
326
|
+
console.log(` sms: ${sms.accountSid || sms.authToken || sms.phoneNumber ? `configured via ${sms.source}` : 'not configured'}`);
|
|
327
|
+
if (sms.accountSid || sms.authToken || sms.phoneNumber) {
|
|
328
|
+
console.log(` account sid: ${sms.accountSid ? 'set' : '(not set)'}`);
|
|
329
|
+
console.log(` auth token: ${sms.authToken ? 'set' : '(not set)'}`);
|
|
330
|
+
console.log(` phone number: ${sms.phoneNumber ?? '(not set)'}`);
|
|
331
|
+
console.log(` home channel: ${sms.homeChannel ?? '(not set)'}`);
|
|
332
|
+
console.log(` allowed users: ${sms.allowedUsers.length ? sms.allowedUsers.join(', ') : sms.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
333
|
+
console.log(` webhook url: ${sms.webhookUrl ?? (sms.insecureNoSignature ? '(signature disabled)' : '(not set)')}`);
|
|
334
|
+
}
|
|
335
|
+
console.log(` ntfy: ${ntfy.topic || ntfy.token ? `configured via ${ntfy.source}` : 'not configured'}`);
|
|
336
|
+
if (ntfy.topic || ntfy.token) {
|
|
337
|
+
console.log(` server url: ${ntfy.serverUrl}`);
|
|
338
|
+
console.log(` topic: ${ntfy.topic ?? '(not set)'}`);
|
|
339
|
+
console.log(` publish topic: ${ntfy.publishTopic ?? '(same as topic)'}`);
|
|
340
|
+
console.log(` home channel: ${ntfy.homeChannel ?? '(not set)'}`);
|
|
341
|
+
console.log(` allowed topics: ${ntfy.allowedUsers.length ? ntfy.allowedUsers.join(', ') : ntfy.allowAllUsers ? '(all topics)' : '(none)'}`);
|
|
342
|
+
console.log(` token: ${ntfy.token ? 'set' : '(not set)'}`);
|
|
343
|
+
console.log(` markdown: ${ntfy.markdown ? 'yes' : 'no'}`);
|
|
344
|
+
}
|
|
345
|
+
console.log(` signal: ${signal.account ? `configured via ${signal.source}` : 'not configured'}`);
|
|
346
|
+
if (signal.account) {
|
|
347
|
+
console.log(` http url: ${signal.httpUrl}`);
|
|
348
|
+
console.log(` account: ${redactSignalId(signal.account)}`);
|
|
349
|
+
console.log(` home channel: ${redactSignalId(signal.homeChannel)}`);
|
|
350
|
+
console.log(` allowed users: ${signal.allowedUsers.length ? signal.allowedUsers.map(redactSignalId).join(', ') : signal.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
351
|
+
console.log(` allowed groups: ${signal.groupAllowedUsers.length ? signal.groupAllowedUsers.map(redactSignalId).join(', ') : '(none)'}`);
|
|
352
|
+
console.log(` require mention: ${signal.requireMention ? 'yes' : 'no'}`);
|
|
353
|
+
}
|
|
354
|
+
console.log(` whatsapp: ${whatsapp.phoneNumberId || whatsapp.accessToken ? `configured via ${whatsapp.source}` : 'not configured'}`);
|
|
355
|
+
if (whatsapp.phoneNumberId || whatsapp.accessToken) {
|
|
356
|
+
console.log(` phone number id: ${whatsapp.phoneNumberId ? 'set' : '(not set)'}`);
|
|
357
|
+
console.log(` access token: ${whatsapp.accessToken ? 'set' : '(not set)'}`);
|
|
358
|
+
console.log(` app secret: ${whatsapp.appSecret ? 'set' : '(not set — needed for webhook)'}`);
|
|
359
|
+
console.log(` verify token: ${whatsapp.verifyToken ? 'set' : '(not set — needed for webhook verify)'}`);
|
|
360
|
+
console.log(` home channel: ${redactWhatsAppId(whatsapp.homeChannel)}`);
|
|
361
|
+
console.log(` allowed users: ${whatsapp.allowedUsers.length ? whatsapp.allowedUsers.map(redactWhatsAppId).join(', ') : whatsapp.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
362
|
+
console.log(` public url: ${whatsapp.publicUrl ?? '(not set)'}`);
|
|
363
|
+
console.log(` api version: ${whatsapp.apiVersion}`);
|
|
364
|
+
}
|
|
365
|
+
console.log(` matrix: ${matrix.homeserver || matrix.accessToken || matrix.userId ? `configured via ${matrix.source}` : 'not configured'}`);
|
|
366
|
+
if (matrix.homeserver || matrix.accessToken || matrix.userId) {
|
|
367
|
+
console.log(` homeserver: ${matrix.homeserver ?? '(not set)'}`);
|
|
368
|
+
console.log(` access token: ${matrix.accessToken ? 'set' : '(not set)'}`);
|
|
369
|
+
console.log(` user id: ${matrix.userId ?? '(not set)'}`);
|
|
370
|
+
console.log(` password: ${matrix.password ? 'set' : '(not set)'}`);
|
|
371
|
+
console.log(` home room: ${matrix.homeRoom ?? '(not set)'}`);
|
|
372
|
+
console.log(` allowed users: ${matrix.allowedUsers.length ? matrix.allowedUsers.join(', ') : matrix.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
373
|
+
console.log(` allowed rooms: ${matrix.allowedRooms.length ? matrix.allowedRooms.join(', ') : '(none)'}`);
|
|
374
|
+
console.log(` free rooms: ${matrix.freeResponseRooms.length ? matrix.freeResponseRooms.join(', ') : '(none)'}`);
|
|
375
|
+
console.log(` require mention: ${matrix.requireMention ? 'yes' : 'no'}`);
|
|
376
|
+
console.log(` auto join: ${matrix.autoJoin ? 'yes' : 'no'}`);
|
|
377
|
+
}
|
|
378
|
+
console.log(` googlechat: ${googleChat.serviceAccountJson || googleChat.incomingWebhookUrl ? `configured via ${googleChat.source}` : 'not configured'}`);
|
|
379
|
+
if (googleChat.serviceAccountJson || googleChat.incomingWebhookUrl) {
|
|
380
|
+
console.log(` project id: ${googleChat.projectId ?? '(not set)'}`);
|
|
381
|
+
console.log(` subscription: ${googleChat.subscriptionName ? 'set' : '(not set — needed for Pub/Sub inbound)'}`);
|
|
382
|
+
console.log(` service account: ${googleChat.serviceAccountJson ? 'set' : '(not set)'}`);
|
|
383
|
+
console.log(` api base url: ${googleChat.apiBaseUrl}`);
|
|
384
|
+
console.log(` webhook url: ${googleChat.incomingWebhookUrl ? 'set' : '(not set)'}`);
|
|
385
|
+
console.log(` home channel: ${googleChat.homeChannel ?? '(not set)'}`);
|
|
386
|
+
console.log(` allowed spaces: ${googleChat.allowedSpaces.length ? googleChat.allowedSpaces.join(', ') : googleChat.allowAllSpaces ? '(all spaces)' : '(none)'}`);
|
|
387
|
+
console.log(` allowed users: ${googleChat.allowedUsers.length ? googleChat.allowedUsers.join(', ') : googleChat.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
388
|
+
console.log(` free spaces: ${googleChat.freeResponseSpaces.length ? googleChat.freeResponseSpaces.join(', ') : '(none)'}`);
|
|
389
|
+
console.log(` flow control: messages=${googleChat.maxMessages}, bytes=${googleChat.maxBytes}`);
|
|
390
|
+
}
|
|
391
|
+
console.log(` bluebubbles: ${bluebubbles.serverUrl || bluebubbles.password ? `configured via ${bluebubbles.source}` : 'not configured'}`);
|
|
392
|
+
if (bluebubbles.serverUrl || bluebubbles.password) {
|
|
393
|
+
console.log(` server url: ${bluebubbles.serverUrl ?? '(not set)'}`);
|
|
394
|
+
console.log(` password: ${bluebubbles.password ? 'set' : '(not set)'}`);
|
|
395
|
+
console.log(` webhook: ${bluebubbles.webhookHost}:${bluebubbles.webhookPort}${bluebubbles.webhookPath}`);
|
|
396
|
+
console.log(` home channel: ${bluebubbles.homeChannel ?? '(not set)'}`);
|
|
397
|
+
console.log(` allowed targets: ${bluebubbles.allowedUsers.length ? bluebubbles.allowedUsers.join(', ') : bluebubbles.allowAllUsers ? '(all targets)' : '(none)'}`);
|
|
398
|
+
console.log(` require mention: ${bluebubbles.requireMention ? 'yes' : 'no'}`);
|
|
399
|
+
}
|
|
400
|
+
console.log(` teams: ${teams.incomingWebhookUrl || teams.graphAccessToken || teams.clientId ? `configured via ${teams.source}` : 'not configured'}`);
|
|
401
|
+
if (teams.incomingWebhookUrl || teams.graphAccessToken || teams.clientId) {
|
|
402
|
+
console.log(` delivery mode: ${teams.deliveryMode}`);
|
|
403
|
+
console.log(` webhook url: ${teams.incomingWebhookUrl ? 'set' : '(not set)'}`);
|
|
404
|
+
console.log(` graph token: ${teams.graphAccessToken ? 'set' : '(not set)'}`);
|
|
405
|
+
console.log(` chat id: ${teams.chatId ?? '(not set)'}`);
|
|
406
|
+
console.log(` team/channel: ${teams.teamId && teams.channelId ? `${teams.teamId}/${teams.channelId}` : '(not set)'}`);
|
|
407
|
+
console.log(` home channel: ${teams.homeChannel ?? '(not set)'}`);
|
|
408
|
+
console.log(` bot app: ${teams.clientId && teams.tenantId ? 'set' : '(not set)'}`);
|
|
409
|
+
console.log(` allowed users: ${teams.allowedUsers.length ? teams.allowedUsers.join(', ') : teams.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
410
|
+
console.log(` webhook port: ${teams.port}`);
|
|
411
|
+
}
|
|
412
|
+
console.log(` webhooks: ${webhooks.enabled ? `enabled via ${webhooks.source}` : 'not enabled'} (${Object.keys(webhooks.routes).length} route${Object.keys(webhooks.routes).length === 1 ? '' : 's'})`);
|
|
413
|
+
if (webhooks.enabled) {
|
|
414
|
+
console.log(` global secret: ${webhooks.secret ? 'set' : '(not set)'}`);
|
|
415
|
+
console.log(` public url: ${webhooks.publicUrl ?? '(not set)'}`);
|
|
416
|
+
console.log(` rate limit: ${webhooks.rateLimitPerMinute}/minute`);
|
|
417
|
+
}
|
|
418
|
+
console.log(`\nredacted config:\n${JSON.stringify(redactGatewayConfig(cfg), null, 2)}`);
|
|
419
|
+
}
|
|
420
|
+
async function runGatewaySetup(args) {
|
|
421
|
+
const platformArgProvided = Boolean(args[0] && !args[0].startsWith('--'));
|
|
422
|
+
let platform = platformArgProvided ? args[0] : undefined;
|
|
423
|
+
const rest = platformArgProvided ? args.slice(1) : args;
|
|
424
|
+
if (!platform) {
|
|
425
|
+
if (!process.stdin.isTTY) {
|
|
426
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup <telegram|discord|slack|mattermost|homeassistant|email|line|sms|ntfy|signal|whatsapp|matrix|googlechat|bluebubbles|teams|webhooks> [options]`);
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
const { readGatewayConfig, resolveBlueBubblesConfig, resolveDiscordConfig, resolveEmailConfig, resolveGoogleChatConfig, resolveHomeAssistantConfig, resolveLineConfig, resolveMattermostConfig, resolveMatrixConfig, resolveNtfyConfig, resolveSignalConfig, resolveSlackConfig, resolveSmsConfig, resolveTelegramConfig, resolveTeamsConfig, resolveWhatsAppConfig, resolveWebhookConfig, } = await import('./gateway/config.js');
|
|
430
|
+
const cfg = await readGatewayConfig();
|
|
431
|
+
const options = [
|
|
432
|
+
{ id: 'telegram', label: `Telegram ${resolveTelegramConfig(cfg).token ? '(configured)' : ''}` },
|
|
433
|
+
{ id: 'discord', label: `Discord ${resolveDiscordConfig(cfg).token ? '(configured)' : ''}` },
|
|
434
|
+
{ id: 'slack', label: `Slack ${resolveSlackConfig(cfg).botToken ? '(configured)' : ''}` },
|
|
435
|
+
{ id: 'mattermost', label: `Mattermost ${resolveMattermostConfig(cfg).serverUrl ? '(configured)' : ''}` },
|
|
436
|
+
{ id: 'homeassistant', label: `Home Assistant ${resolveHomeAssistantConfig(cfg).token ? '(configured)' : ''}` },
|
|
437
|
+
{ id: 'email', label: `Email ${resolveEmailConfig(cfg).address ? '(configured)' : ''}` },
|
|
438
|
+
{ id: 'line', label: `LINE ${resolveLineConfig(cfg).channelAccessToken ? '(configured)' : ''}` },
|
|
439
|
+
{ id: 'sms', label: `SMS/Twilio ${resolveSmsConfig(cfg).accountSid ? '(configured)' : ''}` },
|
|
440
|
+
{ id: 'ntfy', label: `ntfy ${resolveNtfyConfig(cfg).topic ? '(configured)' : ''}` },
|
|
441
|
+
{ id: 'signal', label: `Signal ${resolveSignalConfig(cfg).account ? '(configured)' : ''}` },
|
|
442
|
+
{ id: 'whatsapp', label: `WhatsApp Cloud ${resolveWhatsAppConfig(cfg).phoneNumberId ? '(configured)' : ''}` },
|
|
443
|
+
{ id: 'matrix', label: `Matrix ${resolveMatrixConfig(cfg).homeserver ? '(configured)' : ''}` },
|
|
444
|
+
{ id: 'googlechat', label: `Google Chat ${resolveGoogleChatConfig(cfg).serviceAccountJson || resolveGoogleChatConfig(cfg).incomingWebhookUrl ? '(configured)' : ''}` },
|
|
445
|
+
{ id: 'bluebubbles', label: `BlueBubbles/iMessage ${resolveBlueBubblesConfig(cfg).serverUrl ? '(configured)' : ''}` },
|
|
446
|
+
{ id: 'teams', label: `Microsoft Teams ${resolveTeamsConfig(cfg).incomingWebhookUrl || resolveTeamsConfig(cfg).graphAccessToken ? '(configured)' : ''}` },
|
|
447
|
+
{ id: 'webhooks', label: `Webhooks ${resolveWebhookConfig(cfg).enabled ? '(configured)' : ''}` },
|
|
448
|
+
];
|
|
449
|
+
console.log(`${BRAND.productName} gateway setup`);
|
|
450
|
+
for (const [i, option] of options.entries())
|
|
451
|
+
console.log(` ${i + 1}. ${option.label}`);
|
|
452
|
+
const answer = await askText('เลือก platform [1-16]: ');
|
|
453
|
+
const index = Number(answer || '1') - 1;
|
|
454
|
+
platform = options[index]?.id;
|
|
455
|
+
}
|
|
456
|
+
if (platform === 'whatsapp-cloud')
|
|
457
|
+
platform = 'whatsapp';
|
|
458
|
+
if (platform === 'msteams' || platform === 'ms-teams' || platform === 'microsoft-teams')
|
|
459
|
+
platform = 'teams';
|
|
460
|
+
if (platform === 'google-chat' || platform === 'google_chat' || platform === 'gchat')
|
|
461
|
+
platform = 'googlechat';
|
|
462
|
+
if (platform === 'blue-bubbles' || platform === 'blue_bubbles' || platform === 'imessage')
|
|
463
|
+
platform = 'bluebubbles';
|
|
464
|
+
if (!platform ||
|
|
465
|
+
![
|
|
466
|
+
'telegram',
|
|
467
|
+
'discord',
|
|
468
|
+
'slack',
|
|
469
|
+
'mattermost',
|
|
470
|
+
'homeassistant',
|
|
471
|
+
'hass',
|
|
472
|
+
'email',
|
|
473
|
+
'line',
|
|
474
|
+
'sms',
|
|
475
|
+
'ntfy',
|
|
476
|
+
'signal',
|
|
477
|
+
'whatsapp',
|
|
478
|
+
'matrix',
|
|
479
|
+
'googlechat',
|
|
480
|
+
'bluebubbles',
|
|
481
|
+
'teams',
|
|
482
|
+
'webhooks',
|
|
483
|
+
].includes(platform)) {
|
|
484
|
+
console.error(`ตอนนี้ setup อัตโนมัติรองรับ telegram / discord / slack / mattermost / homeassistant / email / line / sms / ntfy / signal / whatsapp / matrix / googlechat / bluebubbles / teams / webhooks — ได้ "${platform ?? ''}"`);
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
if (platform === 'discord')
|
|
488
|
+
return runDiscordGatewaySetup(rest);
|
|
489
|
+
if (platform === 'slack')
|
|
490
|
+
return runSlackGatewaySetup(rest);
|
|
491
|
+
if (platform === 'mattermost')
|
|
492
|
+
return runMattermostGatewaySetup(rest);
|
|
493
|
+
if (platform === 'homeassistant' || platform === 'hass')
|
|
494
|
+
return runHomeAssistantGatewaySetup(rest);
|
|
495
|
+
if (platform === 'email')
|
|
496
|
+
return runEmailGatewaySetup(rest);
|
|
497
|
+
if (platform === 'line')
|
|
498
|
+
return runLineGatewaySetup(rest);
|
|
499
|
+
if (platform === 'sms')
|
|
500
|
+
return runSmsGatewaySetup(rest);
|
|
501
|
+
if (platform === 'ntfy')
|
|
502
|
+
return runNtfyGatewaySetup(rest);
|
|
503
|
+
if (platform === 'signal')
|
|
504
|
+
return runSignalGatewaySetup(rest);
|
|
505
|
+
if (platform === 'whatsapp')
|
|
506
|
+
return runWhatsAppGatewaySetup(rest);
|
|
507
|
+
if (platform === 'matrix')
|
|
508
|
+
return runMatrixGatewaySetup(rest);
|
|
509
|
+
if (platform === 'googlechat')
|
|
510
|
+
return runGoogleChatGatewaySetup(rest);
|
|
511
|
+
if (platform === 'bluebubbles')
|
|
512
|
+
return runBlueBubblesGatewaySetup(rest);
|
|
513
|
+
if (platform === 'teams')
|
|
514
|
+
return runTeamsGatewaySetup(rest);
|
|
515
|
+
if (platform === 'webhooks')
|
|
516
|
+
return runWebhookGatewaySetup(rest);
|
|
517
|
+
let token = argValue(rest, '--bot-token', '--token');
|
|
518
|
+
let allowedRaw = argValue(rest, '--allowed-chats', '--chat-ids');
|
|
519
|
+
const allowWrite = rest.includes('--allow-write');
|
|
520
|
+
if (!token) {
|
|
521
|
+
if (!process.stdin.isTTY) {
|
|
522
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup telegram --bot-token <token> --allowed-chats <chat_id[,chat_id]>`);
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
console.log(`${BRAND.productName} Telegram setup`);
|
|
526
|
+
console.log(`สร้าง bot ผ่าน @BotFather แล้ววาง token ที่นี่ (จะเก็บใน ${appHomePath('gateway', 'config.json')} chmod 600)`);
|
|
527
|
+
token = await askText('Telegram bot token: ');
|
|
528
|
+
}
|
|
529
|
+
if (!allowedRaw) {
|
|
530
|
+
if (!process.stdin.isTTY) {
|
|
531
|
+
console.error('ต้องระบุ --allowed-chats <chat_id[,chat_id]> เพื่อ fail-closed');
|
|
532
|
+
process.exit(1);
|
|
533
|
+
}
|
|
534
|
+
allowedRaw = await askText('Allowed private chat IDs (comma-separated): ');
|
|
535
|
+
}
|
|
536
|
+
const { parseAllowedChats } = await import('./gateway/telegram.js');
|
|
537
|
+
const allowedChatIds = parseAllowedChats(allowedRaw);
|
|
538
|
+
if (!token.trim() || !allowedChatIds.length) {
|
|
539
|
+
console.error('Telegram setup ต้องมี bot token และ allowed chat id อย่างน้อย 1 ค่า');
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
543
|
+
await patchGatewayConfig({
|
|
544
|
+
telegram: {
|
|
545
|
+
enabled: true,
|
|
546
|
+
botToken: token.trim(),
|
|
547
|
+
allowedChatIds,
|
|
548
|
+
allowWrite,
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
console.log(`บันทึก Telegram gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
552
|
+
console.log(`รัน: ${BRAND.cliName} gateway run`);
|
|
553
|
+
}
|
|
554
|
+
function parseStringCsv(raw) {
|
|
555
|
+
if (!raw)
|
|
556
|
+
return [];
|
|
557
|
+
return raw
|
|
558
|
+
.split(',')
|
|
559
|
+
.map((s) => s.trim())
|
|
560
|
+
.filter(Boolean);
|
|
561
|
+
}
|
|
562
|
+
async function runDiscordGatewaySetup(args) {
|
|
563
|
+
let token = argValue(args, '--bot-token', '--token');
|
|
564
|
+
let defaultChannel = argValue(args, '--channel', '--default-channel');
|
|
565
|
+
let allowedRaw = argValue(args, '--allowed-channels', '--channel-ids');
|
|
566
|
+
const allowWrite = args.includes('--allow-write');
|
|
567
|
+
if (!token) {
|
|
568
|
+
if (!process.stdin.isTTY) {
|
|
569
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup discord --bot-token <token> --channel <channel_id>`);
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
console.log(`${BRAND.productName} Discord setup`);
|
|
573
|
+
console.log('สร้าง bot ใน Discord Developer Portal, เปิด Message Content Intent, แล้ววาง Bot Token ที่นี่');
|
|
574
|
+
token = await askText('Discord bot token: ');
|
|
575
|
+
}
|
|
576
|
+
if (!defaultChannel && !allowedRaw) {
|
|
577
|
+
if (!process.stdin.isTTY) {
|
|
578
|
+
console.error('ต้องระบุ --channel <channel_id> หรือ --allowed-channels <id[,id]>');
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
defaultChannel = await askText('Default Discord channel ID: ');
|
|
582
|
+
}
|
|
583
|
+
const allowedChannelIds = parseStringCsv(allowedRaw ?? defaultChannel);
|
|
584
|
+
if (!token.trim() || !allowedChannelIds.length) {
|
|
585
|
+
console.error('Discord setup ต้องมี bot token และ channel id อย่างน้อย 1 ค่า');
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
589
|
+
await patchGatewayConfig({
|
|
590
|
+
discord: {
|
|
591
|
+
enabled: true,
|
|
592
|
+
botToken: token.trim(),
|
|
593
|
+
defaultChannelId: (defaultChannel ?? allowedChannelIds[0]).trim(),
|
|
594
|
+
allowedChannelIds,
|
|
595
|
+
allowWrite,
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
console.log(`บันทึก Discord gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
599
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to discord "hello"`);
|
|
600
|
+
}
|
|
601
|
+
async function runSlackGatewaySetup(args) {
|
|
602
|
+
let botToken = argValue(args, '--bot-token', '--token');
|
|
603
|
+
let appToken = argValue(args, '--app-token');
|
|
604
|
+
let defaultChannel = argValue(args, '--channel', '--default-channel');
|
|
605
|
+
let allowedRaw = argValue(args, '--allowed-channels', '--channel-ids');
|
|
606
|
+
const allowWrite = args.includes('--allow-write');
|
|
607
|
+
if (!botToken) {
|
|
608
|
+
if (!process.stdin.isTTY) {
|
|
609
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup slack --bot-token <xoxb-token> --app-token <xapp-token> --channel <channel_id>`);
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
console.log(`${BRAND.productName} Slack setup`);
|
|
613
|
+
console.log('สร้าง Slack app, เปิด Socket Mode, เพิ่ม scopes แล้ววาง Bot Token (xoxb-) ที่นี่');
|
|
614
|
+
botToken = await askText('Slack bot token (xoxb-): ');
|
|
615
|
+
}
|
|
616
|
+
if (!appToken && process.stdin.isTTY) {
|
|
617
|
+
appToken = await askText('Slack app token (xapp-, optional for outbound send but needed for gateway Socket Mode): ');
|
|
618
|
+
}
|
|
619
|
+
if (!defaultChannel && !allowedRaw) {
|
|
620
|
+
if (!process.stdin.isTTY) {
|
|
621
|
+
console.error('ต้องระบุ --channel <channel_id> หรือ --allowed-channels <id[,id]>');
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
defaultChannel = await askText('Default Slack channel ID: ');
|
|
625
|
+
}
|
|
626
|
+
const allowedChannelIds = parseStringCsv(allowedRaw ?? defaultChannel);
|
|
627
|
+
if (!botToken.trim() || !allowedChannelIds.length) {
|
|
628
|
+
console.error('Slack setup ต้องมี bot token และ channel id อย่างน้อย 1 ค่า');
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
632
|
+
await patchGatewayConfig({
|
|
633
|
+
slack: {
|
|
634
|
+
enabled: true,
|
|
635
|
+
botToken: botToken.trim(),
|
|
636
|
+
appToken: appToken?.trim() || undefined,
|
|
637
|
+
defaultChannelId: (defaultChannel ?? allowedChannelIds[0]).trim(),
|
|
638
|
+
allowedChannelIds,
|
|
639
|
+
allowWrite,
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
console.log(`บันทึก Slack gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
643
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to slack "hello"`);
|
|
644
|
+
}
|
|
645
|
+
async function runMattermostGatewaySetup(args) {
|
|
646
|
+
let serverUrl = argValue(args, '--url', '--server-url');
|
|
647
|
+
let token = argValue(args, '--token');
|
|
648
|
+
let homeChannel = argValue(args, '--home-channel', '--channel', '--to');
|
|
649
|
+
let allowedUsersRaw = argValue(args, '--allowed-users');
|
|
650
|
+
let allowedChannelsRaw = argValue(args, '--allowed-channels', '--channel-ids');
|
|
651
|
+
let freeResponseChannelsRaw = argValue(args, '--free-response-channels');
|
|
652
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
653
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
654
|
+
const requireMention = !args.includes('--no-require-mention');
|
|
655
|
+
const groupSessionsPerUser = !args.includes('--shared-channel-session');
|
|
656
|
+
const replyMode = args.includes('--thread-replies') || argValue(args, '--reply-mode') === 'thread' ? 'thread' : 'off';
|
|
657
|
+
if (!serverUrl) {
|
|
658
|
+
if (!process.stdin.isTTY) {
|
|
659
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup mattermost --url <https://mm.example.com> --token <token> --allowed-users <user_id[,user_id]> --home-channel <channel_id>`);
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
console.log(`${BRAND.productName} Mattermost setup`);
|
|
663
|
+
console.log('ใช้ Mattermost REST API v4 + WebSocket; แนะนำ token ของ dedicated bot account');
|
|
664
|
+
serverUrl = await askText('Mattermost server URL (เช่น https://mm.example.com): ');
|
|
665
|
+
}
|
|
666
|
+
if (!token) {
|
|
667
|
+
if (!process.stdin.isTTY) {
|
|
668
|
+
console.error('ต้องระบุ --token <Mattermost personal/bot access token>');
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
token = await askText('Mattermost token: ');
|
|
672
|
+
}
|
|
673
|
+
if (!allowedUsersRaw && !allowAllUsers) {
|
|
674
|
+
if (!process.stdin.isTTY) {
|
|
675
|
+
console.error('ต้องระบุ --allowed-users <user_id[,user_id]> เพื่อ fail-closed หรือ --allow-all-users');
|
|
676
|
+
process.exit(1);
|
|
677
|
+
}
|
|
678
|
+
allowedUsersRaw = await askText('Allowed Mattermost user IDs (comma-separated): ');
|
|
679
|
+
}
|
|
680
|
+
if (!homeChannel && !allowedChannelsRaw) {
|
|
681
|
+
if (!process.stdin.isTTY) {
|
|
682
|
+
console.error('ต้องระบุ --home-channel <channel_id> หรือ --allowed-channels <channel_id[,id]> เพื่อส่งข้อความออกแบบ fail-closed');
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
homeChannel = await askText('Mattermost home channel ID (blank = skip outbound home): ');
|
|
686
|
+
}
|
|
687
|
+
const { normalizeMattermostUrl } = await import('./gateway/mattermost.js');
|
|
688
|
+
const cleanServerUrl = normalizeMattermostUrl(serverUrl);
|
|
689
|
+
const cleanHome = homeChannel?.trim();
|
|
690
|
+
const allowedUsers = parseStringCsv(allowedUsersRaw);
|
|
691
|
+
const allowedChannels = parseStringCsv(allowedChannelsRaw ?? cleanHome);
|
|
692
|
+
const freeResponseChannels = parseStringCsv(freeResponseChannelsRaw);
|
|
693
|
+
if (!cleanServerUrl) {
|
|
694
|
+
console.error('Mattermost setup ต้องมี server URL ที่ขึ้นต้นด้วย http:// หรือ https://');
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
if (!token.trim()) {
|
|
698
|
+
console.error('Mattermost setup ต้องมี token');
|
|
699
|
+
process.exit(1);
|
|
700
|
+
}
|
|
701
|
+
if (!allowAllUsers && !allowedUsers.length) {
|
|
702
|
+
console.error('Mattermost setup ต้องมี allowed users อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
703
|
+
process.exit(1);
|
|
704
|
+
}
|
|
705
|
+
if (!cleanHome && !allowedChannels.length) {
|
|
706
|
+
console.error('Mattermost setup ต้องมี home channel/allowed channels อย่างน้อย 1 ค่า');
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
710
|
+
await patchGatewayConfig({
|
|
711
|
+
mattermost: {
|
|
712
|
+
enabled: true,
|
|
713
|
+
serverUrl: cleanServerUrl,
|
|
714
|
+
token: token.trim(),
|
|
715
|
+
homeChannel: cleanHome || allowedChannels[0],
|
|
716
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
717
|
+
allowedUsers,
|
|
718
|
+
allowedChannels,
|
|
719
|
+
freeResponseChannels,
|
|
720
|
+
allowAllUsers,
|
|
721
|
+
requireMention,
|
|
722
|
+
groupSessionsPerUser,
|
|
723
|
+
replyMode,
|
|
724
|
+
},
|
|
725
|
+
});
|
|
726
|
+
console.log(`บันทึก Mattermost gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
727
|
+
console.log(`Mattermost websocket: ${cleanServerUrl}/api/v4/websocket`);
|
|
728
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to mattermost "hello"`);
|
|
729
|
+
}
|
|
730
|
+
async function runHomeAssistantGatewaySetup(args) {
|
|
731
|
+
const url = argValue(args, '--url')?.trim() || 'http://homeassistant.local:8123';
|
|
732
|
+
let token = argValue(args, '--token');
|
|
733
|
+
let homeChannel = argValue(args, '--home-channel', '--notification-id', '--to');
|
|
734
|
+
let watchDomainsRaw = argValue(args, '--watch-domains', '--domains');
|
|
735
|
+
let watchEntitiesRaw = argValue(args, '--watch-entities', '--entities');
|
|
736
|
+
let ignoreEntitiesRaw = argValue(args, '--ignore-entities');
|
|
737
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
738
|
+
const watchAll = args.includes('--watch-all');
|
|
739
|
+
const cooldownSecondsRaw = argValue(args, '--cooldown-seconds', '--cooldown');
|
|
740
|
+
if (!token) {
|
|
741
|
+
if (!process.stdin.isTTY) {
|
|
742
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup homeassistant --token <long-lived-token> [--url http://homeassistant.local:8123] --watch-domains climate,binary_sensor`);
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
console.log(`${BRAND.productName} Home Assistant setup`);
|
|
746
|
+
console.log('สร้าง Long-Lived Access Token จาก Home Assistant Profile แล้ววาง token ที่นี่');
|
|
747
|
+
token = await askText('Home Assistant long-lived access token: ');
|
|
748
|
+
}
|
|
749
|
+
if (!homeChannel && process.stdin.isTTY) {
|
|
750
|
+
homeChannel = (await askText('Persistent notification id (blank = sanook_agent): ')) || 'sanook_agent';
|
|
751
|
+
}
|
|
752
|
+
if (!watchDomainsRaw && !watchEntitiesRaw && !watchAll) {
|
|
753
|
+
if (!process.stdin.isTTY) {
|
|
754
|
+
console.error('ต้องระบุ --watch-domains, --watch-entities หรือ --watch-all เพื่อรับ state_changed events');
|
|
755
|
+
process.exit(1);
|
|
756
|
+
}
|
|
757
|
+
watchDomainsRaw = await askText('Watch domains (comma-separated; เช่น climate,binary_sensor,alarm_control_panel): ');
|
|
758
|
+
}
|
|
759
|
+
const { homeAssistantWebSocketUrl, normalizeHomeAssistantUrl } = await import('./gateway/homeassistant.js');
|
|
760
|
+
const cleanUrl = normalizeHomeAssistantUrl(url);
|
|
761
|
+
const watchDomains = parseStringCsv(watchDomainsRaw);
|
|
762
|
+
const watchEntities = parseStringCsv(watchEntitiesRaw);
|
|
763
|
+
const ignoreEntities = parseStringCsv(ignoreEntitiesRaw);
|
|
764
|
+
const cooldownSeconds = cooldownSecondsRaw ? Number(cooldownSecondsRaw) : undefined;
|
|
765
|
+
if (!cleanUrl) {
|
|
766
|
+
console.error('Home Assistant setup ต้องมี URL ที่ขึ้นต้นด้วย http:// หรือ https://');
|
|
767
|
+
process.exit(1);
|
|
768
|
+
}
|
|
769
|
+
if (!token.trim()) {
|
|
770
|
+
console.error('Home Assistant setup ต้องมี token');
|
|
771
|
+
process.exit(1);
|
|
772
|
+
}
|
|
773
|
+
if (!watchAll && !watchDomains.length && !watchEntities.length) {
|
|
774
|
+
console.error('Home Assistant setup ต้องมี watch domains/entities อย่างน้อย 1 ค่า หรือระบุ --watch-all');
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
|
777
|
+
if (cooldownSecondsRaw && (!Number.isInteger(cooldownSeconds) || Number(cooldownSeconds) <= 0)) {
|
|
778
|
+
console.error('--cooldown-seconds ต้องเป็น integer มากกว่า 0');
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
782
|
+
await patchGatewayConfig({
|
|
783
|
+
homeassistant: {
|
|
784
|
+
enabled: true,
|
|
785
|
+
url: cleanUrl,
|
|
786
|
+
token: token.trim(),
|
|
787
|
+
homeChannel: homeChannel?.trim() || 'sanook_agent',
|
|
788
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
789
|
+
watchDomains,
|
|
790
|
+
watchEntities,
|
|
791
|
+
ignoreEntities,
|
|
792
|
+
watchAll,
|
|
793
|
+
cooldownSeconds,
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
console.log(`บันทึก Home Assistant gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
797
|
+
console.log(`Home Assistant websocket: ${homeAssistantWebSocketUrl(cleanUrl)}`);
|
|
798
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to homeassistant "hello"`);
|
|
799
|
+
}
|
|
800
|
+
async function runLineGatewaySetup(args) {
|
|
801
|
+
let channelAccessToken = argValue(args, '--channel-access-token', '--access-token', '--token');
|
|
802
|
+
let channelSecret = argValue(args, '--channel-secret', '--secret');
|
|
803
|
+
let homeChannel = argValue(args, '--home-channel', '--to');
|
|
804
|
+
let allowedUsersRaw = argValue(args, '--allowed-users');
|
|
805
|
+
let allowedGroupsRaw = argValue(args, '--allowed-groups');
|
|
806
|
+
let allowedRoomsRaw = argValue(args, '--allowed-rooms');
|
|
807
|
+
const publicUrl = argValue(args, '--public-url');
|
|
808
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
809
|
+
if (!channelAccessToken) {
|
|
810
|
+
if (!process.stdin.isTTY) {
|
|
811
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup line --channel-access-token <token> --home-channel <U/C/R-id>`);
|
|
812
|
+
process.exit(1);
|
|
813
|
+
}
|
|
814
|
+
console.log(`${BRAND.productName} LINE setup`);
|
|
815
|
+
console.log('สร้าง LINE Messaging API channel แล้ววาง long-lived Channel access token ที่นี่');
|
|
816
|
+
channelAccessToken = await askText('LINE channel access token: ');
|
|
817
|
+
}
|
|
818
|
+
if (!channelSecret && process.stdin.isTTY) {
|
|
819
|
+
channelSecret = await askText('LINE channel secret (needed for webhook replies): ');
|
|
820
|
+
}
|
|
821
|
+
if (!homeChannel && !allowedUsersRaw && !allowedGroupsRaw && !allowedRoomsRaw && !allowAllUsers) {
|
|
822
|
+
if (!process.stdin.isTTY) {
|
|
823
|
+
console.error('ต้องระบุ --home-channel <U/C/R-id> หรือ allowed list อย่างน้อยหนึ่งชุด เพื่อ fail-closed');
|
|
824
|
+
process.exit(1);
|
|
825
|
+
}
|
|
826
|
+
homeChannel = await askText('LINE home channel ID (U user / C group / R room): ');
|
|
827
|
+
}
|
|
828
|
+
const home = homeChannel?.trim();
|
|
829
|
+
const allowedUsers = parseStringCsv(allowedUsersRaw);
|
|
830
|
+
const allowedGroups = parseStringCsv(allowedGroupsRaw);
|
|
831
|
+
const allowedRooms = parseStringCsv(allowedRoomsRaw);
|
|
832
|
+
if (!allowAllUsers && !home && !allowedUsers.length && !allowedGroups.length && !allowedRooms.length) {
|
|
833
|
+
console.error('LINE setup ต้องมี home channel/allowlist อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
if (!channelAccessToken.trim()) {
|
|
837
|
+
console.error('LINE setup ต้องมี channel access token');
|
|
838
|
+
process.exit(1);
|
|
839
|
+
}
|
|
840
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
841
|
+
await patchGatewayConfig({
|
|
842
|
+
line: {
|
|
843
|
+
enabled: true,
|
|
844
|
+
channelAccessToken: channelAccessToken.trim(),
|
|
845
|
+
channelSecret: channelSecret?.trim() || undefined,
|
|
846
|
+
homeChannel: home || allowedUsers[0] || allowedGroups[0] || allowedRooms[0],
|
|
847
|
+
allowedUsers,
|
|
848
|
+
allowedGroups,
|
|
849
|
+
allowedRooms,
|
|
850
|
+
allowAllUsers,
|
|
851
|
+
publicUrl: publicUrl?.trim() || undefined,
|
|
852
|
+
},
|
|
853
|
+
});
|
|
854
|
+
console.log(`บันทึก LINE gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
855
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to line "hello"`);
|
|
856
|
+
}
|
|
857
|
+
async function runSmsGatewaySetup(args) {
|
|
858
|
+
let accountSid = argValue(args, '--account-sid', '--sid');
|
|
859
|
+
let authToken = argValue(args, '--auth-token', '--token');
|
|
860
|
+
let phoneNumber = argValue(args, '--phone-number', '--from');
|
|
861
|
+
let homeChannel = argValue(args, '--home-channel', '--to');
|
|
862
|
+
let allowedRaw = argValue(args, '--allowed-users', '--allowed-numbers');
|
|
863
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
864
|
+
const webhookUrl = argValue(args, '--webhook-url');
|
|
865
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
866
|
+
const insecureNoSignature = args.includes('--insecure-no-signature');
|
|
867
|
+
if (!accountSid) {
|
|
868
|
+
if (!process.stdin.isTTY) {
|
|
869
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup sms --account-sid <AC...> --auth-token <token> --phone-number <+1555...> --home-channel <+1555...> --webhook-url <https://.../sms/webhook>`);
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
console.log(`${BRAND.productName} SMS/Twilio setup`);
|
|
873
|
+
console.log('ใช้ Twilio Programmable Messaging; inbound webhook ต้องตั้ง URL เดียวกันใน Twilio Console');
|
|
874
|
+
accountSid = await askText('Twilio Account SID: ');
|
|
875
|
+
}
|
|
876
|
+
if (!authToken) {
|
|
877
|
+
if (!process.stdin.isTTY) {
|
|
878
|
+
console.error('ต้องระบุ --auth-token <token>');
|
|
879
|
+
process.exit(1);
|
|
880
|
+
}
|
|
881
|
+
authToken = await askText('Twilio Auth Token: ');
|
|
882
|
+
}
|
|
883
|
+
if (!phoneNumber) {
|
|
884
|
+
if (!process.stdin.isTTY) {
|
|
885
|
+
console.error('ต้องระบุ --phone-number <E.164 Twilio number>');
|
|
886
|
+
process.exit(1);
|
|
887
|
+
}
|
|
888
|
+
phoneNumber = await askText('Twilio phone number (+1555...): ');
|
|
889
|
+
}
|
|
890
|
+
if (!homeChannel && !allowedRaw && !allowAllUsers) {
|
|
891
|
+
if (!process.stdin.isTTY) {
|
|
892
|
+
console.error('ต้องระบุ --home-channel <phone> หรือ --allowed-users <phone[,phone]> เพื่อ fail-closed');
|
|
893
|
+
process.exit(1);
|
|
894
|
+
}
|
|
895
|
+
homeChannel = await askText('Home/allowed phone number (+1555...): ');
|
|
896
|
+
}
|
|
897
|
+
const { normalizeSmsPhone } = await import('./gateway/sms.js');
|
|
898
|
+
const from = normalizeSmsPhone(phoneNumber);
|
|
899
|
+
const home = normalizeSmsPhone(homeChannel);
|
|
900
|
+
const allowedUsers = parseStringCsv(allowedRaw ?? home).map((phone) => normalizeSmsPhone(phone)).filter((phone) => Boolean(phone));
|
|
901
|
+
if (!accountSid.trim() || !authToken.trim() || !from) {
|
|
902
|
+
console.error('SMS setup ต้องมี account sid, auth token และ Twilio phone number');
|
|
903
|
+
process.exit(1);
|
|
904
|
+
}
|
|
905
|
+
if (!allowAllUsers && !home && !allowedUsers.length) {
|
|
906
|
+
console.error('SMS setup ต้องมี home channel/allowlist อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
907
|
+
process.exit(1);
|
|
908
|
+
}
|
|
909
|
+
if (!webhookUrl && !insecureNoSignature) {
|
|
910
|
+
if (!process.stdin.isTTY) {
|
|
911
|
+
console.error('ต้องระบุ --webhook-url <https://.../sms/webhook> เพื่อ verify Twilio signature หรือ --insecure-no-signature สำหรับ local dev');
|
|
912
|
+
process.exit(1);
|
|
913
|
+
}
|
|
914
|
+
console.log('ยังไม่ได้ตั้ง webhook URL; inbound SMS จะไม่เริ่มจนกว่าจะตั้ง SMS_WEBHOOK_URL หรือรัน setup ใหม่พร้อม --webhook-url');
|
|
915
|
+
}
|
|
916
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
917
|
+
await patchGatewayConfig({
|
|
918
|
+
sms: {
|
|
919
|
+
enabled: true,
|
|
920
|
+
accountSid: accountSid.trim(),
|
|
921
|
+
authToken: authToken.trim(),
|
|
922
|
+
phoneNumber: from,
|
|
923
|
+
homeChannel: home || allowedUsers[0],
|
|
924
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
925
|
+
allowedUsers,
|
|
926
|
+
allowAllUsers,
|
|
927
|
+
webhookUrl: webhookUrl?.trim() || undefined,
|
|
928
|
+
insecureNoSignature,
|
|
929
|
+
},
|
|
930
|
+
});
|
|
931
|
+
console.log(`บันทึก SMS/Twilio gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
932
|
+
console.log(`ตั้ง Twilio webhook เป็น: ${webhookUrl?.trim() || `http://127.0.0.1:<port>/sms/webhook`}`);
|
|
933
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to sms "hello"`);
|
|
934
|
+
}
|
|
935
|
+
async function runSignalGatewaySetup(args) {
|
|
936
|
+
const httpUrl = argValue(args, '--http-url', '--url')?.trim() || 'http://127.0.0.1:8080';
|
|
937
|
+
let account = argValue(args, '--account', '--phone-number');
|
|
938
|
+
let homeChannel = argValue(args, '--home-channel', '--to');
|
|
939
|
+
let allowedRaw = argValue(args, '--allowed-users', '--allowed-numbers');
|
|
940
|
+
let groupAllowedRaw = argValue(args, '--group-allowed-users', '--allowed-groups');
|
|
941
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
942
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
943
|
+
const requireMention = args.includes('--require-mention');
|
|
944
|
+
if (!account) {
|
|
945
|
+
if (!process.stdin.isTTY) {
|
|
946
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup signal --account <+1555...> --home-channel <+1555...> [--http-url http://127.0.0.1:8080]`);
|
|
947
|
+
process.exit(1);
|
|
948
|
+
}
|
|
949
|
+
console.log(`${BRAND.productName} Signal setup`);
|
|
950
|
+
console.log('ต้องมี signal-cli daemon --http รันอยู่; Sanook ใช้ JSON-RPC /api/v1/rpc และ SSE /api/v1/events');
|
|
951
|
+
account = await askText('Signal account (+E.164): ');
|
|
952
|
+
}
|
|
953
|
+
if (!homeChannel && !allowedRaw && !groupAllowedRaw && !allowAllUsers) {
|
|
954
|
+
if (process.stdin.isTTY) {
|
|
955
|
+
homeChannel = await askText('Signal home/allowed user (+E.164 หรือ UUID; blank = account/Note to Self): ');
|
|
956
|
+
}
|
|
957
|
+
if (!homeChannel)
|
|
958
|
+
homeChannel = account;
|
|
959
|
+
}
|
|
960
|
+
if (!allowedRaw && !allowAllUsers && homeChannel && !homeChannel.trim().toLowerCase().startsWith('group:')) {
|
|
961
|
+
allowedRaw = homeChannel;
|
|
962
|
+
}
|
|
963
|
+
if (!groupAllowedRaw && homeChannel?.trim().toLowerCase().startsWith('group:')) {
|
|
964
|
+
groupAllowedRaw = homeChannel;
|
|
965
|
+
}
|
|
966
|
+
const { normalizeSignalId } = await import('./gateway/signal.js');
|
|
967
|
+
const cleanAccount = normalizeSignalId(account);
|
|
968
|
+
const cleanHome = normalizeSignalId(homeChannel);
|
|
969
|
+
const allowedUsers = parseStringCsv(allowedRaw).map(normalizeSignalId).filter((id) => Boolean(id));
|
|
970
|
+
const groupAllowedUsers = parseStringCsv(groupAllowedRaw)
|
|
971
|
+
.map((id) => {
|
|
972
|
+
if (id.trim() === '*')
|
|
973
|
+
return '*';
|
|
974
|
+
const normalized = normalizeSignalId(id);
|
|
975
|
+
return normalized?.startsWith('group:') ? normalized : normalized ? `group:${normalized}` : undefined;
|
|
976
|
+
})
|
|
977
|
+
.filter((id) => Boolean(id));
|
|
978
|
+
if (!cleanAccount) {
|
|
979
|
+
console.error('Signal setup ต้องมี account (+E.164 หรือ account id)');
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
if (!allowAllUsers && !cleanHome && !allowedUsers.length && !groupAllowedUsers.length) {
|
|
983
|
+
console.error('Signal setup ต้องมี home channel/allowlist อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
984
|
+
process.exit(1);
|
|
985
|
+
}
|
|
986
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
987
|
+
await patchGatewayConfig({
|
|
988
|
+
signal: {
|
|
989
|
+
enabled: true,
|
|
990
|
+
httpUrl,
|
|
991
|
+
account: cleanAccount,
|
|
992
|
+
homeChannel: cleanHome || allowedUsers[0] || groupAllowedUsers[0] || cleanAccount,
|
|
993
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
994
|
+
allowedUsers,
|
|
995
|
+
groupAllowedUsers,
|
|
996
|
+
allowAllUsers,
|
|
997
|
+
requireMention,
|
|
998
|
+
},
|
|
999
|
+
});
|
|
1000
|
+
console.log(`บันทึก Signal gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1001
|
+
console.log(`ตรวจ signal-cli daemon: ${httpUrl}/api/v1/check`);
|
|
1002
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to signal "hello"`);
|
|
1003
|
+
}
|
|
1004
|
+
async function runWhatsAppGatewaySetup(args) {
|
|
1005
|
+
let phoneNumberId = argValue(args, '--phone-number-id', '--phone-id');
|
|
1006
|
+
let accessToken = argValue(args, '--access-token', '--token');
|
|
1007
|
+
let appSecret = argValue(args, '--app-secret', '--secret');
|
|
1008
|
+
let verifyToken = argValue(args, '--verify-token');
|
|
1009
|
+
let homeChannel = argValue(args, '--home-channel', '--to');
|
|
1010
|
+
let allowedRaw = argValue(args, '--allowed-users', '--allowed-numbers');
|
|
1011
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
1012
|
+
const publicUrl = argValue(args, '--public-url');
|
|
1013
|
+
const apiVersion = argValue(args, '--api-version');
|
|
1014
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1015
|
+
if (!phoneNumberId) {
|
|
1016
|
+
if (!process.stdin.isTTY) {
|
|
1017
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup whatsapp --phone-number-id <id> --access-token <EAA...> --app-secret <secret> --home-channel <wa_id>`);
|
|
1018
|
+
process.exit(1);
|
|
1019
|
+
}
|
|
1020
|
+
console.log(`${BRAND.productName} WhatsApp Cloud setup`);
|
|
1021
|
+
console.log('ใช้ Meta WhatsApp Business Cloud API: ต้องมี Phone Number ID, Access Token, App Secret และ public HTTPS webhook URL');
|
|
1022
|
+
phoneNumberId = await askText('WhatsApp Phone Number ID (ตัวเลขจาก Meta API Setup ไม่ใช่เบอร์โทร): ');
|
|
1023
|
+
}
|
|
1024
|
+
if (!accessToken) {
|
|
1025
|
+
if (!process.stdin.isTTY) {
|
|
1026
|
+
console.error('ต้องระบุ --access-token <Meta WhatsApp Cloud token>');
|
|
1027
|
+
process.exit(1);
|
|
1028
|
+
}
|
|
1029
|
+
accessToken = await askText('WhatsApp Cloud access token: ');
|
|
1030
|
+
}
|
|
1031
|
+
if (!appSecret) {
|
|
1032
|
+
if (!process.stdin.isTTY) {
|
|
1033
|
+
console.error('ต้องระบุ --app-secret <Meta app secret> เพื่อ verify X-Hub-Signature-256');
|
|
1034
|
+
process.exit(1);
|
|
1035
|
+
}
|
|
1036
|
+
appSecret = await askText('Meta app secret (Settings > Basic): ');
|
|
1037
|
+
}
|
|
1038
|
+
if (!homeChannel && !allowedRaw && !allowAllUsers) {
|
|
1039
|
+
if (!process.stdin.isTTY) {
|
|
1040
|
+
console.error('ต้องระบุ --home-channel <wa_id> หรือ --allowed-users <wa_id[,wa_id]> เพื่อ fail-closed');
|
|
1041
|
+
process.exit(1);
|
|
1042
|
+
}
|
|
1043
|
+
homeChannel = await askText('WhatsApp home/allowed wa_id (country code, no +): ');
|
|
1044
|
+
}
|
|
1045
|
+
const { randomBytes } = await import('node:crypto');
|
|
1046
|
+
const { normalizeWhatsAppId } = await import('./gateway/whatsapp.js');
|
|
1047
|
+
const cleanPhoneNumberId = phoneNumberId.trim();
|
|
1048
|
+
const cleanHome = normalizeWhatsAppId(homeChannel);
|
|
1049
|
+
const allowedUsers = parseStringCsv(allowedRaw ?? cleanHome).map(normalizeWhatsAppId).filter((id) => Boolean(id));
|
|
1050
|
+
if (!verifyToken)
|
|
1051
|
+
verifyToken = randomBytes(24).toString('base64url');
|
|
1052
|
+
if (!cleanPhoneNumberId || !accessToken.trim() || !appSecret.trim()) {
|
|
1053
|
+
console.error('WhatsApp setup ต้องมี phone number id, access token และ app secret');
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
1056
|
+
if (!allowAllUsers && !cleanHome && !allowedUsers.length) {
|
|
1057
|
+
console.error('WhatsApp setup ต้องมี home channel/allowlist อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
1058
|
+
process.exit(1);
|
|
1059
|
+
}
|
|
1060
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1061
|
+
await patchGatewayConfig({
|
|
1062
|
+
whatsapp: {
|
|
1063
|
+
enabled: true,
|
|
1064
|
+
phoneNumberId: cleanPhoneNumberId,
|
|
1065
|
+
accessToken: accessToken.trim(),
|
|
1066
|
+
appSecret: appSecret.trim(),
|
|
1067
|
+
verifyToken: verifyToken.trim(),
|
|
1068
|
+
homeChannel: cleanHome || allowedUsers[0],
|
|
1069
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
1070
|
+
allowedUsers,
|
|
1071
|
+
allowAllUsers,
|
|
1072
|
+
publicUrl: publicUrl?.trim() || undefined,
|
|
1073
|
+
apiVersion: apiVersion?.trim() || undefined,
|
|
1074
|
+
},
|
|
1075
|
+
});
|
|
1076
|
+
const callback = publicUrl?.trim() ? `${publicUrl.trim().replace(/\/+$/, '')}/whatsapp/webhook` : `https://<your-tunnel>/whatsapp/webhook`;
|
|
1077
|
+
console.log(`บันทึก WhatsApp Cloud gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1078
|
+
console.log(`Meta webhook callback URL: ${callback}`);
|
|
1079
|
+
console.log(`Meta verify token: ${verifyToken.trim()}`);
|
|
1080
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to whatsapp "hello"`);
|
|
1081
|
+
}
|
|
1082
|
+
async function runMatrixGatewaySetup(args) {
|
|
1083
|
+
let homeserver = argValue(args, '--homeserver', '--server', '--url');
|
|
1084
|
+
let accessToken = argValue(args, '--access-token', '--token');
|
|
1085
|
+
let userId = argValue(args, '--user-id', '--user');
|
|
1086
|
+
let password = argValue(args, '--password');
|
|
1087
|
+
let homeRoom = argValue(args, '--home-room', '--room', '--to');
|
|
1088
|
+
let allowedUsersRaw = argValue(args, '--allowed-users');
|
|
1089
|
+
let allowedRoomsRaw = argValue(args, '--allowed-rooms');
|
|
1090
|
+
let freeResponseRoomsRaw = argValue(args, '--free-response-rooms');
|
|
1091
|
+
const homeRoomName = argValue(args, '--home-room-name');
|
|
1092
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1093
|
+
const requireMention = !args.includes('--no-require-mention');
|
|
1094
|
+
const groupSessionsPerUser = !args.includes('--shared-room-session');
|
|
1095
|
+
const autoJoin = !args.includes('--no-auto-join');
|
|
1096
|
+
const pollTimeoutMs = argValue(args, '--poll-timeout-ms');
|
|
1097
|
+
if (!homeserver) {
|
|
1098
|
+
if (!process.stdin.isTTY) {
|
|
1099
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup matrix --homeserver <https://matrix.org> --access-token <token> --allowed-users <@you:server> [--home-room '!room:server']`);
|
|
1100
|
+
process.exit(1);
|
|
1101
|
+
}
|
|
1102
|
+
console.log(`${BRAND.productName} Matrix setup`);
|
|
1103
|
+
console.log('ใช้ Matrix Client-Server API: ต้องมี homeserver URL และ access token หรือ user/password ของ bot account');
|
|
1104
|
+
homeserver = await askText('Matrix homeserver URL (เช่น https://matrix.org): ');
|
|
1105
|
+
}
|
|
1106
|
+
if (!accessToken && (!userId || !password)) {
|
|
1107
|
+
if (!process.stdin.isTTY) {
|
|
1108
|
+
console.error('ต้องระบุ --access-token <token> หรือ --user-id <@bot:server> --password <password>');
|
|
1109
|
+
process.exit(1);
|
|
1110
|
+
}
|
|
1111
|
+
accessToken = await askText('Matrix access token (แนะนำ; blank = ใช้ user/password): ');
|
|
1112
|
+
if (!accessToken) {
|
|
1113
|
+
userId = await askText('Matrix bot user id (@bot:server): ');
|
|
1114
|
+
password = await askText('Matrix bot password: ');
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
if (!allowedUsersRaw && !allowAllUsers) {
|
|
1118
|
+
if (!process.stdin.isTTY) {
|
|
1119
|
+
console.error('ต้องระบุ --allowed-users <@user:server[,user]> เพื่อ fail-closed หรือ --allow-all-users');
|
|
1120
|
+
process.exit(1);
|
|
1121
|
+
}
|
|
1122
|
+
allowedUsersRaw = await askText('Allowed Matrix user IDs (comma-separated): ');
|
|
1123
|
+
}
|
|
1124
|
+
if (!homeRoom && process.stdin.isTTY) {
|
|
1125
|
+
homeRoom = await askText('Matrix home room id/alias (!room:server หรือ #room:server; blank = skip): ');
|
|
1126
|
+
}
|
|
1127
|
+
const { normalizeMatrixHomeserver, normalizeMatrixRoomId, normalizeMatrixUserId } = await import('./gateway/matrix.js');
|
|
1128
|
+
const cleanHomeserver = normalizeMatrixHomeserver(homeserver);
|
|
1129
|
+
const cleanUserId = normalizeMatrixUserId(userId);
|
|
1130
|
+
const cleanHomeRoom = normalizeMatrixRoomId(homeRoom);
|
|
1131
|
+
const allowedUsers = parseStringCsv(allowedUsersRaw).map(normalizeMatrixUserId).filter((id) => Boolean(id));
|
|
1132
|
+
const allowedRooms = parseStringCsv(allowedRoomsRaw).map(normalizeMatrixRoomId).filter((id) => Boolean(id));
|
|
1133
|
+
const freeResponseRooms = parseStringCsv(freeResponseRoomsRaw).map(normalizeMatrixRoomId).filter((id) => Boolean(id));
|
|
1134
|
+
const timeout = pollTimeoutMs ? Number(pollTimeoutMs) : undefined;
|
|
1135
|
+
if (!cleanHomeserver) {
|
|
1136
|
+
console.error('Matrix setup ต้องมี homeserver URL ที่ขึ้นต้นด้วย http:// หรือ https://');
|
|
1137
|
+
process.exit(1);
|
|
1138
|
+
}
|
|
1139
|
+
if (!accessToken?.trim() && (!cleanUserId || !password?.trim())) {
|
|
1140
|
+
console.error('Matrix setup ต้องมี access token หรือ user id/password');
|
|
1141
|
+
process.exit(1);
|
|
1142
|
+
}
|
|
1143
|
+
if (!allowAllUsers && !allowedUsers.length) {
|
|
1144
|
+
console.error('Matrix setup ต้องมี allowed users อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
1145
|
+
process.exit(1);
|
|
1146
|
+
}
|
|
1147
|
+
if (homeRoom?.trim() && !cleanHomeRoom) {
|
|
1148
|
+
console.error('Matrix home room ต้องเป็น room id/alias เช่น !abc123:matrix.org หรือ #room:matrix.org');
|
|
1149
|
+
process.exit(1);
|
|
1150
|
+
}
|
|
1151
|
+
if (pollTimeoutMs && (!Number.isInteger(timeout) || Number(timeout) <= 0)) {
|
|
1152
|
+
console.error('--poll-timeout-ms ต้องเป็น integer มากกว่า 0');
|
|
1153
|
+
process.exit(1);
|
|
1154
|
+
}
|
|
1155
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1156
|
+
await patchGatewayConfig({
|
|
1157
|
+
matrix: {
|
|
1158
|
+
enabled: true,
|
|
1159
|
+
homeserver: cleanHomeserver,
|
|
1160
|
+
accessToken: accessToken?.trim() || undefined,
|
|
1161
|
+
userId: cleanUserId,
|
|
1162
|
+
password: password?.trim() || undefined,
|
|
1163
|
+
homeRoom: cleanHomeRoom || allowedRooms[0],
|
|
1164
|
+
homeRoomName: homeRoomName?.trim() || undefined,
|
|
1165
|
+
allowedUsers,
|
|
1166
|
+
allowedRooms,
|
|
1167
|
+
freeResponseRooms,
|
|
1168
|
+
allowAllUsers,
|
|
1169
|
+
requireMention,
|
|
1170
|
+
groupSessionsPerUser,
|
|
1171
|
+
autoJoin,
|
|
1172
|
+
pollTimeoutMs: timeout,
|
|
1173
|
+
},
|
|
1174
|
+
});
|
|
1175
|
+
console.log(`บันทึก Matrix gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1176
|
+
console.log(`Matrix sync: ${cleanHomeserver}/_matrix/client/v3/sync`);
|
|
1177
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to matrix "hello"${cleanHomeRoom ? '' : ` หรือ ${BRAND.cliName} send --to matrix:!room:server "hello"`}`);
|
|
1178
|
+
}
|
|
1179
|
+
async function runGoogleChatGatewaySetup(args) {
|
|
1180
|
+
const projectId = argValue(args, '--project-id');
|
|
1181
|
+
const subscriptionName = argValue(args, '--subscription-name', '--subscription');
|
|
1182
|
+
let serviceAccountJson = argValue(args, '--service-account-json', '--service-account', '--credentials');
|
|
1183
|
+
const apiBaseUrl = argValue(args, '--api-base-url', '--base-url');
|
|
1184
|
+
let incomingWebhookUrl = argValue(args, '--incoming-webhook-url', '--webhook-url', '--url');
|
|
1185
|
+
let homeChannel = argValue(args, '--home-channel', '--space', '--to');
|
|
1186
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
1187
|
+
const allowedUsersRaw = argValue(args, '--allowed-users');
|
|
1188
|
+
const allowedSpacesRaw = argValue(args, '--allowed-spaces', '--spaces');
|
|
1189
|
+
const freeResponseSpacesRaw = argValue(args, '--free-response-spaces');
|
|
1190
|
+
const maxMessagesRaw = argValue(args, '--max-messages');
|
|
1191
|
+
const maxBytesRaw = argValue(args, '--max-bytes');
|
|
1192
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1193
|
+
const allowAllSpaces = args.includes('--allow-all-spaces');
|
|
1194
|
+
if ((!incomingWebhookUrl && !serviceAccountJson) || (!incomingWebhookUrl && !homeChannel && !allowedSpacesRaw && !allowAllSpaces)) {
|
|
1195
|
+
if (!process.stdin.isTTY) {
|
|
1196
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup googlechat --service-account-json <path> --home-channel <spaces/AAA> หรือ --incoming-webhook-url <https://chat.googleapis.com/v1/spaces/.../messages?...>`);
|
|
1197
|
+
process.exit(1);
|
|
1198
|
+
}
|
|
1199
|
+
console.log(`${BRAND.productName} Google Chat setup`);
|
|
1200
|
+
console.log('ใช้ Service Account JSON + Chat REST API สำหรับ bot app หรือ incoming webhook URL สำหรับส่งง่าย ๆ');
|
|
1201
|
+
serviceAccountJson ||= await askText('Service Account JSON path (blank = webhook mode): ');
|
|
1202
|
+
if (serviceAccountJson) {
|
|
1203
|
+
homeChannel ||= await askText('Home space (spaces/AAA...; blank = skip): ');
|
|
1204
|
+
}
|
|
1205
|
+
else {
|
|
1206
|
+
incomingWebhookUrl ||= await askText('Google Chat incoming webhook URL: ');
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
const { normalizeGoogleChatApiBaseUrl, normalizeGoogleChatWebhookUrl, parseGoogleChatTarget } = await import('./gateway/googlechat.js');
|
|
1210
|
+
const cleanApiBaseUrl = normalizeGoogleChatApiBaseUrl(apiBaseUrl);
|
|
1211
|
+
const cleanWebhookUrl = normalizeGoogleChatWebhookUrl(incomingWebhookUrl);
|
|
1212
|
+
const cleanServiceAccountJson = serviceAccountJson?.trim();
|
|
1213
|
+
const cleanHomeChannel = homeChannel?.trim();
|
|
1214
|
+
const maxMessages = maxMessagesRaw ? Number(maxMessagesRaw) : undefined;
|
|
1215
|
+
const maxBytes = maxBytesRaw ? Number(maxBytesRaw) : undefined;
|
|
1216
|
+
if (!cleanApiBaseUrl) {
|
|
1217
|
+
console.error('Google Chat API base URL ต้องเป็น https:// URL');
|
|
1218
|
+
process.exit(1);
|
|
1219
|
+
}
|
|
1220
|
+
if (incomingWebhookUrl?.trim() && !cleanWebhookUrl) {
|
|
1221
|
+
console.error('Google Chat incoming webhook URL ต้องเป็น https:// URL');
|
|
1222
|
+
process.exit(1);
|
|
1223
|
+
}
|
|
1224
|
+
if (!cleanWebhookUrl && !cleanServiceAccountJson) {
|
|
1225
|
+
console.error('Google Chat setup ต้องมี service account JSON หรือ incoming webhook URL');
|
|
1226
|
+
process.exit(1);
|
|
1227
|
+
}
|
|
1228
|
+
if (cleanHomeChannel) {
|
|
1229
|
+
try {
|
|
1230
|
+
parseGoogleChatTarget({
|
|
1231
|
+
apiBaseUrl: cleanApiBaseUrl,
|
|
1232
|
+
homeChannel: cleanHomeChannel,
|
|
1233
|
+
allowedUsers: [],
|
|
1234
|
+
allowedSpaces: [],
|
|
1235
|
+
freeResponseSpaces: [],
|
|
1236
|
+
allowAllUsers: false,
|
|
1237
|
+
allowAllSpaces: false,
|
|
1238
|
+
maxMessages: 1,
|
|
1239
|
+
maxBytes: 16_777_216,
|
|
1240
|
+
enabled: true,
|
|
1241
|
+
source: 'config',
|
|
1242
|
+
serviceAccountJson: cleanServiceAccountJson,
|
|
1243
|
+
incomingWebhookUrl: cleanWebhookUrl,
|
|
1244
|
+
}, cleanHomeChannel);
|
|
1245
|
+
}
|
|
1246
|
+
catch (e) {
|
|
1247
|
+
console.error(e instanceof Error ? e.message : 'Google Chat home channel ไม่ถูกต้อง');
|
|
1248
|
+
process.exit(1);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (!cleanWebhookUrl && !cleanHomeChannel && !allowedSpacesRaw?.trim() && !allowAllSpaces) {
|
|
1252
|
+
console.error('Google Chat service-account setup ต้องมี home channel, allowed spaces หรือ --allow-all-spaces');
|
|
1253
|
+
process.exit(1);
|
|
1254
|
+
}
|
|
1255
|
+
if (maxMessagesRaw && (!Number.isInteger(maxMessages) || Number(maxMessages) <= 0)) {
|
|
1256
|
+
console.error('--max-messages ต้องเป็น integer มากกว่า 0');
|
|
1257
|
+
process.exit(1);
|
|
1258
|
+
}
|
|
1259
|
+
if (maxBytesRaw && (!Number.isInteger(maxBytes) || Number(maxBytes) <= 0)) {
|
|
1260
|
+
console.error('--max-bytes ต้องเป็น integer มากกว่า 0');
|
|
1261
|
+
process.exit(1);
|
|
1262
|
+
}
|
|
1263
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1264
|
+
await patchGatewayConfig({
|
|
1265
|
+
googleChat: {
|
|
1266
|
+
enabled: true,
|
|
1267
|
+
projectId: projectId?.trim() || undefined,
|
|
1268
|
+
subscriptionName: subscriptionName?.trim() || undefined,
|
|
1269
|
+
serviceAccountJson: cleanServiceAccountJson || undefined,
|
|
1270
|
+
apiBaseUrl: cleanApiBaseUrl,
|
|
1271
|
+
incomingWebhookUrl: cleanWebhookUrl,
|
|
1272
|
+
homeChannel: cleanHomeChannel || (cleanWebhookUrl ? 'webhook' : undefined),
|
|
1273
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
1274
|
+
allowedUsers: parseStringCsv(allowedUsersRaw),
|
|
1275
|
+
allowedSpaces: parseStringCsv(allowedSpacesRaw),
|
|
1276
|
+
freeResponseSpaces: parseStringCsv(freeResponseSpacesRaw),
|
|
1277
|
+
allowAllUsers,
|
|
1278
|
+
allowAllSpaces,
|
|
1279
|
+
maxMessages,
|
|
1280
|
+
maxBytes,
|
|
1281
|
+
},
|
|
1282
|
+
});
|
|
1283
|
+
console.log(`บันทึก Google Chat gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1284
|
+
console.log(cleanWebhookUrl ? 'Google Chat delivery mode: incoming webhook' : 'Google Chat delivery mode: Chat REST API');
|
|
1285
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to googlechat "hello"${cleanHomeChannel ? '' : ` หรือ ${BRAND.cliName} send --to googlechat:spaces/<space> "hello"`}`);
|
|
1286
|
+
}
|
|
1287
|
+
async function runBlueBubblesGatewaySetup(args) {
|
|
1288
|
+
let serverUrl = argValue(args, '--server-url', '--url');
|
|
1289
|
+
let password = argValue(args, '--password', '--token', '--guid');
|
|
1290
|
+
const webhookHost = argValue(args, '--webhook-host');
|
|
1291
|
+
const webhookPortRaw = argValue(args, '--webhook-port');
|
|
1292
|
+
const webhookPath = argValue(args, '--webhook-path');
|
|
1293
|
+
let homeChannel = argValue(args, '--home-channel', '--chat-guid', '--to');
|
|
1294
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
1295
|
+
let allowedUsersRaw = argValue(args, '--allowed-users', '--allowed-targets');
|
|
1296
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1297
|
+
const requireMention = args.includes('--require-mention');
|
|
1298
|
+
const mentionPatternsRaw = argValue(args, '--mention-patterns');
|
|
1299
|
+
const sendReadReceipts = !args.includes('--no-read-receipts');
|
|
1300
|
+
if (!serverUrl || !password || (!homeChannel && !allowedUsersRaw && !allowAllUsers)) {
|
|
1301
|
+
if (!process.stdin.isTTY) {
|
|
1302
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup bluebubbles --server-url <http://mac:1234> --password <server-password> --home-channel <chat-guid|email|phone>`);
|
|
1303
|
+
process.exit(1);
|
|
1304
|
+
}
|
|
1305
|
+
console.log(`${BRAND.productName} BlueBubbles/iMessage setup`);
|
|
1306
|
+
console.log('ต้องมี BlueBubbles Server URL + server password; outbound ใช้ REST API /api/v1/message/text');
|
|
1307
|
+
serverUrl ||= await askText('BlueBubbles server URL (เช่น http://localhost:1234): ');
|
|
1308
|
+
password ||= await askText('BlueBubbles server password: ');
|
|
1309
|
+
homeChannel ||= await askText('Home chat GUID/email/phone (blank = explicit targets only): ');
|
|
1310
|
+
}
|
|
1311
|
+
if (!allowedUsersRaw && homeChannel && !allowAllUsers)
|
|
1312
|
+
allowedUsersRaw = homeChannel;
|
|
1313
|
+
const { normalizeBlueBubblesServerUrl, normalizeBlueBubblesWebhookPath } = await import('./gateway/bluebubbles.js');
|
|
1314
|
+
const cleanServerUrl = normalizeBlueBubblesServerUrl(serverUrl);
|
|
1315
|
+
const cleanPassword = password?.trim();
|
|
1316
|
+
const cleanHomeChannel = homeChannel?.trim();
|
|
1317
|
+
const webhookPort = webhookPortRaw ? Number(webhookPortRaw) : undefined;
|
|
1318
|
+
if (!cleanServerUrl) {
|
|
1319
|
+
console.error('BlueBubbles server URL ต้องเป็น http:// หรือ https:// URL');
|
|
1320
|
+
process.exit(1);
|
|
1321
|
+
}
|
|
1322
|
+
if (!cleanPassword) {
|
|
1323
|
+
console.error('BlueBubbles setup ต้องมี server password');
|
|
1324
|
+
process.exit(1);
|
|
1325
|
+
}
|
|
1326
|
+
if (webhookPortRaw && (!Number.isInteger(webhookPort) || Number(webhookPort) <= 0 || Number(webhookPort) > 65535)) {
|
|
1327
|
+
console.error('--webhook-port ต้องเป็น port 1-65535');
|
|
1328
|
+
process.exit(1);
|
|
1329
|
+
}
|
|
1330
|
+
const allowedUsers = parseStringCsv(allowedUsersRaw);
|
|
1331
|
+
if (!allowAllUsers && !cleanHomeChannel && !allowedUsers.length) {
|
|
1332
|
+
console.error('BlueBubbles setup ต้องมี home channel/allowlist อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
1333
|
+
process.exit(1);
|
|
1334
|
+
}
|
|
1335
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1336
|
+
await patchGatewayConfig({
|
|
1337
|
+
bluebubbles: {
|
|
1338
|
+
enabled: true,
|
|
1339
|
+
serverUrl: cleanServerUrl,
|
|
1340
|
+
password: cleanPassword,
|
|
1341
|
+
webhookHost: webhookHost?.trim() || undefined,
|
|
1342
|
+
webhookPort,
|
|
1343
|
+
webhookPath: normalizeBlueBubblesWebhookPath(webhookPath),
|
|
1344
|
+
homeChannel: cleanHomeChannel || allowedUsers[0],
|
|
1345
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
1346
|
+
allowedUsers,
|
|
1347
|
+
allowAllUsers,
|
|
1348
|
+
requireMention,
|
|
1349
|
+
mentionPatterns: parseStringCsv(mentionPatternsRaw),
|
|
1350
|
+
sendReadReceipts,
|
|
1351
|
+
},
|
|
1352
|
+
});
|
|
1353
|
+
console.log(`บันทึก BlueBubbles gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1354
|
+
console.log(`BlueBubbles REST: ${cleanServerUrl}/api/v1/message/text`);
|
|
1355
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to bluebubbles "hello"${cleanHomeChannel ? '' : ` หรือ ${BRAND.cliName} send --to bluebubbles:<chat-guid|email|phone> "hello"`}`);
|
|
1356
|
+
}
|
|
1357
|
+
async function runTeamsGatewaySetup(args) {
|
|
1358
|
+
let incomingWebhookUrl = argValue(args, '--incoming-webhook-url', '--webhook-url', '--url');
|
|
1359
|
+
const graphAccessToken = argValue(args, '--graph-access-token', '--access-token', '--token');
|
|
1360
|
+
const teamId = argValue(args, '--team-id');
|
|
1361
|
+
const channelId = argValue(args, '--channel-id');
|
|
1362
|
+
const chatId = argValue(args, '--chat-id');
|
|
1363
|
+
let homeChannel = argValue(args, '--home-channel', '--to');
|
|
1364
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
1365
|
+
const clientId = argValue(args, '--client-id');
|
|
1366
|
+
const clientSecret = argValue(args, '--client-secret');
|
|
1367
|
+
const tenantId = argValue(args, '--tenant-id');
|
|
1368
|
+
const allowedUsersRaw = argValue(args, '--allowed-users');
|
|
1369
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1370
|
+
const portRaw = argValue(args, '--port');
|
|
1371
|
+
const rawMode = argValue(args, '--delivery-mode', '--mode');
|
|
1372
|
+
const deliveryMode = rawMode === 'graph' || (!rawMode && graphAccessToken) ? 'graph' : 'incoming_webhook';
|
|
1373
|
+
if (deliveryMode === 'incoming_webhook' && !incomingWebhookUrl) {
|
|
1374
|
+
if (!process.stdin.isTTY) {
|
|
1375
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup teams --incoming-webhook-url <https://...>`);
|
|
1376
|
+
process.exit(1);
|
|
1377
|
+
}
|
|
1378
|
+
console.log(`${BRAND.productName} Microsoft Teams setup`);
|
|
1379
|
+
console.log('โหมดง่าย: สร้าง Incoming Webhook ใน Teams channel แล้ววาง URL ที่นี่');
|
|
1380
|
+
incomingWebhookUrl = await askText('Teams incoming webhook URL: ');
|
|
1381
|
+
homeChannel ||= (await askText('Teams home target label (blank = webhook): ')) || 'webhook';
|
|
1382
|
+
}
|
|
1383
|
+
if (deliveryMode === 'graph' && (!graphAccessToken || (!chatId && !homeChannel && (!teamId || !channelId)))) {
|
|
1384
|
+
if (!process.stdin.isTTY) {
|
|
1385
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup teams --delivery-mode graph --graph-access-token <token> (--chat-id <id> หรือ --team-id <id> --channel-id <id>)`);
|
|
1386
|
+
process.exit(1);
|
|
1387
|
+
}
|
|
1388
|
+
console.log(`${BRAND.productName} Microsoft Teams Graph setup`);
|
|
1389
|
+
console.log('ต้องมี Microsoft Graph token และ chat id หรือ team/channel id สำหรับ proactive delivery');
|
|
1390
|
+
}
|
|
1391
|
+
const { normalizeTeamsWebhookUrl } = await import('./gateway/teams.js');
|
|
1392
|
+
const cleanWebhookUrl = normalizeTeamsWebhookUrl(incomingWebhookUrl);
|
|
1393
|
+
const cleanPort = portRaw ? Number(portRaw) : undefined;
|
|
1394
|
+
if (deliveryMode === 'incoming_webhook' && !cleanWebhookUrl) {
|
|
1395
|
+
console.error('Microsoft Teams incoming webhook URL ต้องเป็น https:// URL');
|
|
1396
|
+
process.exit(1);
|
|
1397
|
+
}
|
|
1398
|
+
if (deliveryMode === 'graph' && !graphAccessToken?.trim()) {
|
|
1399
|
+
console.error('Microsoft Teams Graph mode ต้องมี --graph-access-token');
|
|
1400
|
+
process.exit(1);
|
|
1401
|
+
}
|
|
1402
|
+
if (deliveryMode === 'graph' && !chatId?.trim() && !homeChannel?.trim() && (!teamId?.trim() || !channelId?.trim())) {
|
|
1403
|
+
console.error('Microsoft Teams Graph mode ต้องมี --chat-id หรือ --team-id + --channel-id');
|
|
1404
|
+
process.exit(1);
|
|
1405
|
+
}
|
|
1406
|
+
if (portRaw && (!Number.isInteger(cleanPort) || Number(cleanPort) <= 0)) {
|
|
1407
|
+
console.error('--port ต้องเป็น integer มากกว่า 0');
|
|
1408
|
+
process.exit(1);
|
|
1409
|
+
}
|
|
1410
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1411
|
+
const graphHome = homeChannel?.trim() || chatId?.trim() || (teamId?.trim() && channelId?.trim() ? `team/${teamId.trim()}/channel/${channelId.trim()}` : undefined);
|
|
1412
|
+
await patchGatewayConfig({
|
|
1413
|
+
teams: {
|
|
1414
|
+
enabled: true,
|
|
1415
|
+
deliveryMode,
|
|
1416
|
+
incomingWebhookUrl: cleanWebhookUrl,
|
|
1417
|
+
graphAccessToken: graphAccessToken?.trim() || undefined,
|
|
1418
|
+
teamId: teamId?.trim() || undefined,
|
|
1419
|
+
channelId: channelId?.trim() || undefined,
|
|
1420
|
+
chatId: chatId?.trim() || undefined,
|
|
1421
|
+
homeChannel: graphHome || (cleanWebhookUrl ? 'webhook' : undefined),
|
|
1422
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
1423
|
+
clientId: clientId?.trim() || undefined,
|
|
1424
|
+
clientSecret: clientSecret?.trim() || undefined,
|
|
1425
|
+
tenantId: tenantId?.trim() || undefined,
|
|
1426
|
+
allowedUsers: parseStringCsv(allowedUsersRaw),
|
|
1427
|
+
allowAllUsers,
|
|
1428
|
+
port: cleanPort,
|
|
1429
|
+
},
|
|
1430
|
+
});
|
|
1431
|
+
console.log(`บันทึก Microsoft Teams gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1432
|
+
console.log(`Teams delivery mode: ${deliveryMode}`);
|
|
1433
|
+
if (deliveryMode === 'incoming_webhook')
|
|
1434
|
+
console.log('ส่งผ่าน Incoming Webhook ที่ตั้งไว้');
|
|
1435
|
+
else
|
|
1436
|
+
console.log(`Graph target: ${graphHome}`);
|
|
1437
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to teams "hello"`);
|
|
1438
|
+
}
|
|
1439
|
+
async function runNtfyGatewaySetup(args) {
|
|
1440
|
+
let topic = argValue(args, '--topic');
|
|
1441
|
+
const serverUrl = argValue(args, '--server-url') ?? argValue(args, '--url');
|
|
1442
|
+
const token = argValue(args, '--token');
|
|
1443
|
+
const publishTopic = argValue(args, '--publish-topic');
|
|
1444
|
+
let homeChannel = argValue(args, '--home-channel', '--to');
|
|
1445
|
+
let allowedRaw = argValue(args, '--allowed-users', '--allowed-topics');
|
|
1446
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
1447
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1448
|
+
const markdown = args.includes('--markdown');
|
|
1449
|
+
if (!topic) {
|
|
1450
|
+
if (!process.stdin.isTTY) {
|
|
1451
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup ntfy --topic <topic> [--token <tk_...|user:pass>]`);
|
|
1452
|
+
process.exit(1);
|
|
1453
|
+
}
|
|
1454
|
+
console.log(`${BRAND.productName} ntfy setup`);
|
|
1455
|
+
console.log('เลือก topic ยาว/เดายาก แล้ว subscribe topic นี้ใน ntfy mobile app หรือ self-hosted ntfy');
|
|
1456
|
+
topic = await askText('ntfy topic: ');
|
|
1457
|
+
}
|
|
1458
|
+
const cleanTopic = topic.trim();
|
|
1459
|
+
if (!cleanTopic) {
|
|
1460
|
+
console.error('ntfy setup ต้องมี topic');
|
|
1461
|
+
process.exit(1);
|
|
1462
|
+
}
|
|
1463
|
+
if (!homeChannel)
|
|
1464
|
+
homeChannel = cleanTopic;
|
|
1465
|
+
if (!allowedRaw && !allowAllUsers)
|
|
1466
|
+
allowedRaw = cleanTopic;
|
|
1467
|
+
const allowedUsers = parseStringCsv(allowedRaw);
|
|
1468
|
+
if (!allowAllUsers && !allowedUsers.includes(cleanTopic) && homeChannel !== cleanTopic) {
|
|
1469
|
+
console.error('ntfy setup ต้องมี topic ใน --allowed-users หรือใช้ --allow-all-users เพื่อรับ inbound');
|
|
1470
|
+
process.exit(1);
|
|
1471
|
+
}
|
|
1472
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1473
|
+
await patchGatewayConfig({
|
|
1474
|
+
ntfy: {
|
|
1475
|
+
enabled: true,
|
|
1476
|
+
serverUrl: serverUrl?.trim() || undefined,
|
|
1477
|
+
topic: cleanTopic,
|
|
1478
|
+
publishTopic: publishTopic?.trim() || undefined,
|
|
1479
|
+
token: token?.trim() || undefined,
|
|
1480
|
+
homeChannel: homeChannel?.trim() || cleanTopic,
|
|
1481
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
1482
|
+
allowedUsers,
|
|
1483
|
+
allowAllUsers,
|
|
1484
|
+
markdown,
|
|
1485
|
+
},
|
|
1486
|
+
});
|
|
1487
|
+
console.log(`บันทึก ntfy gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1488
|
+
console.log(`subscribe topic ในแอป ntfy: ${cleanTopic}`);
|
|
1489
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to ntfy "hello"`);
|
|
1490
|
+
}
|
|
1491
|
+
async function runWebhookGatewaySetup(args) {
|
|
1492
|
+
let secret = argValue(args, '--secret', '--webhook-secret');
|
|
1493
|
+
const publicUrl = argValue(args, '--public-url');
|
|
1494
|
+
const rateLimitRaw = argValue(args, '--rate-limit', '--rate-limit-per-minute');
|
|
1495
|
+
const insecureNoAuth = args.includes('--insecure-no-auth');
|
|
1496
|
+
if (!secret && !insecureNoAuth) {
|
|
1497
|
+
if (process.stdin.isTTY) {
|
|
1498
|
+
const { generateWebhookSecret } = await import('./gateway/webhooks.js');
|
|
1499
|
+
console.log(`${BRAND.productName} Webhooks setup`);
|
|
1500
|
+
console.log('ตั้ง global HMAC secret สำหรับ route ที่ไม่ได้ระบุ secret เอง');
|
|
1501
|
+
secret = (await askText('Webhook global secret (blank = auto-generate): ')) || generateWebhookSecret();
|
|
1502
|
+
}
|
|
1503
|
+
else {
|
|
1504
|
+
const { generateWebhookSecret } = await import('./gateway/webhooks.js');
|
|
1505
|
+
secret = generateWebhookSecret();
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
const rateLimitPerMinute = rateLimitRaw ? parsePort(rateLimitRaw, 30, 'webhook rate limit') : undefined;
|
|
1509
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1510
|
+
await patchGatewayConfig({
|
|
1511
|
+
webhooks: {
|
|
1512
|
+
enabled: true,
|
|
1513
|
+
secret: insecureNoAuth ? 'INSECURE_NO_AUTH' : secret?.trim(),
|
|
1514
|
+
publicUrl: publicUrl?.trim() || undefined,
|
|
1515
|
+
rateLimitPerMinute,
|
|
1516
|
+
},
|
|
1517
|
+
});
|
|
1518
|
+
console.log(`บันทึก Webhooks gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1519
|
+
console.log(`เพิ่ม route ได้ด้วย: ${BRAND.cliName} webhook subscribe github-issues --events issues --prompt "New issue: {issue.title}" --to telegram`);
|
|
1520
|
+
}
|
|
1521
|
+
function parsePort(raw, fallback, label) {
|
|
1522
|
+
if (!raw)
|
|
1523
|
+
return fallback;
|
|
1524
|
+
const n = Number(raw);
|
|
1525
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
1526
|
+
console.error(`${label} ต้องเป็น port 1-65535`);
|
|
1527
|
+
process.exit(1);
|
|
1528
|
+
}
|
|
1529
|
+
return n;
|
|
1530
|
+
}
|
|
1531
|
+
async function runEmailGatewaySetup(args) {
|
|
1532
|
+
let address = argValue(args, '--address', '--email');
|
|
1533
|
+
let password = argValue(args, '--password', '--app-password');
|
|
1534
|
+
let imapHost = argValue(args, '--imap-host');
|
|
1535
|
+
let smtpHost = argValue(args, '--smtp-host');
|
|
1536
|
+
let homeAddress = argValue(args, '--home-address', '--to');
|
|
1537
|
+
let allowedRaw = argValue(args, '--allowed-users', '--allowed-senders');
|
|
1538
|
+
const imapPort = parsePort(argValue(args, '--imap-port'), 993, 'imap port');
|
|
1539
|
+
const smtpPort = parsePort(argValue(args, '--smtp-port'), 587, 'smtp port');
|
|
1540
|
+
const pollIntervalSeconds = parsePort(argValue(args, '--poll-interval'), 15, 'poll interval');
|
|
1541
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1542
|
+
if (!address) {
|
|
1543
|
+
if (!process.stdin.isTTY) {
|
|
1544
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup email --address bot@example.com --password <app-password> --imap-host imap.example.com --smtp-host smtp.example.com --home-address you@example.com`);
|
|
1545
|
+
process.exit(1);
|
|
1546
|
+
}
|
|
1547
|
+
console.log(`${BRAND.productName} Email setup`);
|
|
1548
|
+
console.log('แนะนำให้ใช้ dedicated mailbox + app password ไม่ใช่บัญชีส่วนตัวหลัก');
|
|
1549
|
+
address = await askText('Email address ของ bot: ');
|
|
1550
|
+
}
|
|
1551
|
+
if (!password) {
|
|
1552
|
+
if (!process.stdin.isTTY) {
|
|
1553
|
+
console.error('ต้องระบุ --password <app-password>');
|
|
1554
|
+
process.exit(1);
|
|
1555
|
+
}
|
|
1556
|
+
password = await askText('Email app password: ');
|
|
1557
|
+
}
|
|
1558
|
+
if (!imapHost) {
|
|
1559
|
+
if (!process.stdin.isTTY) {
|
|
1560
|
+
console.error('ต้องระบุ --imap-host <host>');
|
|
1561
|
+
process.exit(1);
|
|
1562
|
+
}
|
|
1563
|
+
imapHost = await askText('IMAP host (เช่น imap.gmail.com): ');
|
|
1564
|
+
}
|
|
1565
|
+
if (!smtpHost) {
|
|
1566
|
+
if (!process.stdin.isTTY) {
|
|
1567
|
+
console.error('ต้องระบุ --smtp-host <host>');
|
|
1568
|
+
process.exit(1);
|
|
1569
|
+
}
|
|
1570
|
+
smtpHost = await askText('SMTP host (เช่น smtp.gmail.com): ');
|
|
1571
|
+
}
|
|
1572
|
+
if (!homeAddress && !allowedRaw && !allowAllUsers) {
|
|
1573
|
+
if (!process.stdin.isTTY) {
|
|
1574
|
+
console.error('ต้องระบุ --home-address <email> หรือ --allowed-users <email[,email]> เพื่อ fail-closed');
|
|
1575
|
+
process.exit(1);
|
|
1576
|
+
}
|
|
1577
|
+
homeAddress = await askText('Home/allowed email address: ');
|
|
1578
|
+
}
|
|
1579
|
+
const allowedUsers = parseStringCsv(allowedRaw ?? homeAddress).map((s) => s.toLowerCase());
|
|
1580
|
+
if (!allowAllUsers && !allowedUsers.length) {
|
|
1581
|
+
console.error('Email setup ต้องมี allowed sender/home address อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
1582
|
+
process.exit(1);
|
|
1583
|
+
}
|
|
1584
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1585
|
+
await patchGatewayConfig({
|
|
1586
|
+
email: {
|
|
1587
|
+
enabled: true,
|
|
1588
|
+
address: address.trim(),
|
|
1589
|
+
password: password.trim(),
|
|
1590
|
+
imapHost: imapHost.trim(),
|
|
1591
|
+
imapPort,
|
|
1592
|
+
smtpHost: smtpHost.trim(),
|
|
1593
|
+
smtpPort,
|
|
1594
|
+
homeAddress: homeAddress?.trim() || allowedUsers[0],
|
|
1595
|
+
allowedUsers,
|
|
1596
|
+
allowAllUsers,
|
|
1597
|
+
pollIntervalSeconds,
|
|
1598
|
+
},
|
|
1599
|
+
});
|
|
1600
|
+
console.log(`บันทึก Email gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1601
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to email:${homeAddress?.trim() || allowedUsers[0]} "hello"`);
|
|
1602
|
+
}
|
|
1603
|
+
async function runGateway(args) {
|
|
1604
|
+
const [action, ...rest] = args;
|
|
1605
|
+
if (!action || action === 'status' || action === 'list')
|
|
1606
|
+
return runGatewayStatus();
|
|
1607
|
+
if (action === 'setup')
|
|
1608
|
+
return runGatewaySetup(rest);
|
|
1609
|
+
if (action === 'run') {
|
|
1610
|
+
if (!hasServeCommandRequest(['serve', ...rest])) {
|
|
1611
|
+
console.error(`ไม่รู้จัก: gateway run ${rest.join(' ')} — ใช้ gateway run [--port N] [--model spec]`);
|
|
1612
|
+
process.exit(1);
|
|
1613
|
+
}
|
|
1614
|
+
return runServe(rest);
|
|
1615
|
+
}
|
|
1616
|
+
if (action === 'start') {
|
|
1617
|
+
const { startGatewayService } = await import('./gateway/service.js');
|
|
1618
|
+
const res = await startGatewayService({ entrypoint: resolve(process.argv[1]), gatewayArgs: rest });
|
|
1619
|
+
console.log(res.started
|
|
1620
|
+
? `เริ่ม ${BRAND.cliName} gateway background แล้ว (pid ${res.state.pid})`
|
|
1621
|
+
: `${BRAND.cliName} gateway รันอยู่แล้ว (pid ${res.state.pid})`);
|
|
1622
|
+
console.log(`log: ${res.state.logPath}`);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
if (action === 'stop') {
|
|
1626
|
+
const { stopGatewayService } = await import('./gateway/service.js');
|
|
1627
|
+
const res = await stopGatewayService();
|
|
1628
|
+
console.log(res.state ? (res.stopped ? `หยุด gateway pid ${res.state.pid} แล้ว` : `gateway ไม่ได้รันอยู่ (last pid ${res.state.pid})`) : 'ยังไม่มี gateway service state');
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
if (action === 'restart') {
|
|
1632
|
+
const { startGatewayService, stopGatewayService } = await import('./gateway/service.js');
|
|
1633
|
+
await stopGatewayService();
|
|
1634
|
+
const res = await startGatewayService({ entrypoint: resolve(process.argv[1]), gatewayArgs: rest });
|
|
1635
|
+
console.log(`restart gateway แล้ว (pid ${res.state.pid})`);
|
|
1636
|
+
console.log(`log: ${res.state.logPath}`);
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
if (action === 'install') {
|
|
1640
|
+
const { installGatewayService } = await import('./gateway/service.js');
|
|
1641
|
+
const res = await installGatewayService(resolve(process.argv[1]));
|
|
1642
|
+
console.log(`ติดตั้ง service file แล้ว (${res.kind}): ${res.path}`);
|
|
1643
|
+
console.log('เริ่ม service ด้วย:');
|
|
1644
|
+
for (const line of res.instructions)
|
|
1645
|
+
console.log(` ${line}`);
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
if (action === 'uninstall' || action === 'remove-service') {
|
|
1649
|
+
const { uninstallGatewayService } = await import('./gateway/service.js');
|
|
1650
|
+
const removed = await uninstallGatewayService();
|
|
1651
|
+
console.log(removed.length ? `ลบ service files:\n${removed.map((p) => ` ${p}`).join('\n')}` : 'ไม่พบ service file ที่ต้องลบ');
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
console.error(`ไม่รู้จัก: gateway ${action} — ใช้ setup / run / start / stop / restart / install / status`);
|
|
1655
|
+
process.exit(1);
|
|
1656
|
+
}
|
|
1657
|
+
async function runStatus() {
|
|
1658
|
+
const cfg = await loadConfig({});
|
|
1659
|
+
const parsed = parseSpec(cfg.model);
|
|
1660
|
+
const provider = PROVIDERS[parsed.provider];
|
|
1661
|
+
const keyReady = provider ? (!provider.requiresKey || Boolean(resolveKeyFromEnv(provider.envVar, provider.envFallbacks))) : false;
|
|
1662
|
+
const { readGatewayConfig, resolveBlueBubblesConfig, resolveDiscordConfig, resolveEmailConfig, resolveGoogleChatConfig, resolveHomeAssistantConfig, resolveLineConfig, resolveMattermostConfig, resolveMatrixConfig, resolveNtfyConfig, resolveSignalConfig, resolveSlackConfig, resolveSmsConfig, resolveTelegramConfig, resolveTeamsConfig, resolveWhatsAppConfig, resolveWebhookConfig, } = await import('./gateway/config.js');
|
|
1663
|
+
const gatewayConfig = await readGatewayConfig();
|
|
1664
|
+
const telegram = resolveTelegramConfig(gatewayConfig);
|
|
1665
|
+
const discord = resolveDiscordConfig(gatewayConfig);
|
|
1666
|
+
const slack = resolveSlackConfig(gatewayConfig);
|
|
1667
|
+
const email = resolveEmailConfig(gatewayConfig);
|
|
1668
|
+
const homeassistant = resolveHomeAssistantConfig(gatewayConfig);
|
|
1669
|
+
const line = resolveLineConfig(gatewayConfig);
|
|
1670
|
+
const mattermost = resolveMattermostConfig(gatewayConfig);
|
|
1671
|
+
const sms = resolveSmsConfig(gatewayConfig);
|
|
1672
|
+
const ntfy = resolveNtfyConfig(gatewayConfig);
|
|
1673
|
+
const signal = resolveSignalConfig(gatewayConfig);
|
|
1674
|
+
const whatsapp = resolveWhatsAppConfig(gatewayConfig);
|
|
1675
|
+
const matrix = resolveMatrixConfig(gatewayConfig);
|
|
1676
|
+
const googleChat = resolveGoogleChatConfig(gatewayConfig);
|
|
1677
|
+
const bluebubbles = resolveBlueBubblesConfig(gatewayConfig);
|
|
1678
|
+
const teams = resolveTeamsConfig(gatewayConfig);
|
|
1679
|
+
const webhooks = resolveWebhookConfig(gatewayConfig);
|
|
1680
|
+
console.log(`${BRAND.productName} status`);
|
|
1681
|
+
console.log(` version: ${VERSION}`);
|
|
1682
|
+
console.log(` model: ${cfg.model}`);
|
|
1683
|
+
console.log(` provider: ${provider?.label ?? parsed.provider}`);
|
|
1684
|
+
console.log(` personality:${cfg.personality ? ` ${cfg.personality}` : ' none'}`);
|
|
1685
|
+
console.log(` key: ${keyReady ? 'ready' : provider?.requiresKey ? `missing (${provider.envVar})` : 'not required'}`);
|
|
1686
|
+
console.log(` brain: ${cfg.brainPath ?? '(not configured)'}`);
|
|
1687
|
+
console.log(' gateway: HTTP loopback + cron available');
|
|
1688
|
+
console.log(` telegram: ${telegram.token ? `configured (${telegram.allowedChatIds.length} allowed chat${telegram.allowedChatIds.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
1689
|
+
console.log(` discord: ${discord.token ? `configured (${discord.allowedChannelIds.length} allowed channel${discord.allowedChannelIds.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
1690
|
+
console.log(` slack: ${slack.botToken ? `configured (${slack.allowedChannelIds.length} allowed channel${slack.allowedChannelIds.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
1691
|
+
console.log(` mattermost:${mattermost.serverUrl && mattermost.token ? ` configured (${mattermost.allowedUsers.length} allowed user${mattermost.allowedUsers.length === 1 ? '' : 's'}, ${mattermost.allowedChannels.length} channel${mattermost.allowedChannels.length === 1 ? '' : 's'})` : ' not configured'}`);
|
|
1692
|
+
console.log(` homeassist:${homeassistant.token ? ` configured (${homeassistant.watchDomains.length} domain${homeassistant.watchDomains.length === 1 ? '' : 's'}, ${homeassistant.watchEntities.length} entit${homeassistant.watchEntities.length === 1 ? 'y' : 'ies'}, watchAll=${homeassistant.watchAll ? 'yes' : 'no'})` : ' not configured'}`);
|
|
1693
|
+
console.log(` email: ${email.address ? `configured (${email.allowedUsers.length} allowed sender${email.allowedUsers.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
1694
|
+
console.log(` line: ${line.channelAccessToken ? `configured (${line.allowedUsers.length + line.allowedGroups.length + line.allowedRooms.length} allowed target${line.allowedUsers.length + line.allowedGroups.length + line.allowedRooms.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
1695
|
+
console.log(` sms: ${sms.accountSid && sms.authToken && sms.phoneNumber ? `configured (${sms.allowedUsers.length} allowed sender${sms.allowedUsers.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
1696
|
+
console.log(` ntfy: ${ntfy.topic ? `configured (${ntfy.allowedUsers.length} allowed topic${ntfy.allowedUsers.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
1697
|
+
console.log(` signal: ${signal.account ? `configured (${signal.allowedUsers.length} allowed user${signal.allowedUsers.length === 1 ? '' : 's'}, ${signal.groupAllowedUsers.length} group${signal.groupAllowedUsers.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
1698
|
+
console.log(` whatsapp: ${whatsapp.phoneNumberId && whatsapp.accessToken ? `configured (${whatsapp.allowedUsers.length} allowed user${whatsapp.allowedUsers.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
1699
|
+
console.log(` matrix: ${matrix.homeserver && (matrix.accessToken || (matrix.userId && matrix.password)) ? `configured (${matrix.allowedUsers.length} allowed user${matrix.allowedUsers.length === 1 ? '' : 's'}, ${matrix.allowedRooms.length} room${matrix.allowedRooms.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
1700
|
+
console.log(` googlechat:${googleChat.serviceAccountJson || googleChat.incomingWebhookUrl ? ` configured (${googleChat.serviceAccountJson ? 'chat api' : 'webhook'})` : ' not configured'}`);
|
|
1701
|
+
console.log(` bluebubbles:${bluebubbles.serverUrl && bluebubbles.password ? ` configured (${bluebubbles.allowedUsers.length} allowed target${bluebubbles.allowedUsers.length === 1 ? '' : 's'})` : ' not configured'}`);
|
|
1702
|
+
console.log(` teams: ${teams.incomingWebhookUrl || teams.graphAccessToken ? `configured (${teams.deliveryMode})` : 'not configured'}`);
|
|
1703
|
+
console.log(` webhooks: ${webhooks.enabled ? `enabled (${Object.keys(webhooks.routes).length} route${Object.keys(webhooks.routes).length === 1 ? '' : 's'})` : 'not enabled'}`);
|
|
1704
|
+
console.log(` config: ${appHomePath('config.json')}`);
|
|
1705
|
+
}
|
|
1706
|
+
function compactText(raw, max = 120) {
|
|
1707
|
+
const text = redactKey(raw).replace(/\s+/g, ' ').trim();
|
|
1708
|
+
return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text;
|
|
1709
|
+
}
|
|
1710
|
+
function messageContentText(content) {
|
|
1711
|
+
if (typeof content === 'string')
|
|
1712
|
+
return content;
|
|
1713
|
+
if (!Array.isArray(content))
|
|
1714
|
+
return '';
|
|
1715
|
+
return content
|
|
1716
|
+
.map((part) => {
|
|
1717
|
+
if (typeof part === 'string')
|
|
1718
|
+
return part;
|
|
1719
|
+
if (part && typeof part === 'object') {
|
|
1720
|
+
const record = part;
|
|
1721
|
+
if (typeof record.text === 'string')
|
|
1722
|
+
return record.text;
|
|
1723
|
+
if (typeof record.type === 'string')
|
|
1724
|
+
return `[${record.type}]`;
|
|
1725
|
+
}
|
|
1726
|
+
return '';
|
|
1727
|
+
})
|
|
1728
|
+
.filter(Boolean)
|
|
1729
|
+
.join(' ');
|
|
1730
|
+
}
|
|
1731
|
+
function sessionPreview(session) {
|
|
1732
|
+
if (session.title)
|
|
1733
|
+
return compactText(session.title);
|
|
1734
|
+
const firstUser = session.messages.find((m) => m.role === 'user') ?? session.messages[0];
|
|
1735
|
+
return firstUser ? compactText(messageContentText(firstUser.content)) : '';
|
|
1736
|
+
}
|
|
1737
|
+
function sessionUsage() {
|
|
1738
|
+
return `ใช้:
|
|
1739
|
+
${BRAND.cliName} sessions [list] [--all] [--limit N]
|
|
1740
|
+
${BRAND.cliName} sessions latest [--all]
|
|
1741
|
+
${BRAND.cliName} sessions show <id>
|
|
1742
|
+
${BRAND.cliName} sessions export <id> [--format json|markdown] [--output path]
|
|
1743
|
+
${BRAND.cliName} sessions rename <id> <title>
|
|
1744
|
+
${BRAND.cliName} sessions stats [--all]
|
|
1745
|
+
${BRAND.cliName} sessions prune --keep N [--all] [--yes]
|
|
1746
|
+
${BRAND.cliName} sessions rm <id>
|
|
1747
|
+
|
|
1748
|
+
resume:
|
|
1749
|
+
${BRAND.cliName} --resume <id> "<task>"
|
|
1750
|
+
${BRAND.cliName} -r <id> "<task>"`;
|
|
1751
|
+
}
|
|
1752
|
+
function parseLimit(args, fallback) {
|
|
1753
|
+
const provided = args.some((a) => a === '--limit' || a === '-n' || a.startsWith('--limit=') || a.startsWith('-n='));
|
|
1754
|
+
const raw = argValue(args, '--limit', '-n');
|
|
1755
|
+
if (!provided)
|
|
1756
|
+
return fallback;
|
|
1757
|
+
const n = Number(raw);
|
|
1758
|
+
if (!raw || !Number.isInteger(n) || n <= 0) {
|
|
1759
|
+
console.error('--limit ต้องเป็น integer บวก');
|
|
1760
|
+
process.exit(2);
|
|
1761
|
+
}
|
|
1762
|
+
return n;
|
|
1763
|
+
}
|
|
1764
|
+
function printSessionDetails(session) {
|
|
1765
|
+
const users = session.messages.filter((m) => m.role === 'user');
|
|
1766
|
+
const assistants = session.messages.filter((m) => m.role === 'assistant');
|
|
1767
|
+
const lastUser = users[users.length - 1];
|
|
1768
|
+
const lastAssistant = assistants[assistants.length - 1];
|
|
1769
|
+
console.log(`${BRAND.productName} session ${session.id}`);
|
|
1770
|
+
if (session.title)
|
|
1771
|
+
console.log(` title: ${redactKey(session.title)}`);
|
|
1772
|
+
console.log(` model: ${session.model}`);
|
|
1773
|
+
console.log(` cwd: ${session.cwd}`);
|
|
1774
|
+
console.log(` created: ${session.created}`);
|
|
1775
|
+
console.log(` updated: ${session.updated}`);
|
|
1776
|
+
console.log(` messages: ${session.messages.length} (${users.length} user, ${assistants.length} assistant)`);
|
|
1777
|
+
console.log(` preview: ${sessionPreview(session) || '(empty)'}`);
|
|
1778
|
+
if (lastUser)
|
|
1779
|
+
console.log(` last user: ${compactText(messageContentText(lastUser.content)) || '(empty)'}`);
|
|
1780
|
+
if (lastAssistant)
|
|
1781
|
+
console.log(` last ai: ${compactText(messageContentText(lastAssistant.content)) || '(empty)'}`);
|
|
1782
|
+
}
|
|
1783
|
+
function sessionToMarkdown(session) {
|
|
1784
|
+
const safe = sanitizeSessionForExport(session);
|
|
1785
|
+
const lines = [
|
|
1786
|
+
`# ${safe.title ? redactKey(safe.title) : `Session ${safe.id}`}`,
|
|
1787
|
+
'',
|
|
1788
|
+
`- id: ${safe.id}`,
|
|
1789
|
+
`- model: ${safe.model}`,
|
|
1790
|
+
`- cwd: ${safe.cwd}`,
|
|
1791
|
+
`- created: ${safe.created}`,
|
|
1792
|
+
`- updated: ${safe.updated}`,
|
|
1793
|
+
'',
|
|
1794
|
+
];
|
|
1795
|
+
for (const [i, msg] of safe.messages.entries()) {
|
|
1796
|
+
const role = msg.role ?? 'message';
|
|
1797
|
+
const text = compactText(messageContentText(msg.content), 20_000);
|
|
1798
|
+
lines.push(`## ${i + 1}. ${role}`, '', text || '(empty)', '');
|
|
1799
|
+
}
|
|
1800
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
1801
|
+
}
|
|
1802
|
+
function parseDateFlag(args, ...names) {
|
|
1803
|
+
const raw = argValue(args, ...names);
|
|
1804
|
+
if (!raw)
|
|
1805
|
+
return undefined;
|
|
1806
|
+
const d = new Date(raw);
|
|
1807
|
+
if (!Number.isFinite(d.getTime())) {
|
|
1808
|
+
console.error(`วันที่ไม่ถูกต้อง: ${raw}`);
|
|
1809
|
+
process.exit(2);
|
|
1810
|
+
}
|
|
1811
|
+
return d;
|
|
1812
|
+
}
|
|
1813
|
+
async function loadSessionOrExit(id) {
|
|
1814
|
+
const session = await loadSession(id);
|
|
1815
|
+
if (!session) {
|
|
1816
|
+
console.error(`ไม่เจอ session ${id}`);
|
|
1817
|
+
process.exit(1);
|
|
1818
|
+
}
|
|
1819
|
+
return session;
|
|
1820
|
+
}
|
|
1821
|
+
async function requestedResumeSession(rawArgs, resumeId) {
|
|
1822
|
+
const requested = hasResumeRequest(rawArgs);
|
|
1823
|
+
if (!requested)
|
|
1824
|
+
return null;
|
|
1825
|
+
if (!resumeId) {
|
|
1826
|
+
console.error(`ใช้: ${BRAND.cliName} --resume <session_id> "<task>"`);
|
|
1827
|
+
process.exit(2);
|
|
1828
|
+
}
|
|
1829
|
+
return loadSessionOrExit(resumeId);
|
|
1830
|
+
}
|
|
1831
|
+
async function requestedContinuationHistory(rawArgs) {
|
|
1832
|
+
if (!hasContinueRequest(rawArgs))
|
|
1833
|
+
return undefined;
|
|
1834
|
+
return (await latestSession(hasContinueAnyRequest(rawArgs) ? null : process.cwd()))?.messages;
|
|
1835
|
+
}
|
|
1836
|
+
function printSessionStats(sessions, scope) {
|
|
1837
|
+
const byModel = new Map();
|
|
1838
|
+
let messages = 0;
|
|
1839
|
+
for (const s of sessions) {
|
|
1840
|
+
byModel.set(s.model, (byModel.get(s.model) ?? 0) + 1);
|
|
1841
|
+
messages += s.messages.length;
|
|
1842
|
+
}
|
|
1843
|
+
console.log(`${BRAND.productName} session stats (${scope})`);
|
|
1844
|
+
console.log(` sessions: ${sessions.length}`);
|
|
1845
|
+
console.log(` messages: ${messages}`);
|
|
1846
|
+
if (sessions[0])
|
|
1847
|
+
console.log(` latest: ${sessions[0].id} (${sessions[0].updated})`);
|
|
1848
|
+
if (sessions[sessions.length - 1])
|
|
1849
|
+
console.log(` oldest: ${sessions[sessions.length - 1].id} (${sessions[sessions.length - 1].updated})`);
|
|
1850
|
+
console.log(' models:');
|
|
1851
|
+
for (const [model, count] of [...byModel.entries()].sort((a, b) => b[1] - a[1])) {
|
|
1852
|
+
console.log(` ${model}: ${count}`);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
async function runSessions(args) {
|
|
1856
|
+
if (args.includes('-h') || args.includes('--help') || args[0] === 'help') {
|
|
1857
|
+
console.log(sessionUsage());
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
const action = args[0] && !args[0].startsWith('-') ? args[0] : 'list';
|
|
1861
|
+
const rest = action === 'list' && (args[0]?.startsWith('-') || args[0] === undefined) ? args : args.slice(1);
|
|
1862
|
+
const all = rest.includes('--all') || rest.includes('-a');
|
|
1863
|
+
const cwd = all ? null : process.cwd();
|
|
1864
|
+
if (action === 'list' || action === 'ls') {
|
|
1865
|
+
const sessions = await listSessions({ cwd, limit: parseLimit(rest, 20) });
|
|
1866
|
+
if (!sessions.length) {
|
|
1867
|
+
console.log(`ยังไม่มี saved sessions${all ? '' : ' สำหรับ project นี้'} — store: ${sessionStorePath()}`);
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
console.log(`${BRAND.productName} sessions (${all ? 'all projects' : 'current project'})`);
|
|
1871
|
+
for (const s of sessions) {
|
|
1872
|
+
const cwdSuffix = all ? ` ${s.cwd}` : '';
|
|
1873
|
+
console.log(`${s.id} ${s.updated} ${s.model} ${s.messages.length} msg ${sessionPreview(s)}${cwdSuffix}`);
|
|
1874
|
+
}
|
|
1875
|
+
console.log(`\nstore: ${sessionStorePath()}`);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
if (action === 'latest') {
|
|
1879
|
+
const session = (await listSessions({ cwd, limit: 1 }))[0];
|
|
1880
|
+
if (!session) {
|
|
1881
|
+
console.log(`ไม่เจอ session${all ? '' : ' สำหรับ project นี้'}`);
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
printSessionDetails(session);
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
if (action === 'show' || action === 'cat') {
|
|
1888
|
+
const id = positionalArgs(rest, ['--limit', '-n'])[0];
|
|
1889
|
+
if (!id) {
|
|
1890
|
+
console.error(`ใช้: ${BRAND.cliName} sessions show <id>`);
|
|
1891
|
+
process.exit(2);
|
|
1892
|
+
}
|
|
1893
|
+
printSessionDetails(await loadSessionOrExit(id));
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
if (action === 'export') {
|
|
1897
|
+
const id = positionalArgs(rest, ['--format', '--output', '-o'])[0];
|
|
1898
|
+
if (!id) {
|
|
1899
|
+
console.error(`ใช้: ${BRAND.cliName} sessions export <id> [--format json|markdown] [--output path]`);
|
|
1900
|
+
process.exit(2);
|
|
1901
|
+
}
|
|
1902
|
+
const format = argValue(rest, '--format') ?? 'markdown';
|
|
1903
|
+
if (format !== 'json' && format !== 'markdown' && format !== 'md') {
|
|
1904
|
+
console.error('--format ต้องเป็น json หรือ markdown');
|
|
1905
|
+
process.exit(2);
|
|
1906
|
+
}
|
|
1907
|
+
const session = await loadSessionOrExit(id);
|
|
1908
|
+
const out = format === 'json' ? `${JSON.stringify(sanitizeSessionForExport(session), null, 2)}\n` : sessionToMarkdown(session);
|
|
1909
|
+
const outputPath = argValue(rest, '--output', '-o');
|
|
1910
|
+
if (!outputPath || outputPath === '-') {
|
|
1911
|
+
process.stdout.write(out);
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
1915
|
+
await writeFile(outputPath, out, { mode: 0o600 });
|
|
1916
|
+
await chmod(outputPath, 0o600).catch(() => { });
|
|
1917
|
+
console.log(`exported session ${id} → ${outputPath}`);
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
if (action === 'rename' || action === 'title') {
|
|
1921
|
+
const [id, ...titleParts] = positionalArgs(rest, ['--limit', '-n']);
|
|
1922
|
+
const title = titleParts.join(' ').trim();
|
|
1923
|
+
if (!id || !title) {
|
|
1924
|
+
console.error(`ใช้: ${BRAND.cliName} sessions rename <id> <title>`);
|
|
1925
|
+
process.exit(2);
|
|
1926
|
+
}
|
|
1927
|
+
const next = await renameSession(id, title);
|
|
1928
|
+
if (!next) {
|
|
1929
|
+
console.error(`ไม่เจอ session ${id}`);
|
|
1930
|
+
process.exit(1);
|
|
1931
|
+
}
|
|
1932
|
+
console.log(`ตั้งชื่อ session ${id}: ${redactKey(next.title ?? '')}`);
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
if (action === 'stats') {
|
|
1936
|
+
printSessionStats(await listSessions({ cwd }), all ? 'all projects' : 'current project');
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
if (action === 'prune') {
|
|
1940
|
+
const keepRaw = argValue(rest, '--keep');
|
|
1941
|
+
const before = parseDateFlag(rest, '--before');
|
|
1942
|
+
if (!keepRaw && !before) {
|
|
1943
|
+
console.error(`ใช้: ${BRAND.cliName} sessions prune --keep N [--before YYYY-MM-DD] [--all] [--yes]`);
|
|
1944
|
+
process.exit(2);
|
|
1945
|
+
}
|
|
1946
|
+
const keep = keepRaw == null ? undefined : Number(keepRaw);
|
|
1947
|
+
if (keep != null && (!Number.isInteger(keep) || keep < 0)) {
|
|
1948
|
+
console.error('--keep ต้องเป็น integer >= 0');
|
|
1949
|
+
process.exit(2);
|
|
1950
|
+
}
|
|
1951
|
+
const candidates = await listSessions({ cwd });
|
|
1952
|
+
const candidateIds = new Set();
|
|
1953
|
+
if (keep != null)
|
|
1954
|
+
for (const s of candidates.slice(keep))
|
|
1955
|
+
candidateIds.add(s.id);
|
|
1956
|
+
if (before) {
|
|
1957
|
+
const beforeMs = before.getTime();
|
|
1958
|
+
for (const s of candidates) {
|
|
1959
|
+
const updatedMs = Date.parse(s.updated);
|
|
1960
|
+
if (Number.isFinite(updatedMs) && updatedMs < beforeMs)
|
|
1961
|
+
candidateIds.add(s.id);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
if (!candidateIds.size) {
|
|
1965
|
+
console.log('ไม่มี session ที่ต้อง prune');
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
if (!rest.includes('--yes') && !rest.includes('-y')) {
|
|
1969
|
+
console.log(`จะลบ ${candidateIds.size} sessions (dry-run):`);
|
|
1970
|
+
for (const s of candidates.filter((x) => candidateIds.has(x.id)))
|
|
1971
|
+
console.log(` ${s.id} ${s.updated} ${sessionPreview(s)}`);
|
|
1972
|
+
console.log(`\nรันซ้ำพร้อม --yes เพื่อยืนยัน`);
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
const removed = await pruneSessions({ cwd, keep, before });
|
|
1976
|
+
console.log(`ลบ ${removed.length} sessions แล้ว`);
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
if (action === 'rm' || action === 'remove' || action === 'delete') {
|
|
1980
|
+
const id = positionalArgs(rest, ['--limit', '-n'])[0];
|
|
1981
|
+
if (!id) {
|
|
1982
|
+
console.error(`ใช้: ${BRAND.cliName} sessions rm <id>`);
|
|
1983
|
+
process.exit(2);
|
|
1984
|
+
}
|
|
1985
|
+
const ok = await removeSession(id);
|
|
1986
|
+
console.log(ok ? `ลบ session ${id} แล้ว` : `ไม่เจอ session ${id}`);
|
|
1987
|
+
return;
|
|
1988
|
+
}
|
|
1989
|
+
console.error(`ไม่รู้จัก: sessions ${action}\n${sessionUsage()}`);
|
|
1990
|
+
process.exit(1);
|
|
1991
|
+
}
|
|
1992
|
+
async function runInsights(args) {
|
|
1993
|
+
const { parseInsightsArgs } = await import('./insights-args.js');
|
|
1994
|
+
const parsed = parseInsightsArgs(args);
|
|
1995
|
+
if (parsed === null) {
|
|
1996
|
+
console.error(`ใช้: ${BRAND.cliName} insights [--days N] [--all]`);
|
|
1997
|
+
process.exit(2);
|
|
1998
|
+
}
|
|
1999
|
+
const { renderInsights } = await import('./insights.js');
|
|
2000
|
+
console.log(await renderInsights({ days: parsed.days, cwd: parsed.all ? null : process.cwd(), includeGateway: true }));
|
|
2001
|
+
}
|
|
2002
|
+
async function runDump(args) {
|
|
2003
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
2004
|
+
console.log(`ใช้: ${BRAND.cliName} dump [--show-keys]\n\nสร้าง diagnostic/support dump โดย redact secret เสมอ`);
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
const { buildSupportDump } = await import('./support-dump.js');
|
|
2008
|
+
process.stdout.write(await buildSupportDump({
|
|
2009
|
+
showKeys: args.includes('--show-keys'),
|
|
2010
|
+
version: VERSION,
|
|
2011
|
+
packageName: PACKAGE_NAME,
|
|
2012
|
+
cwd: process.cwd(),
|
|
2013
|
+
}));
|
|
2014
|
+
}
|
|
2015
|
+
function providerIds() {
|
|
2016
|
+
return Object.keys(PROVIDERS).join(', ');
|
|
2017
|
+
}
|
|
2018
|
+
function findProviderId(raw) {
|
|
2019
|
+
if (!raw)
|
|
2020
|
+
return undefined;
|
|
2021
|
+
const lower = raw.toLowerCase();
|
|
2022
|
+
if (PROVIDERS[lower])
|
|
2023
|
+
return lower;
|
|
2024
|
+
for (const [id, cfg] of Object.entries(PROVIDERS)) {
|
|
2025
|
+
if (raw === cfg.envVar || cfg.envFallbacks?.includes(raw))
|
|
2026
|
+
return id;
|
|
2027
|
+
}
|
|
2028
|
+
return undefined;
|
|
2029
|
+
}
|
|
2030
|
+
function authEnvSource(providerId) {
|
|
2031
|
+
const cfg = PROVIDERS[providerId];
|
|
2032
|
+
if (!cfg)
|
|
2033
|
+
return undefined;
|
|
2034
|
+
for (const name of [cfg.envVar, ...(cfg.envFallbacks ?? [])]) {
|
|
2035
|
+
if (process.env[name]?.trim())
|
|
2036
|
+
return name;
|
|
2037
|
+
}
|
|
2038
|
+
return undefined;
|
|
2039
|
+
}
|
|
2040
|
+
function authUsage() {
|
|
2041
|
+
return `ใช้:
|
|
2042
|
+
${BRAND.cliName} auth list
|
|
2043
|
+
${BRAND.cliName} auth status <provider>
|
|
2044
|
+
${BRAND.cliName} auth add <provider> --api-key <key> [--use]
|
|
2045
|
+
${BRAND.cliName} auth remove <provider|ENV_VAR>
|
|
2046
|
+
${BRAND.cliName} auth reset [provider|ENV_VAR]
|
|
2047
|
+
|
|
2048
|
+
providers: ${providerIds()}`;
|
|
2049
|
+
}
|
|
2050
|
+
async function runAuth(args) {
|
|
2051
|
+
const action = args[0] ?? 'list';
|
|
2052
|
+
const rest = args.slice(action === 'list' && args[0] !== 'list' ? 0 : 1);
|
|
2053
|
+
const { authConfigPath, clearStoredAuth, readStoredAuthRaw, removeStoredKey, saveGlobalConfig, saveKey } = await import('./config.js');
|
|
2054
|
+
if (action === '-h' || action === '--help' || action === 'help') {
|
|
2055
|
+
console.log(authUsage());
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
if (action === 'list' || action === 'ls' || action === 'status-all') {
|
|
2059
|
+
const stored = await readStoredAuthRaw();
|
|
2060
|
+
console.log(`${BRAND.productName} auth`);
|
|
2061
|
+
console.log(` store: ${authConfigPath()}`);
|
|
2062
|
+
for (const [id, cfg] of Object.entries(PROVIDERS)) {
|
|
2063
|
+
if (!cfg.requiresKey) {
|
|
2064
|
+
console.log(` ${id.padEnd(10)} ${cfg.label} — no API key required`);
|
|
2065
|
+
continue;
|
|
2066
|
+
}
|
|
2067
|
+
const key = resolveKeyFromEnv(cfg.envVar, cfg.envFallbacks);
|
|
2068
|
+
const source = authEnvSource(id);
|
|
2069
|
+
const saved = stored[cfg.envVar];
|
|
2070
|
+
const state = key ? `ready via ${source ?? cfg.envVar}` : `missing ${cfg.envVar}`;
|
|
2071
|
+
const savedText = saved ? ` · stored ${redactKey(saved)}` : '';
|
|
2072
|
+
console.log(` ${id.padEnd(10)} ${cfg.label} — ${state}${savedText}`);
|
|
2073
|
+
}
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
if (action === 'status') {
|
|
2077
|
+
const providerId = findProviderId(positionalArgs(rest, ['--api-key', '--key', '--token', '--model'])[0]);
|
|
2078
|
+
if (!providerId) {
|
|
2079
|
+
console.error(`ใช้: ${BRAND.cliName} auth status <provider>\nproviders: ${providerIds()}`);
|
|
2080
|
+
process.exit(1);
|
|
2081
|
+
}
|
|
2082
|
+
const cfg = PROVIDERS[providerId];
|
|
2083
|
+
const stored = await readStoredAuthRaw();
|
|
2084
|
+
const key = resolveKeyFromEnv(cfg.envVar, cfg.envFallbacks);
|
|
2085
|
+
const source = authEnvSource(providerId);
|
|
2086
|
+
console.log(`${cfg.label} (${providerId})`);
|
|
2087
|
+
console.log(` key required: ${cfg.requiresKey ? 'yes' : 'no'}`);
|
|
2088
|
+
console.log(` env var: ${cfg.envVar}${cfg.envFallbacks?.length ? ` (fallback: ${cfg.envFallbacks.join(', ')})` : ''}`);
|
|
2089
|
+
console.log(` stored: ${stored[cfg.envVar] ? redactKey(stored[cfg.envVar]) : '(not stored)'}`);
|
|
2090
|
+
console.log(` runtime: ${key ? `${redactKey(key)} via ${source ?? cfg.envVar}` : '(missing)'}`);
|
|
2091
|
+
const url = consoleUrl(providerId);
|
|
2092
|
+
if (url)
|
|
2093
|
+
console.log(` console: ${url}`);
|
|
2094
|
+
if (cfg.note)
|
|
2095
|
+
console.log(` note: ${cfg.note}`);
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
if (action === 'add' || action === 'login') {
|
|
2099
|
+
const providerId = findProviderId(positionalArgs(rest, ['--api-key', '--key', '--token', '--model'])[0]);
|
|
2100
|
+
if (!providerId) {
|
|
2101
|
+
console.error(`ใช้: ${BRAND.cliName} auth add <provider> --api-key <key>\nproviders: ${providerIds()}`);
|
|
2102
|
+
process.exit(1);
|
|
2103
|
+
}
|
|
2104
|
+
const cfg = PROVIDERS[providerId];
|
|
2105
|
+
if (!cfg.requiresKey) {
|
|
2106
|
+
console.log(`${cfg.label} ไม่ต้องเก็บ API key ใน Sanook`);
|
|
2107
|
+
if (cfg.note)
|
|
2108
|
+
console.log(cfg.note);
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
let key = argValue(rest, '--api-key', '--key', '--token');
|
|
2112
|
+
if (!key) {
|
|
2113
|
+
if (!process.stdin.isTTY) {
|
|
2114
|
+
console.error(`ใช้: ${BRAND.cliName} auth add ${providerId} --api-key <key>`);
|
|
2115
|
+
process.exit(1);
|
|
2116
|
+
}
|
|
2117
|
+
key = await askText(`${cfg.label} API key (${cfg.keyExample ?? cfg.envVar}): `);
|
|
2118
|
+
}
|
|
2119
|
+
try {
|
|
2120
|
+
assertDirectApiKey(cfg, key);
|
|
2121
|
+
}
|
|
2122
|
+
catch (e) {
|
|
2123
|
+
console.error(redactKey(e.message));
|
|
2124
|
+
process.exit(1);
|
|
2125
|
+
}
|
|
2126
|
+
await saveKey(cfg.envVar, key.trim());
|
|
2127
|
+
console.log(`บันทึก ${cfg.label} key แล้ว: ${cfg.envVar}=${redactKey(key.trim())}`);
|
|
2128
|
+
if (rest.includes('--use') || rest.includes('--default')) {
|
|
2129
|
+
const modelArg = argValue(rest, '--model', '-m') ?? 'default';
|
|
2130
|
+
const model = modelArg.includes(':') ? modelArg : `${providerId}:${cfg.models[modelArg] ?? modelArg}`;
|
|
2131
|
+
await saveGlobalConfig({ model, provider: providerId });
|
|
2132
|
+
console.log(`ตั้ง default model เป็น ${model}`);
|
|
2133
|
+
}
|
|
2134
|
+
else {
|
|
2135
|
+
console.log(`ใช้เป็น default ได้ด้วย: ${BRAND.cliName} auth add ${providerId} --api-key <key> --use`);
|
|
2136
|
+
}
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
if (action === 'remove' || action === 'rm' || action === 'logout') {
|
|
2140
|
+
const target = positionalArgs(rest, ['--api-key', '--key', '--token', '--model'])[0];
|
|
2141
|
+
if (!target && action !== 'logout') {
|
|
2142
|
+
console.error(`ใช้: ${BRAND.cliName} auth remove <provider|ENV_VAR>`);
|
|
2143
|
+
process.exit(1);
|
|
2144
|
+
}
|
|
2145
|
+
if (!target && action === 'logout') {
|
|
2146
|
+
await clearStoredAuth();
|
|
2147
|
+
console.log('ล้าง key ที่ Sanook เก็บไว้ทั้งหมดแล้ว');
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
const providerId = findProviderId(target);
|
|
2151
|
+
const envVar = providerId ? PROVIDERS[providerId].envVar : target;
|
|
2152
|
+
const ok = await removeStoredKey(envVar);
|
|
2153
|
+
console.log(ok ? `ลบ ${envVar} ออกจาก Sanook auth store แล้ว` : `ไม่เจอ ${envVar} ใน Sanook auth store`);
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
if (action === 'reset' || action === 'clear') {
|
|
2157
|
+
const target = positionalArgs(rest, ['--api-key', '--key', '--token', '--model'])[0];
|
|
2158
|
+
if (!target) {
|
|
2159
|
+
await clearStoredAuth();
|
|
2160
|
+
console.log('ล้าง key ที่ Sanook เก็บไว้ทั้งหมดแล้ว');
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
const providerId = findProviderId(target);
|
|
2164
|
+
const envVar = providerId ? PROVIDERS[providerId].envVar : target;
|
|
2165
|
+
const ok = await removeStoredKey(envVar);
|
|
2166
|
+
console.log(ok ? `ลบ ${envVar} ออกจาก Sanook auth store แล้ว` : `ไม่เจอ ${envVar} ใน Sanook auth store`);
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
console.error(`ไม่รู้จัก: auth ${action}\n${authUsage()}`);
|
|
2170
|
+
process.exit(1);
|
|
2171
|
+
}
|
|
2172
|
+
async function runSetup(args) {
|
|
2173
|
+
if (args.includes('-h') || args.includes('--help') || args[0] === 'help' || args[0] === 'list' || args[0] === 'status') {
|
|
2174
|
+
console.log(await setupOverview());
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
const section = args.find((a) => !a.startsWith('-')) ?? 'model';
|
|
2178
|
+
const start = args.indexOf(section);
|
|
2179
|
+
const rest = start === -1 ? [] : args.slice(start + 1);
|
|
2180
|
+
if (section === 'model')
|
|
2181
|
+
return startModelSetup();
|
|
2182
|
+
if (section === 'gateway')
|
|
2183
|
+
return runGateway(['setup', ...rest]);
|
|
2184
|
+
if (section === 'tools')
|
|
2185
|
+
return runTools(rest);
|
|
2186
|
+
if (section === 'agent')
|
|
2187
|
+
return runAgentSetupSummary();
|
|
2188
|
+
if (section === 'brain')
|
|
2189
|
+
return runBrain(['init', ...rest]);
|
|
2190
|
+
console.error(`ไม่รู้จัก setup section "${section}" — ใช้ model / gateway / tools / agent / brain`);
|
|
2191
|
+
process.exit(1);
|
|
2192
|
+
}
|
|
2193
|
+
async function setupOverview() {
|
|
2194
|
+
const cfg = await loadConfig({});
|
|
2195
|
+
const { readGatewayConfig, resolveBlueBubblesConfig, resolveDiscordConfig, resolveEmailConfig, resolveGoogleChatConfig, resolveHomeAssistantConfig, resolveLineConfig, resolveMattermostConfig, resolveMatrixConfig, resolveNtfyConfig, resolveSignalConfig, resolveSlackConfig, resolveSmsConfig, resolveTelegramConfig, resolveTeamsConfig, resolveWhatsAppConfig, resolveWebhookConfig, } = await import('./gateway/config.js');
|
|
2196
|
+
const gateway = await readGatewayConfig();
|
|
2197
|
+
const telegram = resolveTelegramConfig(gateway);
|
|
2198
|
+
const discord = resolveDiscordConfig(gateway);
|
|
2199
|
+
const slack = resolveSlackConfig(gateway);
|
|
2200
|
+
const mattermost = resolveMattermostConfig(gateway);
|
|
2201
|
+
const homeassistant = resolveHomeAssistantConfig(gateway);
|
|
2202
|
+
const email = resolveEmailConfig(gateway);
|
|
2203
|
+
const line = resolveLineConfig(gateway);
|
|
2204
|
+
const sms = resolveSmsConfig(gateway);
|
|
2205
|
+
const ntfy = resolveNtfyConfig(gateway);
|
|
2206
|
+
const signal = resolveSignalConfig(gateway);
|
|
2207
|
+
const whatsapp = resolveWhatsAppConfig(gateway);
|
|
2208
|
+
const matrix = resolveMatrixConfig(gateway);
|
|
2209
|
+
const googleChat = resolveGoogleChatConfig(gateway);
|
|
2210
|
+
const bluebubbles = resolveBlueBubblesConfig(gateway);
|
|
2211
|
+
const teams = resolveTeamsConfig(gateway);
|
|
2212
|
+
const webhooks = resolveWebhookConfig(gateway);
|
|
2213
|
+
const configuredPlatforms = [
|
|
2214
|
+
telegram.token ? 'telegram' : '',
|
|
2215
|
+
discord.token ? 'discord' : '',
|
|
2216
|
+
slack.botToken ? 'slack' : '',
|
|
2217
|
+
mattermost.serverUrl && mattermost.token ? 'mattermost' : '',
|
|
2218
|
+
homeassistant.token ? 'homeassistant' : '',
|
|
2219
|
+
email.address ? 'email' : '',
|
|
2220
|
+
line.channelAccessToken ? 'line' : '',
|
|
2221
|
+
sms.accountSid ? 'sms' : '',
|
|
2222
|
+
ntfy.topic ? 'ntfy' : '',
|
|
2223
|
+
signal.account ? 'signal' : '',
|
|
2224
|
+
whatsapp.phoneNumberId && whatsapp.accessToken ? 'whatsapp' : '',
|
|
2225
|
+
matrix.homeserver ? 'matrix' : '',
|
|
2226
|
+
googleChat.serviceAccountJson || googleChat.incomingWebhookUrl ? 'googlechat' : '',
|
|
2227
|
+
bluebubbles.serverUrl && bluebubbles.password ? 'bluebubbles' : '',
|
|
2228
|
+
teams.incomingWebhookUrl || teams.graphAccessToken ? 'teams' : '',
|
|
2229
|
+
webhooks.enabled ? 'webhooks' : '',
|
|
2230
|
+
].filter(Boolean);
|
|
2231
|
+
return [
|
|
2232
|
+
`${BRAND.productName} setup`,
|
|
2233
|
+
'',
|
|
2234
|
+
` model ${BRAND.cliName} setup model เลือก provider + model (current: ${cfg.model})`,
|
|
2235
|
+
` gateway ${BRAND.cliName} setup gateway เชื่อม messaging platforms (${configuredPlatforms.length ? configuredPlatforms.join(', ') : 'not configured'})`,
|
|
2236
|
+
` tools ${BRAND.cliName} setup tools ดู tool surface + MCP entry points`,
|
|
2237
|
+
` agent ${BRAND.cliName} setup agent ตั้ง permission/budget/personality/insights`,
|
|
2238
|
+
` brain ${BRAND.cliName} setup brain สร้าง Second Brain vault + AGENTS/GEMINI/SANOOK rules`,
|
|
2239
|
+
'',
|
|
2240
|
+
`เริ่มเร็ว: ${BRAND.cliName} setup model`,
|
|
2241
|
+
`ดูสถานะ: ${BRAND.cliName} status`,
|
|
2242
|
+
].join('\n');
|
|
2243
|
+
}
|
|
2244
|
+
function modelOverrideForProvider(providerArg, modelArg) {
|
|
2245
|
+
const providerId = findProviderId(providerArg);
|
|
2246
|
+
if (!providerArg)
|
|
2247
|
+
return modelArg;
|
|
2248
|
+
if (!providerId) {
|
|
2249
|
+
console.error(`ไม่รู้จัก provider "${providerArg}" — มี: ${providerIds()}`);
|
|
2250
|
+
process.exit(1);
|
|
2251
|
+
}
|
|
2252
|
+
if (!modelArg)
|
|
2253
|
+
return `${providerId}:${PROVIDERS[providerId].models.default}`;
|
|
2254
|
+
if (modelArg.includes(':'))
|
|
2255
|
+
return modelArg;
|
|
2256
|
+
return `${providerId}:${PROVIDERS[providerId].models[modelArg] ?? modelArg}`;
|
|
2257
|
+
}
|
|
2258
|
+
function appendPipedInput(prompt, piped) {
|
|
2259
|
+
return piped ? `${prompt}\n\n<stdin>\n${piped}\n</stdin>`.trim() : prompt;
|
|
2260
|
+
}
|
|
2261
|
+
async function runChat(args) {
|
|
2262
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
2263
|
+
console.log(`ใช้:
|
|
2264
|
+
${BRAND.cliName} chat -q "<query>" [--provider <provider>] [--model <alias|id>]
|
|
2265
|
+
${BRAND.cliName} chat "<query>" [--provider <provider>]
|
|
2266
|
+
${BRAND.cliName} chat เปิด interactive REPL
|
|
2267
|
+
|
|
2268
|
+
providers: ${providerIds()}`);
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
let split = extractValue(args, '-q', '--query');
|
|
2272
|
+
const query = split.value;
|
|
2273
|
+
split = extractValue(split.rest, '--provider');
|
|
2274
|
+
const provider = split.value;
|
|
2275
|
+
split = extractValue(split.rest, '--toolsets', '--tools');
|
|
2276
|
+
const toolsets = split.value;
|
|
2277
|
+
const safeMode = split.rest.includes('--safe-mode');
|
|
2278
|
+
const yolo = split.rest.includes('--yolo') || split.rest.includes('--dangerously-skip-permissions');
|
|
2279
|
+
const cleaned = stripBooleanFlags(split.rest, '--safe-mode', '--yolo', '--dangerously-skip-permissions');
|
|
2280
|
+
const parsed = parseArgs(yolo ? [...cleaned, '--yes'] : cleaned);
|
|
2281
|
+
const resumeSession = await requestedResumeSession(cleaned, parsed.resume);
|
|
2282
|
+
const budgetUsd = Number.isFinite(parsed.budget) ? parsed.budget : undefined;
|
|
2283
|
+
const model = modelOverrideForProvider(provider, parsed.model ?? (provider ? undefined : resumeSession?.model));
|
|
2284
|
+
const piped = process.stdin.isTTY ? '' : (await readStdin()).trim();
|
|
2285
|
+
const prompt = appendPipedInput(query ?? parsed.prompt, piped);
|
|
2286
|
+
if (toolsets && !parsed.quiet && !parsed.json) {
|
|
2287
|
+
process.stderr.write(`${DIM}(toolsets="${toolsets}" accepted; Sanook currently exposes the configured tool surface)${RESET}\n`);
|
|
2288
|
+
}
|
|
2289
|
+
if (!prompt) {
|
|
2290
|
+
const config = await loadConfig({ model, budgetUsd });
|
|
2291
|
+
const { startApp } = await import('./ui/render.js');
|
|
2292
|
+
startApp({
|
|
2293
|
+
needsSetup: false,
|
|
2294
|
+
appProps: {
|
|
2295
|
+
initialModel: config.model,
|
|
2296
|
+
fallbackModel: config.fallbackModel,
|
|
2297
|
+
budgetUsd: config.budgetUsd,
|
|
2298
|
+
permissionMode: parsed.yes || yolo ? 'auto' : safeMode ? 'ask' : config.permissionMode,
|
|
2299
|
+
initialHistory: resumeSession?.messages ?? (await requestedContinuationHistory(cleaned)),
|
|
2300
|
+
},
|
|
2301
|
+
});
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
const config = await loadConfig({ model, budgetUsd });
|
|
2305
|
+
const noKey = headlessKeyHint(config.model);
|
|
2306
|
+
if (noKey) {
|
|
2307
|
+
process.stderr.write(`${noKey}\n`);
|
|
2308
|
+
process.exit(1);
|
|
2309
|
+
}
|
|
2310
|
+
const history = resumeSession?.messages ?? (await requestedContinuationHistory(args));
|
|
2311
|
+
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, parsed.json, history, parsed.planMode, parsed.yes || yolo ? 'auto' : safeMode ? 'ask' : config.permissionMode, parsed.quiet, config.fallbackModel);
|
|
2312
|
+
}
|
|
2313
|
+
async function runPureOneShot(args) {
|
|
2314
|
+
const rest = args;
|
|
2315
|
+
const parsed = parseArgs(rest);
|
|
2316
|
+
const resumeSession = await requestedResumeSession(rest, parsed.resume);
|
|
2317
|
+
const budgetUsd = Number.isFinite(parsed.budget) ? parsed.budget : undefined;
|
|
2318
|
+
const piped = process.stdin.isTTY ? '' : (await readStdin()).trim();
|
|
2319
|
+
const prompt = appendPipedInput(parsed.prompt, piped);
|
|
2320
|
+
if (!prompt) {
|
|
2321
|
+
console.error(`ใช้: ${BRAND.cliName} -z "<task>"`);
|
|
2322
|
+
process.exit(1);
|
|
2323
|
+
}
|
|
2324
|
+
const config = await loadConfig({ model: parsed.model ?? resumeSession?.model, budgetUsd });
|
|
2325
|
+
const noKey = headlessKeyHint(config.model);
|
|
2326
|
+
if (noKey) {
|
|
2327
|
+
process.stderr.write(`${noKey}\n`);
|
|
2328
|
+
process.exit(1);
|
|
2329
|
+
}
|
|
2330
|
+
const history = resumeSession?.messages ?? (await requestedContinuationHistory(rest));
|
|
2331
|
+
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, parsed.json, history, parsed.planMode, parsed.yes ? 'auto' : config.permissionMode, true, config.fallbackModel);
|
|
2332
|
+
}
|
|
2333
|
+
async function runSend(args) {
|
|
2334
|
+
const json = args.includes('--json');
|
|
2335
|
+
const quiet = args.includes('--quiet') || args.includes('-q');
|
|
2336
|
+
const wantsList = args.includes('--list') || args.includes('-l');
|
|
2337
|
+
const valueFlags = ['--to', '-t', '--file', '-f', '--subject', '-s'];
|
|
2338
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
2339
|
+
console.log(`ใช้:
|
|
2340
|
+
${BRAND.cliName} send --to telegram[:chat_id[:thread_id]] "message"
|
|
2341
|
+
${BRAND.cliName} send --to discord[:channel_id[:thread_id]] "message"
|
|
2342
|
+
${BRAND.cliName} send --to slack[:channel_id[:thread_ts]] "message"
|
|
2343
|
+
${BRAND.cliName} send --to mattermost[:channel_id[:root_post_id]] "message"
|
|
2344
|
+
${BRAND.cliName} send --to homeassistant[:notification_id] "message"
|
|
2345
|
+
${BRAND.cliName} send --to email[:recipient@example.com] --subject "[CI]" "message"
|
|
2346
|
+
${BRAND.cliName} send --to line[:U/C/R-id] "message"
|
|
2347
|
+
${BRAND.cliName} send --to sms[:+15558675310] "message"
|
|
2348
|
+
${BRAND.cliName} send --to ntfy[:topic] "message"
|
|
2349
|
+
${BRAND.cliName} send --to signal[:+15558675310|group:<id>] "message"
|
|
2350
|
+
${BRAND.cliName} send --to whatsapp[:15558675310] "message"
|
|
2351
|
+
${BRAND.cliName} send --to matrix[:!roomid:matrix.org] "message"
|
|
2352
|
+
${BRAND.cliName} send --to googlechat[:spaces/AAA|spaces/AAA/threads/BBB] "message"
|
|
2353
|
+
${BRAND.cliName} send --to bluebubbles[:chat-guid|email|phone] "message"
|
|
2354
|
+
${BRAND.cliName} send --to teams[:chat_id|team/<team-id>/channel/<channel-id>] "message"
|
|
2355
|
+
${BRAND.cliName} send --to slack --subject "[CI]" --file build.log
|
|
2356
|
+
echo "done" | ${BRAND.cliName} send --to telegram --quiet
|
|
2357
|
+
${BRAND.cliName} send --list [platform] [--json]`);
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
if (wantsList) {
|
|
2361
|
+
const { listConfiguredTargets } = await import('./gateway/targets.js');
|
|
2362
|
+
const { readGatewayConfig } = await import('./gateway/config.js');
|
|
2363
|
+
const filter = positionalArgs(args, valueFlags)[0];
|
|
2364
|
+
const targets = listConfiguredTargets(await readGatewayConfig()).filter((t) => !filter || t.platform === filter);
|
|
2365
|
+
if (json) {
|
|
2366
|
+
console.log(JSON.stringify({ targets }));
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
if (!targets.length) {
|
|
2370
|
+
console.log(filter ? `ยังไม่มี target สำหรับ ${filter}` : `ยังไม่มี messaging target — เริ่มด้วย: ${BRAND.cliName} gateway setup`);
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
for (const t of targets) {
|
|
2374
|
+
console.log(`${t.target.padEnd(24)} ${t.configured ? 'ready' : 'not-ready'} ${t.label}`);
|
|
2375
|
+
}
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
const to = argValue(args, '--to', '-t');
|
|
2379
|
+
if (!to) {
|
|
2380
|
+
console.error(`ใช้: ${BRAND.cliName} send --to <telegram|discord|slack|mattermost|homeassistant|email|line|sms|ntfy|signal|whatsapp|matrix|googlechat|bluebubbles|teams>[:target] "message"`);
|
|
2381
|
+
process.exit(2);
|
|
2382
|
+
}
|
|
2383
|
+
const file = argValue(args, '--file', '-f');
|
|
2384
|
+
const subject = argValue(args, '--subject', '-s');
|
|
2385
|
+
let message = positionalArgs(args, valueFlags).join(' ').trim();
|
|
2386
|
+
if (!message && file)
|
|
2387
|
+
message = file === '-' ? await readStdin() : await readFile(file, 'utf8');
|
|
2388
|
+
if (!message && !process.stdin.isTTY)
|
|
2389
|
+
message = (await readStdin()).trim();
|
|
2390
|
+
if (subject && message && !to.startsWith('email'))
|
|
2391
|
+
message = `${subject.trim()}\n\n${message.trim()}`;
|
|
2392
|
+
if (!message) {
|
|
2393
|
+
console.error('message ว่าง — ใส่ข้อความ, --file <path>, หรือ pipe stdin เข้ามา');
|
|
2394
|
+
process.exit(2);
|
|
2395
|
+
}
|
|
2396
|
+
const { parseSendTarget } = await import('./gateway/targets.js');
|
|
2397
|
+
try {
|
|
2398
|
+
parseSendTarget(to);
|
|
2399
|
+
}
|
|
2400
|
+
catch (e) {
|
|
2401
|
+
console.error(e.message);
|
|
2402
|
+
process.exit(2);
|
|
2403
|
+
}
|
|
2404
|
+
const { deliverToTarget } = await import('./gateway/deliver.js');
|
|
2405
|
+
try {
|
|
2406
|
+
const result = await deliverToTarget(to, message, { subject });
|
|
111
2407
|
if (json)
|
|
112
|
-
|
|
2408
|
+
console.log(JSON.stringify({ ok: true, ...result }));
|
|
2409
|
+
else if (!quiet)
|
|
2410
|
+
console.log(`sent ${result.target}`);
|
|
2411
|
+
}
|
|
2412
|
+
catch (e) {
|
|
2413
|
+
const msg = redactKey(e.message);
|
|
2414
|
+
if (json)
|
|
2415
|
+
console.log(JSON.stringify({ ok: false, error: msg }));
|
|
113
2416
|
else
|
|
114
|
-
console.error(
|
|
2417
|
+
console.error(`ส่งไม่สำเร็จ: ${msg}`);
|
|
115
2418
|
process.exit(1);
|
|
116
2419
|
}
|
|
117
2420
|
}
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
${BRAND.cliName}
|
|
140
|
-
${BRAND.cliName}
|
|
141
|
-
${BRAND.cliName}
|
|
142
|
-
|
|
143
|
-
second brain (Obsidian workspace สำหรับจัดเก็บงาน + ความจำ AI):
|
|
144
|
-
${BRAND.cliName} brain init [path] สร้างโครงสร้าง second-brain ที่ path (ไม่ใส่ = ถาม)
|
|
145
|
-
|
|
146
|
-
search (BM25 + optional BYOK semantic เหนือ vault + memory + sessions + skills):
|
|
147
|
-
${BRAND.cliName} index (re)index vault+memory แบบ incremental (O(delta))
|
|
148
|
-
${BRAND.cliName} search "<query>" [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,memory]
|
|
149
|
-
${BRAND.cliName} mcp serve expose brain เป็น MCP server (stdio) ให้ Claude Desktop/Cursor
|
|
150
|
-
|
|
151
|
-
config & mcp:
|
|
152
|
-
${BRAND.cliName} config [get|set <k> <v>] ดู/แก้ ${appHomePath('config.json')} (model/budgetUsd/permissionMode/cacheTtl/compaction/thinking)
|
|
153
|
-
${BRAND.cliName} mcp [list|add <name> <cmd> …|remove <name>] จัดการ MCP servers
|
|
154
|
-
${BRAND.cliName} trust [status|add|remove] อนุญาต/ยกเลิก project .sanook mcp/hooks/skills/commands
|
|
155
|
-
|
|
156
|
-
flags:
|
|
157
|
-
-m, --model <spec> sonnet/opus/haiku/fable · gpt/codex · gemini · grok · deepseek · mistral · groq · ollama/lmstudio
|
|
158
|
-
or "provider:model-id" (e.g. openai:gpt-5-codex, groq:fast, google:gemini-2.5-flash)
|
|
159
|
-
-b, --budget <usd> stop when estimated cost exceeds this
|
|
160
|
-
-c, --continue resume the latest session ของ project นี้
|
|
161
|
-
--continue-any resume latest session ข้าม project (explicit)
|
|
162
|
-
--plan plan mode — สำรวจ+วางแผนเท่านั้น ไม่แก้ไฟล์ (read-only)
|
|
163
|
-
-y, --yes อนุมัติ tool อัตโนมัติ (ข้าม ask-mode permission)
|
|
164
|
-
--json machine-readable JSONL output
|
|
165
|
-
-v, --version
|
|
166
|
-
-h, --help
|
|
2421
|
+
async function runWebhook(args) {
|
|
2422
|
+
const action = args[0] ?? 'list';
|
|
2423
|
+
const rest = action === 'list' && args[0] !== 'list' ? args : args.slice(1);
|
|
2424
|
+
const valueFlags = [
|
|
2425
|
+
'--events',
|
|
2426
|
+
'--prompt',
|
|
2427
|
+
'--to',
|
|
2428
|
+
'-t',
|
|
2429
|
+
'--deliver',
|
|
2430
|
+
'--deliver-chat-id',
|
|
2431
|
+
'--chat-id',
|
|
2432
|
+
'--secret',
|
|
2433
|
+
'--description',
|
|
2434
|
+
'--payload',
|
|
2435
|
+
'--public-url',
|
|
2436
|
+
'--rate-limit',
|
|
2437
|
+
'--rate-limit-per-minute',
|
|
2438
|
+
];
|
|
2439
|
+
if (args.includes('-h') || args.includes('--help') || action === 'help') {
|
|
2440
|
+
console.log(`ใช้:
|
|
2441
|
+
${BRAND.cliName} webhook subscribe <route> [--events issues,push] [--prompt "..."] [--to telegram|slack:C01|mattermost:chan|homeassistant|sms|ntfy|signal|whatsapp|matrix|googlechat|bluebubbles|teams]
|
|
2442
|
+
${BRAND.cliName} webhook subscribe <route> --deliver telegram --deliver-chat-id 123 --deliver-only --prompt "New event: {__raw__}"
|
|
2443
|
+
${BRAND.cliName} webhook list
|
|
2444
|
+
${BRAND.cliName} webhook remove <route>
|
|
2445
|
+
${BRAND.cliName} webhook test <route> --payload '{"event_type":"ping"}'
|
|
167
2446
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const portIdx = args.indexOf('--port');
|
|
174
|
-
const port = portIdx !== -1 ? Number(args[portIdx + 1]) : 8787;
|
|
175
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
176
|
-
console.error(`port ไม่ถูกต้อง: ${args[portIdx + 1]}`);
|
|
177
|
-
process.exit(1);
|
|
2447
|
+
signature headers:
|
|
2448
|
+
GitHub: X-Hub-Signature-256: sha256=<hmac>
|
|
2449
|
+
GitLab: X-Gitlab-Token: <secret>
|
|
2450
|
+
Generic: X-Webhook-Signature: <hmac-hex>`);
|
|
2451
|
+
return;
|
|
178
2452
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
2453
|
+
if (action === 'subscribe' || action === 'add') {
|
|
2454
|
+
const name = positionalArgs(rest, valueFlags)[0];
|
|
2455
|
+
if (!name) {
|
|
2456
|
+
console.error(`ใช้: ${BRAND.cliName} webhook subscribe <route> [--prompt "..."] [--to <target>]`);
|
|
2457
|
+
process.exit(2);
|
|
2458
|
+
}
|
|
2459
|
+
const { isValidWebhookRouteName, generateWebhookSecret } = await import('./gateway/webhooks.js');
|
|
2460
|
+
if (!isValidWebhookRouteName(name)) {
|
|
2461
|
+
console.error('route ต้องเป็น a-z/A-Z/0-9/_/- ความยาวไม่เกิน 64 และต้องขึ้นต้นด้วยตัวอักษรหรือตัวเลข');
|
|
2462
|
+
process.exit(2);
|
|
2463
|
+
}
|
|
2464
|
+
const prompt = argValue(rest, '--prompt');
|
|
2465
|
+
const description = argValue(rest, '--description');
|
|
2466
|
+
const events = parseStringCsv(argValue(rest, '--events')).map((event) => event.trim()).filter(Boolean);
|
|
2467
|
+
const deliver = webhookDeliverTarget(rest);
|
|
2468
|
+
const deliverOnly = rest.includes('--deliver-only');
|
|
2469
|
+
const insecureNoAuth = rest.includes('--insecure-no-auth');
|
|
2470
|
+
const routeSecret = insecureNoAuth ? 'INSECURE_NO_AUTH' : (argValue(rest, '--secret')?.trim() || generateWebhookSecret());
|
|
2471
|
+
const publicUrl = argValue(rest, '--public-url');
|
|
2472
|
+
const rateLimitRaw = argValue(rest, '--rate-limit', '--rate-limit-per-minute');
|
|
2473
|
+
const rateLimitPerMinute = rateLimitRaw ? parsePort(rateLimitRaw, 30, 'webhook route rate limit') : undefined;
|
|
2474
|
+
if (deliverOnly && deliver === 'log') {
|
|
2475
|
+
console.error('--deliver-only ต้องมี --to หรือ --deliver เป็น messaging target จริง');
|
|
2476
|
+
process.exit(2);
|
|
2477
|
+
}
|
|
2478
|
+
if (deliver !== 'log') {
|
|
2479
|
+
const { parseSendTarget } = await import('./gateway/targets.js');
|
|
2480
|
+
try {
|
|
2481
|
+
parseSendTarget(deliver);
|
|
2482
|
+
}
|
|
2483
|
+
catch (e) {
|
|
2484
|
+
console.error(e.message);
|
|
2485
|
+
process.exit(2);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
const { patchGatewayConfig, readGatewayConfig } = await import('./gateway/config.js');
|
|
2489
|
+
const current = await readGatewayConfig();
|
|
2490
|
+
await patchGatewayConfig({
|
|
2491
|
+
webhooks: {
|
|
2492
|
+
enabled: true,
|
|
2493
|
+
publicUrl: publicUrl?.trim() || current.webhooks?.publicUrl,
|
|
2494
|
+
routes: {
|
|
2495
|
+
[name]: {
|
|
2496
|
+
events,
|
|
2497
|
+
secret: routeSecret,
|
|
2498
|
+
prompt: prompt?.trim() || undefined,
|
|
2499
|
+
deliver,
|
|
2500
|
+
deliverOnly,
|
|
2501
|
+
description: description?.trim() || undefined,
|
|
2502
|
+
rateLimitPerMinute,
|
|
2503
|
+
},
|
|
2504
|
+
},
|
|
2505
|
+
},
|
|
2506
|
+
});
|
|
2507
|
+
const base = (publicUrl?.trim() || current.webhooks?.publicUrl || 'http://127.0.0.1:8787').replace(/\/+$/, '');
|
|
2508
|
+
console.log(`เพิ่ม webhook route "${name}" แล้ว`);
|
|
2509
|
+
console.log(`URL: ${base}/webhooks/${name}`);
|
|
2510
|
+
console.log(`secret: ${routeSecret}`);
|
|
2511
|
+
console.log(`test: ${BRAND.cliName} webhook test ${name} --payload '{"event_type":"ping"}'`);
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
if (action === 'list' || action === undefined) {
|
|
2515
|
+
const { readGatewayConfig, resolveWebhookConfig } = await import('./gateway/config.js');
|
|
2516
|
+
const cfg = await readGatewayConfig();
|
|
2517
|
+
const webhooks = resolveWebhookConfig(cfg);
|
|
2518
|
+
const routes = Object.values(webhooks.routes);
|
|
2519
|
+
if (!routes.length) {
|
|
2520
|
+
console.log(`ยังไม่มี webhook route — เพิ่มด้วย: ${BRAND.cliName} webhook subscribe <route> --prompt "Event: {__raw__}"`);
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2523
|
+
const base = (webhooks.publicUrl || 'http://127.0.0.1:8787').replace(/\/+$/, '');
|
|
2524
|
+
for (const route of routes) {
|
|
2525
|
+
const events = route.events.length ? route.events.join(',') : '*';
|
|
2526
|
+
const mode = route.deliverOnly ? 'direct' : 'agent';
|
|
2527
|
+
console.log(`${route.name.padEnd(20)} ${mode.padEnd(6)} events:${events.padEnd(12)} deliver:${route.deliver} ${base}/webhooks/${route.name}`);
|
|
2528
|
+
}
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
if (action === 'remove' || action === 'rm') {
|
|
2532
|
+
const name = positionalArgs(rest, valueFlags)[0];
|
|
2533
|
+
if (!name) {
|
|
2534
|
+
console.error(`ใช้: ${BRAND.cliName} webhook remove <route>`);
|
|
2535
|
+
process.exit(2);
|
|
2536
|
+
}
|
|
2537
|
+
const { readGatewayConfig, writeGatewayConfig } = await import('./gateway/config.js');
|
|
2538
|
+
const cfg = await readGatewayConfig();
|
|
2539
|
+
if (!cfg.webhooks?.routes?.[name]) {
|
|
2540
|
+
console.log(`ไม่พบ webhook route "${name}"`);
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
const routes = { ...cfg.webhooks.routes };
|
|
2544
|
+
delete routes[name];
|
|
2545
|
+
await writeGatewayConfig({ ...cfg, webhooks: { ...cfg.webhooks, routes } });
|
|
2546
|
+
console.log(`ลบ webhook route "${name}" แล้ว`);
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
if (action === 'test') {
|
|
2550
|
+
const name = positionalArgs(rest, valueFlags)[0];
|
|
2551
|
+
if (!name) {
|
|
2552
|
+
console.error(`ใช้: ${BRAND.cliName} webhook test <route> [--payload <json>]`);
|
|
2553
|
+
process.exit(2);
|
|
2554
|
+
}
|
|
2555
|
+
const payload = argValue(rest, '--payload') ?? '{"event_type":"ping"}';
|
|
2556
|
+
let rawBody;
|
|
2557
|
+
let parsedPayload;
|
|
2558
|
+
try {
|
|
2559
|
+
parsedPayload = JSON.parse(payload);
|
|
2560
|
+
rawBody = JSON.stringify(parsedPayload);
|
|
2561
|
+
}
|
|
2562
|
+
catch {
|
|
2563
|
+
console.error('--payload ต้องเป็น JSON object/string ที่ parse ได้');
|
|
2564
|
+
process.exit(2);
|
|
2565
|
+
}
|
|
2566
|
+
const { readGatewayConfig, resolveWebhookConfig } = await import('./gateway/config.js');
|
|
2567
|
+
const { handleWebhookRequest } = await import('./gateway/webhooks.js');
|
|
2568
|
+
const cfg = resolveWebhookConfig(await readGatewayConfig());
|
|
2569
|
+
const route = cfg.routes[name];
|
|
2570
|
+
if (!route) {
|
|
2571
|
+
console.error(`ไม่พบ webhook route "${name}"`);
|
|
2572
|
+
process.exit(2);
|
|
2573
|
+
}
|
|
2574
|
+
const secret = route.secret || cfg.secret;
|
|
2575
|
+
const eventType = parsedPayload && typeof parsedPayload === 'object' && typeof parsedPayload.event_type === 'string'
|
|
2576
|
+
? parsedPayload.event_type
|
|
2577
|
+
: 'ping';
|
|
2578
|
+
const headers = { 'x-event-type': eventType };
|
|
2579
|
+
if (secret && secret !== 'INSECURE_NO_AUTH') {
|
|
2580
|
+
const { createHmac } = await import('node:crypto');
|
|
2581
|
+
headers['x-webhook-signature'] = createHmac('sha256', secret).update(rawBody).digest('hex');
|
|
2582
|
+
headers['x-request-id'] = `sanook-test-${Date.now()}`;
|
|
2583
|
+
}
|
|
2584
|
+
const appCfg = await loadConfig({});
|
|
2585
|
+
const result = await handleWebhookRequest({
|
|
2586
|
+
routeName: name,
|
|
2587
|
+
rawBody,
|
|
2588
|
+
headers,
|
|
2589
|
+
config: cfg,
|
|
2590
|
+
model: appCfg.model,
|
|
2591
|
+
budgetUsd: appCfg.budgetUsd,
|
|
2592
|
+
permissionMode: appCfg.permissionMode,
|
|
2593
|
+
onLog: (m) => process.stderr.write(`${DIM}${m}${RESET}\n`),
|
|
2594
|
+
});
|
|
2595
|
+
console.log(JSON.stringify(result.body, null, 2));
|
|
2596
|
+
if (result.status >= 400)
|
|
2597
|
+
process.exit(1);
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
console.error(`ไม่รู้จัก: webhook ${action} — ใช้ subscribe / list / remove / test`);
|
|
2601
|
+
process.exit(2);
|
|
2602
|
+
}
|
|
2603
|
+
function webhookDeliverTarget(args) {
|
|
2604
|
+
const direct = argValue(args, '--to', '-t')?.trim();
|
|
2605
|
+
if (direct)
|
|
2606
|
+
return direct;
|
|
2607
|
+
const deliver = argValue(args, '--deliver')?.trim();
|
|
2608
|
+
if (!deliver || deliver === 'log')
|
|
2609
|
+
return 'log';
|
|
2610
|
+
const chat = argValue(args, '--deliver-chat-id', '--chat-id')?.trim();
|
|
2611
|
+
return chat ? `${deliver}:${chat}` : deliver;
|
|
198
2612
|
}
|
|
199
2613
|
/** sanook cron add "<when>" "<task>" | cron list | cron rm <id> */
|
|
200
2614
|
async function runCron(args) {
|
|
201
2615
|
const [action, ...rest] = args;
|
|
202
2616
|
const { listTasks, enqueueTask, removeTask } = await import('./gateway/ledger.js');
|
|
2617
|
+
const valueFlags = ['--to', '-t', '--model', '-m'];
|
|
203
2618
|
if (action === 'add') {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
2619
|
+
const deliverRaw = argValue(rest, '--to', '-t')?.trim();
|
|
2620
|
+
const model = argValue(rest, '--model', '-m');
|
|
2621
|
+
const positionals = positionalArgs(rest, valueFlags);
|
|
2622
|
+
const schedule = positionals[0];
|
|
2623
|
+
const spec = positionals.slice(1).join(' ').trim();
|
|
206
2624
|
if (!schedule || !spec) {
|
|
207
|
-
console.error(
|
|
2625
|
+
console.error(`ใช้: ${BRAND.cliName} cron add "<when>" "<task>" [--to <target>] [--model <provider:model>]`);
|
|
208
2626
|
console.error('หมายเหตุ: when ที่มีช่องว่างต้องครอบ quote เช่น "every 30m"');
|
|
209
2627
|
process.exit(1);
|
|
210
2628
|
}
|
|
@@ -217,14 +2635,30 @@ async function runCron(args) {
|
|
|
217
2635
|
}
|
|
218
2636
|
process.exit(1);
|
|
219
2637
|
}
|
|
2638
|
+
let deliver;
|
|
2639
|
+
if (deliverRaw) {
|
|
2640
|
+
const { parseSendTarget, formatTarget } = await import('./gateway/targets.js');
|
|
2641
|
+
try {
|
|
2642
|
+
deliver = formatTarget(parseSendTarget(deliverRaw));
|
|
2643
|
+
}
|
|
2644
|
+
catch (e) {
|
|
2645
|
+
console.error(e.message);
|
|
2646
|
+
process.exit(2);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
220
2649
|
const task = await enqueueTask({
|
|
221
2650
|
kind: sched.recurring ? 'cron' : 'once',
|
|
222
2651
|
spec,
|
|
223
2652
|
schedule: sched.recurring ? sched.normalized : undefined,
|
|
2653
|
+
model,
|
|
2654
|
+
deliver,
|
|
224
2655
|
runAt: sched.runAt,
|
|
225
2656
|
});
|
|
226
2657
|
const when = new Date(task.runAt).toLocaleString();
|
|
227
|
-
|
|
2658
|
+
const extras = [task.deliver ? `ส่งไป ${task.deliver}` : undefined, task.model ? `model ${task.model}` : undefined]
|
|
2659
|
+
.filter(Boolean)
|
|
2660
|
+
.join(' · ');
|
|
2661
|
+
console.log(`เพิ่ม task ${task.id} — รัน ${when}${sched.recurring ? ` แล้วทุก ${sched.normalized}` : ''}${extras ? ` · ${extras}` : ''}`);
|
|
228
2662
|
return;
|
|
229
2663
|
}
|
|
230
2664
|
if (action === 'rm' || action === 'remove') {
|
|
@@ -244,7 +2678,8 @@ async function runCron(args) {
|
|
|
244
2678
|
}
|
|
245
2679
|
for (const t of tasks) {
|
|
246
2680
|
const next = new Date(t.runAt).toLocaleString();
|
|
247
|
-
|
|
2681
|
+
const extras = [t.deliver ? `to:${t.deliver}` : undefined, t.model ? `model:${t.model}` : undefined].filter(Boolean).join(' ');
|
|
2682
|
+
console.log(`${t.id} [${t.status}] ${t.schedule ?? 'once'} next:${next}${extras ? ` ${extras}` : ''} → ${t.spec.slice(0, 50)}`);
|
|
248
2683
|
}
|
|
249
2684
|
return;
|
|
250
2685
|
}
|
|
@@ -333,10 +2768,122 @@ async function runModels(args) {
|
|
|
333
2768
|
else
|
|
334
2769
|
console.log('\n✓ ทุก curated id มีใน provider');
|
|
335
2770
|
}
|
|
2771
|
+
function brainDoctorStatusLabel(status) {
|
|
2772
|
+
return status.toUpperCase().padEnd(4);
|
|
2773
|
+
}
|
|
2774
|
+
/** sanook brain doctor — check configured second-brain health without modifying it */
|
|
2775
|
+
async function runBrainDoctor() {
|
|
2776
|
+
const cfg = await loadConfig({});
|
|
2777
|
+
const { checkBrain } = await import('./brain-doctor.js');
|
|
2778
|
+
const report = await checkBrain({ brainPath: cfg.brainPath });
|
|
2779
|
+
console.log(`${BRAND.productName} brain doctor`);
|
|
2780
|
+
for (const check of report.checks) {
|
|
2781
|
+
console.log(`[${brainDoctorStatusLabel(check.status)}] ${check.id} — ${check.message}`);
|
|
2782
|
+
if (check.path)
|
|
2783
|
+
console.log(` ${check.path}`);
|
|
2784
|
+
for (const detail of check.details ?? [])
|
|
2785
|
+
console.log(` - ${detail}`);
|
|
2786
|
+
}
|
|
2787
|
+
if (!report.ok)
|
|
2788
|
+
process.exit(1);
|
|
2789
|
+
}
|
|
2790
|
+
/** sanook brain context [--task "..."] — show the prompt context Sanook loads from the vault */
|
|
2791
|
+
async function runBrainContext(args) {
|
|
2792
|
+
const { parseBrainContextArgs, inspectBrainContext, formatBrainContextReport } = await import('./brain-context.js');
|
|
2793
|
+
const parsed = parseBrainContextArgs(args);
|
|
2794
|
+
if (!parsed.ok) {
|
|
2795
|
+
console.error(parsed.message);
|
|
2796
|
+
console.error(`ใช้: ${BRAND.cliName} brain context [--task "..."] [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,session,skill] [--no-content]`);
|
|
2797
|
+
process.exit(1);
|
|
2798
|
+
}
|
|
2799
|
+
const cfg = await loadConfig({});
|
|
2800
|
+
const report = await inspectBrainContext({
|
|
2801
|
+
brainPath: cfg.brainPath,
|
|
2802
|
+
task: parsed.value.task,
|
|
2803
|
+
mode: parsed.value.mode,
|
|
2804
|
+
limit: parsed.value.limit,
|
|
2805
|
+
sources: parsed.value.sources,
|
|
2806
|
+
});
|
|
2807
|
+
console.log(formatBrainContextReport(report, parsed.value.showContent));
|
|
2808
|
+
if (!report.ok)
|
|
2809
|
+
process.exit(1);
|
|
2810
|
+
}
|
|
2811
|
+
/** sanook brain eval — run the lightweight second-brain benchmark sanity checks */
|
|
2812
|
+
async function runBrainEval(args) {
|
|
2813
|
+
const allowed = new Set(['--no-retrieval']);
|
|
2814
|
+
const unknown = args.filter((arg) => !allowed.has(arg));
|
|
2815
|
+
if (unknown.length) {
|
|
2816
|
+
console.error(`ไม่รู้จัก option: ${unknown.join(' ')}`);
|
|
2817
|
+
console.error(`ใช้: ${BRAND.cliName} brain eval [--no-retrieval]`);
|
|
2818
|
+
process.exit(1);
|
|
2819
|
+
}
|
|
2820
|
+
const cfg = await loadConfig({});
|
|
2821
|
+
const { runBrainEval: evaluate, formatBrainEvalReport } = await import('./brain-eval.js');
|
|
2822
|
+
const report = await evaluate({ brainPath: cfg.brainPath, runRetrieval: !args.includes('--no-retrieval') });
|
|
2823
|
+
console.log(formatBrainEvalReport(report));
|
|
2824
|
+
if (!report.ok)
|
|
2825
|
+
process.exit(1);
|
|
2826
|
+
}
|
|
2827
|
+
/** sanook brain review — curator health review over the configured second-brain vault */
|
|
2828
|
+
async function runBrainReview(args) {
|
|
2829
|
+
const { parseBrainReviewArgs, reviewBrain, formatBrainReviewReport } = await import('./brain-review.js');
|
|
2830
|
+
const parsed = parseBrainReviewArgs(args);
|
|
2831
|
+
if (!parsed.ok) {
|
|
2832
|
+
console.error(parsed.message);
|
|
2833
|
+
console.error(`ใช้: ${BRAND.cliName} brain review [--no-hygiene]`);
|
|
2834
|
+
process.exit(1);
|
|
2835
|
+
}
|
|
2836
|
+
const cfg = await loadConfig({});
|
|
2837
|
+
const report = await reviewBrain({
|
|
2838
|
+
brainPath: cfg.brainPath,
|
|
2839
|
+
scanMarkdownHygiene: parsed.value.scanMarkdownHygiene,
|
|
2840
|
+
});
|
|
2841
|
+
console.log(formatBrainReviewReport(report));
|
|
2842
|
+
if (!report.ok)
|
|
2843
|
+
process.exit(1);
|
|
2844
|
+
}
|
|
2845
|
+
/** sanook brain final [--task "..."] [--from-diff] [--lite] — create an evidence-backed closeout note */
|
|
2846
|
+
async function runBrainFinal(args) {
|
|
2847
|
+
const { parseBrainFinalArgs, createBrainFinal, formatBrainFinalReport } = await import('./brain-final.js');
|
|
2848
|
+
const parsed = parseBrainFinalArgs(args);
|
|
2849
|
+
if (!parsed.ok) {
|
|
2850
|
+
console.error(parsed.message);
|
|
2851
|
+
console.error(`ใช้: ${BRAND.cliName} brain final [--task "..."] [--from-diff] [--lite] [--output Sessions/name.md] [--force]`);
|
|
2852
|
+
process.exit(1);
|
|
2853
|
+
}
|
|
2854
|
+
const cfg = await loadConfig({});
|
|
2855
|
+
const report = await createBrainFinal({
|
|
2856
|
+
brainPath: cfg.brainPath,
|
|
2857
|
+
today: new Date().toISOString().slice(0, 10),
|
|
2858
|
+
...parsed.value,
|
|
2859
|
+
});
|
|
2860
|
+
console.log(formatBrainFinalReport(report));
|
|
2861
|
+
if (!report.ok)
|
|
2862
|
+
process.exit(1);
|
|
2863
|
+
}
|
|
336
2864
|
/** sanook brain init [path] — scaffold second-brain workspace (interactive ถ้าไม่ใส่ path) */
|
|
337
2865
|
async function runBrain(args) {
|
|
2866
|
+
if (args[0] === 'doctor')
|
|
2867
|
+
return runBrainDoctor();
|
|
2868
|
+
if (args[0] === 'context')
|
|
2869
|
+
return runBrainContext(args.slice(1));
|
|
2870
|
+
if (args[0] === 'eval')
|
|
2871
|
+
return runBrainEval(args.slice(1));
|
|
2872
|
+
if (args[0] === 'review')
|
|
2873
|
+
return runBrainReview(args.slice(1));
|
|
2874
|
+
if (args[0] === 'final')
|
|
2875
|
+
return runBrainFinal(args.slice(1));
|
|
338
2876
|
if (args[0] !== 'init') {
|
|
339
|
-
console.log(`ใช้:
|
|
2877
|
+
console.log(`ใช้:
|
|
2878
|
+
sanook brain init [path] สร้างโครงสร้าง second-brain (Obsidian vault)
|
|
2879
|
+
sanook brain doctor ตรวจ health ของ second-brain ที่ config.brainPath
|
|
2880
|
+
sanook brain context แสดง context ที่ Sanook จะ inject
|
|
2881
|
+
sanook brain context --task "..." ดู retrieval hits ต่อ task
|
|
2882
|
+
sanook brain eval รัน second-brain benchmark sanity checks
|
|
2883
|
+
sanook brain review curator review: inbox, packs, sessions, evals, note hygiene
|
|
2884
|
+
sanook brain final --task "..." [--from-diff] [--lite]
|
|
2885
|
+
สร้าง final gate note ใน Sessions
|
|
2886
|
+
|
|
340
2887
|
ไม่ใส่ path → wizard ถาม path + ตัวตน
|
|
341
2888
|
-y, --yes ใช้ค่า default ทั้งหมด (ต้องระบุ path)`);
|
|
342
2889
|
return;
|
|
@@ -379,7 +2926,22 @@ async function readStdin() {
|
|
|
379
2926
|
async function runConfig(args) {
|
|
380
2927
|
const { readGlobalConfigRaw, patchGlobalConfig } = await import('./config.js');
|
|
381
2928
|
const [action, key, ...rest] = args;
|
|
382
|
-
const ALLOWED = [
|
|
2929
|
+
const ALLOWED = [
|
|
2930
|
+
'model',
|
|
2931
|
+
'fallbackModel',
|
|
2932
|
+
'budgetUsd',
|
|
2933
|
+
'maxSteps',
|
|
2934
|
+
'permissionMode',
|
|
2935
|
+
'brainPath',
|
|
2936
|
+
'pricing',
|
|
2937
|
+
'cacheTtl',
|
|
2938
|
+
'compaction',
|
|
2939
|
+
'contextCompression',
|
|
2940
|
+
'thinking',
|
|
2941
|
+
'summaryModel',
|
|
2942
|
+
'embeddingModel',
|
|
2943
|
+
'personality',
|
|
2944
|
+
];
|
|
383
2945
|
if (action === 'set') {
|
|
384
2946
|
if (!key || rest.length === 0) {
|
|
385
2947
|
console.error(`ใช้: ${BRAND.cliName} config set <key> <value> (key: ${ALLOWED.join(' | ')})`);
|
|
@@ -392,8 +2954,8 @@ async function runConfig(args) {
|
|
|
392
2954
|
const raw = rest.join(' ');
|
|
393
2955
|
let value = raw;
|
|
394
2956
|
if (key === 'budgetUsd') {
|
|
395
|
-
const n =
|
|
396
|
-
if (
|
|
2957
|
+
const n = parseBudgetUsd(raw);
|
|
2958
|
+
if (n === undefined) {
|
|
397
2959
|
console.error('budgetUsd ต้องเป็นตัวเลขบวก เช่น 0.25');
|
|
398
2960
|
process.exit(1);
|
|
399
2961
|
}
|
|
@@ -419,20 +2981,26 @@ async function runConfig(args) {
|
|
|
419
2981
|
console.error('compaction ต้องเป็น truncate หรือ summarize');
|
|
420
2982
|
process.exit(1);
|
|
421
2983
|
}
|
|
2984
|
+
else if (key === 'contextCompression' && raw !== 'off' && raw !== 'selective' && raw !== 'headroom') {
|
|
2985
|
+
console.error('contextCompression ต้องเป็น off, selective หรือ headroom');
|
|
2986
|
+
process.exit(1);
|
|
2987
|
+
}
|
|
422
2988
|
else if (key === 'thinking') {
|
|
423
2989
|
// เก็บเป็น number (budget) หรือ boolean ให้ตรง ConfigSchema (ไม่เก็บ string)
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
2990
|
+
value = parseThinkingConfigValue(raw);
|
|
2991
|
+
if (value === undefined) {
|
|
2992
|
+
console.error('thinking ต้องเป็น on/off หรือ budget tokens (integer บวก เช่น 4000)');
|
|
2993
|
+
process.exit(1);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
else if (key === 'personality') {
|
|
2997
|
+
const { normalizePersonalityName, personalityListText } = await import('./personality.js');
|
|
2998
|
+
const name = normalizePersonalityName(raw);
|
|
2999
|
+
if (!name) {
|
|
3000
|
+
console.error(`personality ไม่รู้จัก: ${raw}\n${personalityListText()}`);
|
|
3001
|
+
process.exit(1);
|
|
435
3002
|
}
|
|
3003
|
+
value = name === 'none' ? undefined : name;
|
|
436
3004
|
}
|
|
437
3005
|
else if (key === 'pricing') {
|
|
438
3006
|
try {
|
|
@@ -459,33 +3027,24 @@ async function runIndex(_args) {
|
|
|
459
3027
|
const { reindex } = await import('./search/indexer.js');
|
|
460
3028
|
console.log('indexing…');
|
|
461
3029
|
const r = await reindex();
|
|
3030
|
+
const { resetSearchCaches } = await import('./search/engine.js');
|
|
3031
|
+
resetSearchCaches();
|
|
462
3032
|
console.log(`done: +${r.added} ~${r.updated} -${r.removed} (skipped ${r.skipped}) · ` +
|
|
463
|
-
`memory=${r.memory} sessions=${r.sessions} skills=${r.skills}
|
|
3033
|
+
`memory=${r.memory} sessions=${r.sessions} skills=${r.skills} vectors=${r.vectors}\n` +
|
|
3034
|
+
`vault: ${r.vaultPath ?? '(not set — `' + BRAND.cliName + ' brain init` or set config.brainPath)'}`);
|
|
464
3035
|
}
|
|
465
3036
|
/** sanook search "<query>" [--mode ..] [--limit N] [--source a,b] — one-shot ranked search */
|
|
466
3037
|
async function runSearch(args) {
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
for (let i = 0; i < args.length; i++) {
|
|
472
|
-
const a = args[i];
|
|
473
|
-
if (a === '--mode')
|
|
474
|
-
mode = args[++i] ?? 'auto';
|
|
475
|
-
else if (a === '--limit')
|
|
476
|
-
limit = Number.parseInt(args[++i] ?? '8', 10) || 8;
|
|
477
|
-
else if (a === '--source' || a === '--sources')
|
|
478
|
-
sources = (args[++i] ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
479
|
-
else
|
|
480
|
-
queryParts.push(a);
|
|
481
|
-
}
|
|
482
|
-
const query = queryParts.join(' ').trim();
|
|
483
|
-
if (!query) {
|
|
3038
|
+
const { parseSearchArgs } = await import('./search/cli.js');
|
|
3039
|
+
const parsed = parseSearchArgs(args);
|
|
3040
|
+
if (!parsed.ok) {
|
|
3041
|
+
console.error(parsed.message);
|
|
484
3042
|
console.error(`ใช้: ${BRAND.cliName} search "<query>" [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,memory]`);
|
|
485
3043
|
process.exit(1);
|
|
486
3044
|
}
|
|
3045
|
+
const { query, mode, limit, sources } = parsed.value;
|
|
487
3046
|
const { search } = await import('./search/engine.js');
|
|
488
|
-
const res = await search(query, { mode
|
|
3047
|
+
const res = await search(query, { mode, limit, sources });
|
|
489
3048
|
if (res.degraded)
|
|
490
3049
|
console.log(`${DIM}(mode=${res.mode}, degraded: ${res.degraded})${RESET}`);
|
|
491
3050
|
else
|
|
@@ -506,24 +3065,183 @@ async function runMcpServe() {
|
|
|
506
3065
|
const { runMcpServer } = await import('./mcp-server.js');
|
|
507
3066
|
await runMcpServer();
|
|
508
3067
|
}
|
|
509
|
-
/** sanook mcp [list
|
|
3068
|
+
/** sanook mcp [search|info|install|test|doctor|list|add|remove] — จัดการ ~/.sanook/mcp.json */
|
|
510
3069
|
async function runMcp(args) {
|
|
3070
|
+
const readConfig = async (path) => {
|
|
3071
|
+
try {
|
|
3072
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
3073
|
+
return { mcpServers: parsed.mcpServers ?? {} };
|
|
3074
|
+
}
|
|
3075
|
+
catch {
|
|
3076
|
+
return { mcpServers: {} };
|
|
3077
|
+
}
|
|
3078
|
+
};
|
|
3079
|
+
const writeConfig = async (path, cfg) => {
|
|
3080
|
+
await mkdir(dirname(path), { recursive: true });
|
|
3081
|
+
await writeFile(path, `${JSON.stringify(cfg, null, 2)}\n`, { mode: 0o600 });
|
|
3082
|
+
await chmod(path, 0o600).catch(() => { });
|
|
3083
|
+
};
|
|
511
3084
|
const mcpPath = appHomePath('mcp.json');
|
|
512
|
-
let cfg =
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
3085
|
+
let cfg = await readConfig(mcpPath);
|
|
3086
|
+
const [action = 'list', ...rest] = args;
|
|
3087
|
+
const positionals = (items, valueFlags = new Set()) => {
|
|
3088
|
+
const out = [];
|
|
3089
|
+
for (let i = 0; i < items.length; i++) {
|
|
3090
|
+
const item = items[i];
|
|
3091
|
+
if (!item.startsWith('--')) {
|
|
3092
|
+
out.push(item);
|
|
3093
|
+
continue;
|
|
3094
|
+
}
|
|
3095
|
+
const flag = item.includes('=') ? item.slice(0, item.indexOf('=')) : item;
|
|
3096
|
+
if (!item.includes('=') && valueFlags.has(flag) && items[i + 1])
|
|
3097
|
+
i++;
|
|
3098
|
+
}
|
|
3099
|
+
return out;
|
|
3100
|
+
};
|
|
3101
|
+
if (action === 'search') {
|
|
3102
|
+
const { searchMcpRegistry, formatRegistrySearch, parseMcpRegistrySearchArgs } = await import('./mcp-registry.js');
|
|
3103
|
+
const parsed = parseMcpRegistrySearchArgs(rest);
|
|
3104
|
+
if (!parsed.ok) {
|
|
3105
|
+
console.error(parsed.message);
|
|
3106
|
+
process.exit(1);
|
|
3107
|
+
}
|
|
3108
|
+
const result = await searchMcpRegistry(parsed.value.query, { limit: parsed.value.limit, cursor: parsed.value.cursor });
|
|
3109
|
+
console.log(formatRegistrySearch(result));
|
|
3110
|
+
return;
|
|
516
3111
|
}
|
|
517
|
-
|
|
518
|
-
|
|
3112
|
+
if (action === 'info') {
|
|
3113
|
+
const { getMcpRegistryServer, formatRegistryInfo } = await import('./mcp-registry.js');
|
|
3114
|
+
const name = positionals(rest, new Set(['--version']))[0];
|
|
3115
|
+
const versionArg = rest.find((item) => item.startsWith('--version='));
|
|
3116
|
+
const version = versionArg?.slice('--version='.length) ?? (rest.includes('--version') ? rest[rest.indexOf('--version') + 1] : undefined);
|
|
3117
|
+
if (!name) {
|
|
3118
|
+
console.error(`ใช้: ${BRAND.cliName} mcp info <registry-server-name> [--version=x.y.z]`);
|
|
3119
|
+
process.exit(1);
|
|
3120
|
+
}
|
|
3121
|
+
const server = await getMcpRegistryServer(name, { version });
|
|
3122
|
+
if (!server) {
|
|
3123
|
+
console.error(`ไม่เจอ MCP registry server: ${name}`);
|
|
3124
|
+
process.exit(1);
|
|
3125
|
+
}
|
|
3126
|
+
console.log(formatRegistryInfo(server));
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
if (action === 'preset') {
|
|
3130
|
+
const { formatPreset } = await import('./mcp-registry.js');
|
|
3131
|
+
console.log(formatPreset(rest[0]));
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
if (action === 'install') {
|
|
3135
|
+
const { buildMcpInstallPlan, getMcpRegistryServer, parseKeyValueList, formatRegistryInfo, } = await import('./mcp-registry.js');
|
|
3136
|
+
const name = positionals(rest, new Set(['--name', '--transport', '--env', '--header', '--version']))[0];
|
|
3137
|
+
if (!name) {
|
|
3138
|
+
console.error(`ใช้: ${BRAND.cliName} mcp install <registry-server-name> [--name alias] [--transport auto|remote|stdio] [--env KEY=value] [--header KEY=value] [--project]`);
|
|
3139
|
+
process.exit(1);
|
|
3140
|
+
}
|
|
3141
|
+
const optionValues = (flag) => {
|
|
3142
|
+
const out = [];
|
|
3143
|
+
for (let i = 0; i < rest.length; i++) {
|
|
3144
|
+
if (rest[i] === flag && rest[i + 1])
|
|
3145
|
+
out.push(rest[++i]);
|
|
3146
|
+
else if (rest[i].startsWith(`${flag}=`))
|
|
3147
|
+
out.push(rest[i].slice(flag.length + 1));
|
|
3148
|
+
}
|
|
3149
|
+
return out;
|
|
3150
|
+
};
|
|
3151
|
+
const valueOf = (flag) => optionValues(flag)[0];
|
|
3152
|
+
const alias = valueOf('--name');
|
|
3153
|
+
if (alias && !isValidMcpServerName(alias)) {
|
|
3154
|
+
console.error('ชื่อ MCP server ต้องเป็น a-z/A-Z/0-9/_/- ความยาวไม่เกิน 64 และห้ามใช้ชื่อพิเศษ');
|
|
3155
|
+
process.exit(1);
|
|
3156
|
+
}
|
|
3157
|
+
const transport = valueOf('--transport');
|
|
3158
|
+
if (transport && !['auto', 'remote', 'stdio'].includes(transport)) {
|
|
3159
|
+
console.error('--transport ต้องเป็น auto, remote, หรือ stdio');
|
|
3160
|
+
process.exit(1);
|
|
3161
|
+
}
|
|
3162
|
+
const server = await getMcpRegistryServer(name, { version: valueOf('--version') });
|
|
3163
|
+
if (!server) {
|
|
3164
|
+
console.error(`ไม่เจอ MCP registry server: ${name}`);
|
|
3165
|
+
process.exit(1);
|
|
3166
|
+
}
|
|
3167
|
+
const plan = buildMcpInstallPlan(server, {
|
|
3168
|
+
alias,
|
|
3169
|
+
transport,
|
|
3170
|
+
env: parseKeyValueList(optionValues('--env')),
|
|
3171
|
+
headers: parseKeyValueList(optionValues('--header')),
|
|
3172
|
+
});
|
|
3173
|
+
if (!plan.ok) {
|
|
3174
|
+
console.log(formatRegistryInfo(server));
|
|
3175
|
+
console.error(`\nยัง install ไม่ได้: ต้องระบุ ${plan.missing.join(', ') || 'transport/package ที่รองรับ'}`);
|
|
3176
|
+
if (plan.missing.some((item) => item.startsWith('env:')))
|
|
3177
|
+
console.error(`ตัวอย่าง: ${BRAND.cliName} mcp install ${name} --env KEY=value`);
|
|
3178
|
+
if (plan.missing.some((item) => item.startsWith('header:')))
|
|
3179
|
+
console.error(`ตัวอย่าง: ${BRAND.cliName} mcp install ${name} --header Authorization='Bearer ...'`);
|
|
3180
|
+
for (const warning of plan.warnings)
|
|
3181
|
+
console.error(`warning: ${warning}`);
|
|
3182
|
+
process.exit(1);
|
|
3183
|
+
}
|
|
3184
|
+
let targetPath = mcpPath;
|
|
3185
|
+
if (rest.includes('--project')) {
|
|
3186
|
+
const { projectConfigPathIfTrusted, projectRoot } = await import('./trust.js');
|
|
3187
|
+
const root = await projectRoot(process.cwd());
|
|
3188
|
+
const projectPath = await projectConfigPathIfTrusted('mcp.json', root);
|
|
3189
|
+
if (!projectPath) {
|
|
3190
|
+
console.error(`project MCP ต้อง trust ก่อน: ${BRAND.cliName} trust add`);
|
|
3191
|
+
process.exit(1);
|
|
3192
|
+
}
|
|
3193
|
+
targetPath = projectPath;
|
|
3194
|
+
}
|
|
3195
|
+
cfg = await readConfig(targetPath);
|
|
3196
|
+
cfg.mcpServers[plan.alias] = plan.config;
|
|
3197
|
+
await writeConfig(targetPath, cfg);
|
|
3198
|
+
console.log(`ติดตั้ง MCP "${plan.alias}" จาก ${server.name} (${plan.source}) → ${targetPath}`);
|
|
3199
|
+
if (plan.requirements.length)
|
|
3200
|
+
console.log(`requirements: ${plan.requirements.join(', ')}`);
|
|
3201
|
+
for (const warning of plan.warnings)
|
|
3202
|
+
console.log(`warning: ${warning}`);
|
|
3203
|
+
console.log(`ทดสอบ: ${BRAND.cliName} mcp test ${plan.alias}`);
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
if (action === 'test' || action === 'doctor') {
|
|
3207
|
+
const { loadMcpConfig, probeMcpServer } = await import('./mcp.js');
|
|
3208
|
+
const logs = [];
|
|
3209
|
+
const merged = await loadMcpConfig((m) => logs.push(m));
|
|
3210
|
+
const names = action === 'test' && rest[0] ? [rest[0]] : Object.keys(merged);
|
|
3211
|
+
if (!names.length) {
|
|
3212
|
+
console.log(`ยังไม่มี MCP server — เพิ่ม: ${BRAND.cliName} mcp search github`);
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
let failed = false;
|
|
3216
|
+
if (logs.length)
|
|
3217
|
+
for (const log of logs)
|
|
3218
|
+
console.log(`note: ${log}`);
|
|
3219
|
+
for (const n of names) {
|
|
3220
|
+
const server = merged[n];
|
|
3221
|
+
if (!server) {
|
|
3222
|
+
failed = true;
|
|
3223
|
+
console.log(`[FAIL] ${n} — ไม่เจอใน config`);
|
|
3224
|
+
continue;
|
|
3225
|
+
}
|
|
3226
|
+
const probe = await probeMcpServer(server);
|
|
3227
|
+
if (probe.ok) {
|
|
3228
|
+
console.log(`[PASS] ${n} (${probe.transport}) — ${probe.tools.length} tool(s)`);
|
|
3229
|
+
for (const tool of probe.tools.slice(0, action === 'doctor' ? 8 : 30))
|
|
3230
|
+
console.log(` - ${tool.name}${tool.description ? ` — ${tool.description}` : ''}`);
|
|
3231
|
+
if (action === 'doctor' && probe.tools.length > 8)
|
|
3232
|
+
console.log(` ... ${probe.tools.length - 8} more`);
|
|
3233
|
+
}
|
|
3234
|
+
else {
|
|
3235
|
+
failed = true;
|
|
3236
|
+
console.log(`[FAIL] ${n} (${probe.transport}) — ${probe.error}`);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
if (failed)
|
|
3240
|
+
process.exit(1);
|
|
3241
|
+
return;
|
|
519
3242
|
}
|
|
520
|
-
const write = async () => {
|
|
521
|
-
await mkdir(dirname(mcpPath), { recursive: true });
|
|
522
|
-
await writeFile(mcpPath, `${JSON.stringify(cfg, null, 2)}\n`, { mode: 0o600 });
|
|
523
|
-
await chmod(mcpPath, 0o600).catch(() => { });
|
|
524
|
-
};
|
|
525
|
-
const [action, name, command, ...cmdArgs] = args;
|
|
526
3243
|
if (action === 'add') {
|
|
3244
|
+
const [name, command, ...cmdArgs] = rest;
|
|
527
3245
|
if (!name || !command) {
|
|
528
3246
|
console.error(`ใช้: ${BRAND.cliName} mcp add <name> <command> [args...] (เช่น: mcp add fs npx -y @modelcontextprotocol/server-filesystem /path)`);
|
|
529
3247
|
console.error(` remote: ${BRAND.cliName} mcp add <name> https://host/mcp (Streamable-HTTP)`);
|
|
@@ -535,30 +3253,50 @@ async function runMcp(args) {
|
|
|
535
3253
|
}
|
|
536
3254
|
// command เป็น http(s):// → remote MCP (Streamable-HTTP), ไม่งั้น stdio
|
|
537
3255
|
cfg.mcpServers[name] = /^https?:\/\//.test(command) ? { url: command } : { command, args: cmdArgs };
|
|
538
|
-
await
|
|
3256
|
+
await writeConfig(mcpPath, cfg);
|
|
539
3257
|
console.log(`เพิ่ม MCP server "${name}"${/^https?:\/\//.test(command) ? ' (remote http)' : ''}`);
|
|
540
3258
|
return;
|
|
541
3259
|
}
|
|
542
3260
|
if (action === 'remove' || action === 'rm') {
|
|
3261
|
+
const [name] = rest;
|
|
543
3262
|
if (name && cfg.mcpServers[name]) {
|
|
544
3263
|
delete cfg.mcpServers[name];
|
|
545
|
-
await
|
|
546
|
-
console.log(`ลบ MCP server "${name}"
|
|
3264
|
+
await writeConfig(mcpPath, cfg);
|
|
3265
|
+
console.log(`ลบ MCP server "${name}" แล้ว (${mcpPath})`);
|
|
547
3266
|
}
|
|
548
3267
|
else
|
|
549
3268
|
console.log(`ไม่เจอ MCP server "${name ?? ''}"`);
|
|
550
3269
|
return;
|
|
551
3270
|
}
|
|
552
|
-
|
|
3271
|
+
if (action !== 'list') {
|
|
3272
|
+
console.log(`ใช้: ${BRAND.cliName} mcp [search|info|install|test|doctor|preset|list|add|remove|serve]`);
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
const { loadMcpConfig } = await import('./mcp.js');
|
|
3276
|
+
const logs = [];
|
|
3277
|
+
const merged = await loadMcpConfig((m) => logs.push(m));
|
|
3278
|
+
const names = Object.keys(merged);
|
|
553
3279
|
if (!names.length) {
|
|
554
|
-
console.log(`ยังไม่มี MCP server — เพิ่ม: ${BRAND.cliName} mcp
|
|
3280
|
+
console.log(`ยังไม่มี MCP server — เพิ่ม: ${BRAND.cliName} mcp search github`);
|
|
555
3281
|
return;
|
|
556
3282
|
}
|
|
3283
|
+
if (logs.length)
|
|
3284
|
+
for (const log of logs)
|
|
3285
|
+
console.log(`note: ${log}`);
|
|
557
3286
|
console.log(`${names.length} MCP servers:`);
|
|
558
3287
|
for (const n of names) {
|
|
559
|
-
const s =
|
|
3288
|
+
const s = merged[n];
|
|
560
3289
|
console.log(` ${n} — ${s.url ? `${s.url} (http)` : `${s.command} ${(s.args ?? []).join(' ')}`}`);
|
|
561
3290
|
}
|
|
3291
|
+
if (rest.includes('--tools')) {
|
|
3292
|
+
const { probeMcpServer } = await import('./mcp.js');
|
|
3293
|
+
for (const n of names) {
|
|
3294
|
+
const probe = await probeMcpServer(merged[n]);
|
|
3295
|
+
console.log(`\n${probe.ok ? '[PASS]' : '[FAIL]'} ${n} tools${probe.ok ? ` (${probe.tools.length})` : ` — ${probe.error}`}`);
|
|
3296
|
+
for (const tool of probe.tools.slice(0, 30))
|
|
3297
|
+
console.log(` - ${tool.name}`);
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
562
3300
|
}
|
|
563
3301
|
/** sanook trust [status|add|remove] — trust project .sanook content that can steer/execute code */
|
|
564
3302
|
async function runTrust(args) {
|
|
@@ -642,6 +3380,66 @@ async function askYesNo(question) {
|
|
|
642
3380
|
rl.close();
|
|
643
3381
|
}
|
|
644
3382
|
}
|
|
3383
|
+
async function askText(question) {
|
|
3384
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3385
|
+
try {
|
|
3386
|
+
return (await rl.question(question)).trim();
|
|
3387
|
+
}
|
|
3388
|
+
finally {
|
|
3389
|
+
rl.close();
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
function extractValue(args, ...names) {
|
|
3393
|
+
const rest = [];
|
|
3394
|
+
let value;
|
|
3395
|
+
for (let i = 0; i < args.length; i++) {
|
|
3396
|
+
const a = args[i];
|
|
3397
|
+
const eq = names.find((name) => a.startsWith(`${name}=`));
|
|
3398
|
+
if (eq) {
|
|
3399
|
+
value ??= a.slice(eq.length + 1);
|
|
3400
|
+
continue;
|
|
3401
|
+
}
|
|
3402
|
+
if (names.includes(a)) {
|
|
3403
|
+
if (args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
3404
|
+
value ??= args[i + 1];
|
|
3405
|
+
i++;
|
|
3406
|
+
}
|
|
3407
|
+
continue;
|
|
3408
|
+
}
|
|
3409
|
+
rest.push(a);
|
|
3410
|
+
}
|
|
3411
|
+
return { value, rest };
|
|
3412
|
+
}
|
|
3413
|
+
function stripBooleanFlags(args, ...names) {
|
|
3414
|
+
return args.filter((a) => !names.includes(a));
|
|
3415
|
+
}
|
|
3416
|
+
function positionalArgs(args, valueFlags = []) {
|
|
3417
|
+
const out = [];
|
|
3418
|
+
for (let i = 0; i < args.length; i++) {
|
|
3419
|
+
const a = args[i];
|
|
3420
|
+
if (valueFlags.some((name) => a.startsWith(`${name}=`)))
|
|
3421
|
+
continue;
|
|
3422
|
+
if (valueFlags.includes(a)) {
|
|
3423
|
+
i++;
|
|
3424
|
+
continue;
|
|
3425
|
+
}
|
|
3426
|
+
if (a.startsWith('-'))
|
|
3427
|
+
continue;
|
|
3428
|
+
out.push(a);
|
|
3429
|
+
}
|
|
3430
|
+
return out;
|
|
3431
|
+
}
|
|
3432
|
+
function argValue(args, ...names) {
|
|
3433
|
+
for (const name of names) {
|
|
3434
|
+
const eq = args.find((a) => a.startsWith(`${name}=`));
|
|
3435
|
+
if (eq)
|
|
3436
|
+
return eq.slice(name.length + 1);
|
|
3437
|
+
const idx = args.indexOf(name);
|
|
3438
|
+
if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith('--'))
|
|
3439
|
+
return args[idx + 1];
|
|
3440
|
+
}
|
|
3441
|
+
return undefined;
|
|
3442
|
+
}
|
|
645
3443
|
async function maybePromptForInteractiveUpdate() {
|
|
646
3444
|
if (envFlag(BRAND_ENV.disableUpdateCheck) || process.env.CI)
|
|
647
3445
|
return;
|
|
@@ -686,8 +3484,12 @@ function headlessKeyHint(modelSpec) {
|
|
|
686
3484
|
`⚠ ยังไม่มี API key สำหรับ ${cfg.label} (${cfg.envVar})`,
|
|
687
3485
|
`เริ่มใช้งาน:`,
|
|
688
3486
|
` • รัน "${BRAND.cliName}" (ไม่ใส่ task) → setup wizard ทีละขั้น (แนะนำ)`,
|
|
3487
|
+
` • หรือ: ${BRAND.cliName} auth add ${provider} --api-key "..." --use${url ? ` · เอา key ที่: ${url}` : ''}`,
|
|
689
3488
|
` • หรือ: export ${cfg.envVar}="..."${url ? ` · เอา key ที่: ${url}` : ''}`,
|
|
690
3489
|
];
|
|
3490
|
+
if (provider === 'openai') {
|
|
3491
|
+
lines.push(` • ถ้าต้องการใช้ ChatGPT plan ไม่ใช้ API key: ใช้ ${BRAND.cliName} -m codex แล้วรัน codex login`);
|
|
3492
|
+
}
|
|
691
3493
|
const other = detectEnvProvider();
|
|
692
3494
|
if (other && other.provider !== provider) {
|
|
693
3495
|
lines.push(` • เจอ key ของ ${other.label} อยู่แล้ว → ใช้เลย: ${BRAND.cliName} -m ${other.provider} "<task>"`);
|
|
@@ -722,8 +3524,35 @@ async function main() {
|
|
|
722
3524
|
// โหลด API key จาก ~/.sanook/auth.json เข้า env (ไม่ override env ที่ตั้งไว้แล้ว)
|
|
723
3525
|
await loadKeysIntoEnv();
|
|
724
3526
|
process.on('exit', closeMcp); // ปิด MCP server (kill child) ตอนจบ
|
|
3527
|
+
// management surfaces (Sanook-branded) — setup/model/gateway/status/tools/send
|
|
3528
|
+
if (argv[0] === '-z')
|
|
3529
|
+
return runPureOneShot(argv.slice(1));
|
|
3530
|
+
if (argv[0] === 'chat')
|
|
3531
|
+
return runChat(argv.slice(1));
|
|
3532
|
+
if (argv[0] === 'setup')
|
|
3533
|
+
return runSetup(argv.slice(1));
|
|
3534
|
+
if (argv[0] === 'model' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
3535
|
+
return startModelSetup();
|
|
3536
|
+
if (argv[0] === 'gateway')
|
|
3537
|
+
return runGateway(argv.slice(1));
|
|
3538
|
+
if (argv[0] === 'status' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
3539
|
+
return runStatus();
|
|
3540
|
+
if (argv[0] === 'auth')
|
|
3541
|
+
return runAuth(argv.slice(1));
|
|
3542
|
+
if (argv[0] === 'sessions' || argv[0] === 'session')
|
|
3543
|
+
return runSessions(argv.slice(1));
|
|
3544
|
+
if (argv[0] === 'insights')
|
|
3545
|
+
return runInsights(argv.slice(1));
|
|
3546
|
+
if (argv[0] === 'dump')
|
|
3547
|
+
return runDump(argv.slice(1));
|
|
3548
|
+
if (argv[0] === 'tools' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
3549
|
+
return runTools(argv.slice(1));
|
|
3550
|
+
if (argv[0] === 'send')
|
|
3551
|
+
return runSend(argv.slice(1));
|
|
3552
|
+
if (argv[0] === 'webhook' || argv[0] === 'webhooks')
|
|
3553
|
+
return runWebhook(argv.slice(1));
|
|
725
3554
|
// subcommands: serve · cron — match เฉพาะรูปแบบที่ถูกต้อง กัน prompt unquoted ("serve coffee") misfire
|
|
726
|
-
if (
|
|
3555
|
+
if (hasServeCommandRequest(argv))
|
|
727
3556
|
return runServe(argv.slice(1));
|
|
728
3557
|
if (argv[0] === 'cron' && ['add', 'list', 'rm', 'remove', undefined].includes(argv[1])) {
|
|
729
3558
|
return runCron(argv.slice(1));
|
|
@@ -733,7 +3562,7 @@ async function main() {
|
|
|
733
3562
|
}
|
|
734
3563
|
if (argv[0] === 'models')
|
|
735
3564
|
return runModels(argv.slice(1));
|
|
736
|
-
if (argv[0] === 'brain' && ['init', undefined].includes(argv[1]))
|
|
3565
|
+
if (argv[0] === 'brain' && ['init', 'doctor', 'context', 'eval', 'review', 'final', undefined].includes(argv[1]))
|
|
737
3566
|
return runBrain(argv.slice(1));
|
|
738
3567
|
if (argv[0] === 'config' && ['get', 'set', 'list', undefined].includes(argv[1]))
|
|
739
3568
|
return runConfig(argv.slice(1));
|
|
@@ -743,17 +3572,18 @@ async function main() {
|
|
|
743
3572
|
return runSearch(argv.slice(1));
|
|
744
3573
|
if (argv[0] === 'mcp' && argv[1] === 'serve')
|
|
745
3574
|
return runMcpServe();
|
|
746
|
-
if (argv[0] === 'mcp' && ['add', 'list', 'remove', 'rm', undefined].includes(argv[1]))
|
|
3575
|
+
if (argv[0] === 'mcp' && ['add', 'list', 'remove', 'rm', 'search', 'info', 'install', 'test', 'doctor', 'preset', undefined].includes(argv[1]))
|
|
747
3576
|
return runMcp(argv.slice(1));
|
|
748
3577
|
if (argv[0] === 'trust' && ['status', 'add', 'remove', 'rm', undefined].includes(argv[1]))
|
|
749
3578
|
return runTrust(argv.slice(1));
|
|
750
|
-
const { model, budget, json, quiet, prompt: argPrompt, planMode, yes } = parseArgs(argv);
|
|
3579
|
+
const { model, budget, json, quiet, prompt: argPrompt, planMode, yes, resume } = parseArgs(argv);
|
|
3580
|
+
const resumeSession = await requestedResumeSession(argv, resume);
|
|
751
3581
|
const budgetUsd = Number.isFinite(budget) ? budget : undefined;
|
|
752
3582
|
// stdin piping: `git diff | sanook "review this"` → ผนวก stdin เข้า prompt (headless/CI)
|
|
753
3583
|
const piped = process.stdin.isTTY ? '' : (await readStdin()).trim();
|
|
754
3584
|
const prompt = piped ? `${argPrompt}\n\n<stdin>\n${piped}\n</stdin>`.trim() : argPrompt;
|
|
755
3585
|
if (prompt) {
|
|
756
|
-
const config = await loadConfig({ model, budgetUsd });
|
|
3586
|
+
const config = await loadConfig({ model: model ?? resumeSession?.model, budgetUsd });
|
|
757
3587
|
// headless + ยังไม่มี key → บอกวิธีเริ่มแบบ actionable แทนปล่อยให้ throw error ดิบ (กัน dead-end ของ flow ที่ README แนะนำ)
|
|
758
3588
|
const noKey = headlessKeyHint(config.model);
|
|
759
3589
|
if (noKey) {
|
|
@@ -761,10 +3591,7 @@ async function main() {
|
|
|
761
3591
|
process.exit(1);
|
|
762
3592
|
}
|
|
763
3593
|
// --continue / -c → โหลด session ล่าสุดมาต่อ (จำว่าทำถึงไหน)
|
|
764
|
-
const
|
|
765
|
-
const history = wantsContinue
|
|
766
|
-
? (await latestSession(argv.includes('--continue-any') ? null : process.cwd()))?.messages
|
|
767
|
-
: undefined;
|
|
3594
|
+
const history = resumeSession?.messages ?? (await requestedContinuationHistory(argv));
|
|
768
3595
|
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, json, history, planMode, yes ? 'auto' : config.permissionMode, quiet, config.fallbackModel);
|
|
769
3596
|
return;
|
|
770
3597
|
}
|
|
@@ -776,7 +3603,8 @@ async function main() {
|
|
|
776
3603
|
const flagProvider = model ? parseSpec(model).provider : undefined;
|
|
777
3604
|
const target = flagProvider ?? detectEnvProvider()?.provider;
|
|
778
3605
|
const tcfg = target ? PROVIDERS[target] : undefined;
|
|
779
|
-
|
|
3606
|
+
const { providerCanSkipSetup } = await import('./first-run.js');
|
|
3607
|
+
if (target && tcfg && (await providerCanSkipSetup(target))) {
|
|
780
3608
|
// มี key ใช้ได้จริง (ผ่าน policy ไม่ใช่ OAuth) → ข้าม wizard, ตั้ง default, บอกว่าพร้อมใช้
|
|
781
3609
|
const { saveGlobalConfig } = await import('./config.js');
|
|
782
3610
|
await saveGlobalConfig({ model: model ?? `${target}:${tcfg.models.default}`, provider: target });
|
|
@@ -786,11 +3614,13 @@ async function main() {
|
|
|
786
3614
|
needsSetup = true; // ไม่มี provider ที่ key ใช้ได้ (หรือ -m provider ไม่มี key) → wizard (รัน Ink เดียวกับ REPL)
|
|
787
3615
|
}
|
|
788
3616
|
}
|
|
789
|
-
const config = await loadConfig({ model, budgetUsd });
|
|
3617
|
+
const config = await loadConfig({ model: model ?? resumeSession?.model, budgetUsd });
|
|
3618
|
+
if (!needsSetup) {
|
|
3619
|
+
const { modelNeedsSetup } = await import('./first-run.js');
|
|
3620
|
+
needsSetup = await modelNeedsSetup(config.model);
|
|
3621
|
+
}
|
|
790
3622
|
// --continue / -c → โหลด conversation ล่าสุดเข้า REPL (เดิม resume ได้แค่ headless)
|
|
791
|
-
const initialHistory =
|
|
792
|
-
? (await latestSession(argv.includes('--continue-any') ? null : process.cwd()))?.messages
|
|
793
|
-
: undefined;
|
|
3623
|
+
const initialHistory = resumeSession?.messages ?? (await requestedContinuationHistory(argv));
|
|
794
3624
|
const { startApp } = await import('./ui/render.js');
|
|
795
3625
|
startApp({
|
|
796
3626
|
needsSetup,
|