sanook-cli 0.5.1 → 0.5.5
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 +148 -10
- package/README.md +255 -26
- package/README.th.md +95 -7
- package/dist/approval.js +13 -0
- package/dist/bin.js +3552 -155
- package/dist/brain-consolidate.js +335 -0
- package/dist/brain-context.js +262 -0
- package/dist/brain-doctor.js +318 -0
- package/dist/brain-eval.js +186 -0
- package/dist/brain-final.js +377 -0
- package/dist/brain-metrics.js +277 -0
- package/dist/brain-new.js +402 -0
- package/dist/brain-pack.js +210 -0
- package/dist/brain-repair.js +280 -0
- package/dist/brain-review.js +382 -0
- package/dist/brain.js +15 -1
- package/dist/brand.js +1 -1
- package/dist/cli-args.js +190 -0
- package/dist/cli-option-values.js +16 -0
- package/dist/clipboard.js +65 -0
- package/dist/commands.js +266 -27
- package/dist/compaction.js +96 -11
- package/dist/config.js +149 -33
- package/dist/context-compression.js +191 -0
- package/dist/context-pack.js +145 -0
- package/dist/cost.js +49 -15
- package/dist/dashboard/api-helpers.js +87 -0
- package/dist/dashboard/server.js +179 -0
- package/dist/dashboard/static/app.js +277 -0
- package/dist/dashboard/static/index.html +39 -0
- package/dist/dashboard/static/styles.css +85 -0
- package/dist/diff.js +10 -2
- package/dist/first-run.js +21 -0
- package/dist/gateway/auth.js +49 -9
- package/dist/gateway/bluebubbles.js +205 -0
- package/dist/gateway/config.js +929 -0
- package/dist/gateway/deliver.js +399 -0
- package/dist/gateway/discord.js +124 -0
- package/dist/gateway/doctor.js +456 -0
- package/dist/gateway/email.js +501 -0
- package/dist/gateway/googlechat.js +207 -0
- package/dist/gateway/homeassistant.js +256 -0
- package/dist/gateway/ledger.js +38 -1
- 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 +362 -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/hotkeys.js +21 -0
- package/dist/i18n/en.js +98 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/th.js +98 -0
- package/dist/i18n/types.js +1 -0
- package/dist/insights-args.js +55 -0
- package/dist/insights.js +86 -0
- package/dist/knowledge.js +55 -29
- package/dist/loop.js +157 -29
- package/dist/lsp/index.js +23 -5
- package/dist/mcp-hub.js +33 -0
- package/dist/mcp-registry.js +494 -0
- package/dist/mcp-risk.js +71 -0
- package/dist/mcp-server.js +1 -1
- package/dist/mcp.js +120 -10
- package/dist/memory-log.js +90 -0
- package/dist/memory-store.js +37 -1
- package/dist/memory.js +148 -37
- package/dist/model-picker.js +58 -0
- package/dist/orchestrate.js +51 -19
- package/dist/personality.js +58 -0
- package/dist/plan-handoff.js +17 -0
- package/dist/polyglot.js +162 -0
- package/dist/process-runner.js +96 -0
- package/dist/project-init.js +91 -0
- package/dist/project-registry.js +143 -0
- package/dist/project-scaffold.js +124 -0
- package/dist/prompt-size.js +155 -0
- package/dist/providers/codex-login.js +138 -0
- package/dist/providers/codex.js +89 -43
- package/dist/providers/keys.js +22 -1
- package/dist/providers/models.js +2 -2
- package/dist/providers/registry.js +14 -47
- package/dist/search/chunk.js +7 -8
- package/dist/search/cli.js +83 -0
- package/dist/search/embed-store.js +3 -0
- package/dist/search/embedding-config.js +22 -0
- package/dist/search/engine.js +2 -13
- package/dist/search/indexer.js +44 -1
- package/dist/search/store.js +23 -1
- package/dist/session-distill.js +84 -0
- package/dist/session.js +92 -16
- package/dist/skill-install.js +53 -13
- package/dist/skills.js +33 -0
- package/dist/slash-completion.js +155 -0
- package/dist/support-dump.js +206 -0
- package/dist/tool-catalog.js +59 -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 +10 -0
- package/dist/tools/list.js +19 -6
- package/dist/tools/permission.js +992 -12
- package/dist/tools/polyglot.js +126 -0
- package/dist/tools/read.js +16 -4
- package/dist/tools/sandbox.js +38 -13
- package/dist/tools/schedule.js +19 -3
- package/dist/tools/search.js +226 -15
- package/dist/tools/task.js +40 -9
- package/dist/tools/timeout.js +23 -3
- package/dist/tools/web-fetch-tool.js +33 -0
- package/dist/trust.js +11 -1
- package/dist/turn-retrieval.js +83 -0
- package/dist/ui/app.js +878 -32
- package/dist/ui/banner.js +78 -4
- package/dist/ui/history.js +37 -5
- package/dist/ui/markdown.js +122 -0
- package/dist/ui/mentions.js +3 -2
- package/dist/ui/overlay.js +496 -0
- package/dist/ui/queue.js +23 -0
- package/dist/ui/render.js +20 -1
- package/dist/ui/session-panel.js +115 -0
- package/dist/ui/setup-providers.js +40 -0
- package/dist/ui/setup.js +172 -46
- package/dist/ui/status.js +142 -0
- package/dist/ui/thinking-panel.js +36 -0
- package/dist/ui/tool-trail.js +97 -0
- package/dist/ui/transcript.js +26 -0
- package/dist/ui/useBusyElapsed.js +19 -0
- package/dist/ui/useEditor.js +144 -5
- package/dist/ui/useGitBranch.js +57 -0
- package/dist/update.js +56 -17
- package/dist/web-fetch.js +637 -0
- package/dist/web-surface.js +190 -0
- package/dist/worktree.js +175 -4
- package/package.json +5 -5
- 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 +19 -4
- package/second-brain/Projects/sanook-cli/_Index.md +30 -0
- package/second-brain/Projects/sanook-cli/context.md +35 -0
- package/second-brain/Projects/sanook-cli/current-state.md +32 -0
- package/second-brain/Projects/sanook-cli/overview.md +41 -0
- package/second-brain/Projects/sanook-cli/repo.md +34 -0
- package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +197 -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-hermes-tui-parity-map.md +129 -0
- package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
- package/second-brain/Research/2026-06-19-hermes-python-architecture-for-sanook.md +49 -0
- package/second-brain/Research/2026-06-19-terminal-ui-brand-research.md +52 -0
- package/second-brain/Research/_Index.md +8 -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 +14 -4
- 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 +6 -1
- package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
- package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
- package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -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/Templates/project-workspace/_Index.md +31 -0
- package/second-brain/Templates/project-workspace/context.md +28 -0
- package/second-brain/Templates/project-workspace/current-state.md +29 -0
- package/second-brain/Templates/project-workspace/overview.md +39 -0
- package/second-brain/Templates/project-workspace/repo.md +33 -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,61 +1,24 @@
|
|
|
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
|
|
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
|
-
async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, planMode = false, permissionMode = 'ask', quiet = false, fallbackModel) {
|
|
21
|
+
async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, planMode = false, permissionMode = 'ask', quiet = false, fallbackModel, planHandoffTask) {
|
|
59
22
|
const controller = new AbortController();
|
|
60
23
|
process.on('SIGINT', () => {
|
|
61
24
|
controller.abort();
|
|
@@ -96,6 +59,10 @@ async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, pl
|
|
|
96
59
|
process.stdout.write(`\n${DIM}${cost.summary()}${RESET}\n`);
|
|
97
60
|
else if (quiet)
|
|
98
61
|
process.stdout.write('\n');
|
|
62
|
+
if (planMode && planHandoffTask && !json && !quiet) {
|
|
63
|
+
const { formatPlanExecuteHandoff } = await import('./plan-handoff.js');
|
|
64
|
+
process.stderr.write(`\n${DIM}${formatPlanExecuteHandoff(planHandoffTask)}${RESET}\n`);
|
|
65
|
+
}
|
|
99
66
|
// จำ session ไว้ทำงานต่อได้ (sanook --continue "...") — แก้ concern AI ลืมว่าทำถึงไหน
|
|
100
67
|
const now = new Date().toISOString();
|
|
101
68
|
await saveSession({ id: newSessionId(), created: now, updated: now, model, cwd: process.cwd(), messages });
|
|
@@ -105,6 +72,16 @@ async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, pl
|
|
|
105
72
|
if (brain) {
|
|
106
73
|
await appendBrainWorklog(brain, { prompt, summary: cost.summary(), model, today: now.slice(0, 10) }).catch(() => { });
|
|
107
74
|
}
|
|
75
|
+
// opt-in (experimental, default OFF): auto-distill durable decisions/gotchas/preferences from this
|
|
76
|
+
// session into the compounding memory store so the self-retrieving brain surfaces them next time.
|
|
77
|
+
// Off by default per experiment H5 — extraction is high-precision (~0.88) but end-to-end recall is
|
|
78
|
+
// gated by retrieval quality (semantic helps). Enable with SANOOK_AUTO_DISTILL=1.
|
|
79
|
+
if (envFlag('SANOOK_AUTO_DISTILL')) {
|
|
80
|
+
const { distilledFactsFromMessages } = await import('./session-distill.js');
|
|
81
|
+
const { appendMemory } = await import('./memory.js');
|
|
82
|
+
for (const fact of distilledFactsFromMessages(messages))
|
|
83
|
+
await appendMemory(fact).catch(() => { });
|
|
84
|
+
}
|
|
108
85
|
}
|
|
109
86
|
catch (err) {
|
|
110
87
|
const msg = redactKey(err.message);
|
|
@@ -115,6 +92,28 @@ async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, pl
|
|
|
115
92
|
process.exit(1);
|
|
116
93
|
}
|
|
117
94
|
}
|
|
95
|
+
/** sanook plan "<task>" — read-only plan mode; stderr prints execute handoff after success */
|
|
96
|
+
async function runPlan(args) {
|
|
97
|
+
const parsed = parseArgs(args);
|
|
98
|
+
if (parsed.budgetInvalid)
|
|
99
|
+
process.stderr.write(`${BRAND.cliName}: ⚠ --budget ไม่ถูกต้อง (ต้องเป็นจำนวนบวก) — รันต่อโดยไม่มี spend cap\n`);
|
|
100
|
+
const resumeSession = await requestedResumeSession(args, parsed.resume);
|
|
101
|
+
const budgetUsd = Number.isFinite(parsed.budget) ? parsed.budget : undefined;
|
|
102
|
+
const piped = process.stdin.isTTY ? '' : (await readStdin()).trim();
|
|
103
|
+
const prompt = piped ? `${parsed.prompt}\n\n<stdin>\n${piped}\n</stdin>`.trim() : parsed.prompt;
|
|
104
|
+
if (!prompt) {
|
|
105
|
+
console.error(`ใช้: ${BRAND.cliName} plan "<task>" [--json] [-m model]`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
const config = await loadConfig({ model: parsed.model ?? resumeSession?.model, budgetUsd });
|
|
109
|
+
const noKey = headlessKeyHint(config.model);
|
|
110
|
+
if (noKey) {
|
|
111
|
+
process.stderr.write(`${noKey}\n`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
const history = resumeSession?.messages ?? (await requestedContinuationHistory(args));
|
|
115
|
+
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, parsed.json, history, true, 'ask', parsed.quiet, config.fallbackModel, prompt);
|
|
116
|
+
}
|
|
118
117
|
// อ่านจาก package.json (single source of truth) — กัน version constant drift
|
|
119
118
|
const PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
120
119
|
const VERSION = PACKAGE.version;
|
|
@@ -123,44 +122,113 @@ const HELP = `${BRAND.productName} — a terminal AI coding agent (BYOK)
|
|
|
123
122
|
|
|
124
123
|
usage:
|
|
125
124
|
${BRAND.cliName} "<task>" run one task (headless)
|
|
125
|
+
${BRAND.cliName} plan "<task>" plan-only (read-only) + execute handoff hint on stderr
|
|
126
|
+
${BRAND.cliName} -z "<task>" one-shot final output (script-friendly)
|
|
127
|
+
${BRAND.cliName} chat -q "<query>" direct one-shot query
|
|
126
128
|
${BRAND.cliName} interactive REPL
|
|
129
|
+
${BRAND.cliName} setup [section] setup wizard (model | gateway | tools | agent | brain)
|
|
130
|
+
${BRAND.cliName} dashboard [--port] Sanook Dashboard (local web admin UI)
|
|
131
|
+
${BRAND.cliName} model choose provider + model
|
|
127
132
|
${BRAND.cliName} --json "<task>" headless, JSONL output (for CI/scripts)
|
|
133
|
+
${BRAND.cliName} sessions list/resume-audit saved conversation sessions
|
|
134
|
+
${BRAND.cliName} insights local usage/session insights
|
|
135
|
+
${BRAND.cliName} dump [--show-keys] support snapshot (secrets redacted)
|
|
136
|
+
${BRAND.cliName} prompt-size [--json] inspect prompt/context budget without calling a model
|
|
137
|
+
${BRAND.cliName} runtimes [--json] inspect optional Python/Rust runtime surface
|
|
128
138
|
${BRAND.cliName} update update ${BRAND.cliName} to the latest npm release
|
|
129
139
|
${BRAND.cliName} doctor ตรวจการติดตั้ง + วิธีแก้ PATH (เมื่อพิมพ์ "${BRAND.cliName}" แล้วไม่เจอ)
|
|
130
140
|
|
|
131
141
|
gateway (อยู่ยาว 24/7 — HTTP loopback + cron):
|
|
132
|
-
${BRAND.cliName}
|
|
133
|
-
${BRAND.cliName}
|
|
142
|
+
${BRAND.cliName} gateway setup telegram ตั้งค่า Telegram token + allowlist
|
|
143
|
+
${BRAND.cliName} gateway setup discord ตั้งค่า Discord bot token + channel allowlist
|
|
144
|
+
${BRAND.cliName} gateway setup slack ตั้งค่า Slack bot/app token + channel allowlist
|
|
145
|
+
${BRAND.cliName} gateway setup mattermost ตั้งค่า Mattermost token + user/channel allowlist
|
|
146
|
+
${BRAND.cliName} gateway setup homeassistant ตั้งค่า Home Assistant token + state-change filters
|
|
147
|
+
${BRAND.cliName} gateway setup email ตั้งค่า Email IMAP/SMTP + allowed senders
|
|
148
|
+
${BRAND.cliName} gateway setup line ตั้งค่า LINE Messaging API push target
|
|
149
|
+
${BRAND.cliName} gateway setup sms ตั้งค่า Twilio SMS webhook + allowlist
|
|
150
|
+
${BRAND.cliName} gateway setup ntfy ตั้งค่า ntfy topic push + subscribe
|
|
151
|
+
${BRAND.cliName} gateway setup signal ตั้งค่า Signal ผ่าน signal-cli HTTP daemon
|
|
152
|
+
${BRAND.cliName} gateway setup whatsapp ตั้งค่า WhatsApp Cloud API webhook + send
|
|
153
|
+
${BRAND.cliName} gateway setup matrix ตั้งค่า Matrix homeserver sync + send
|
|
154
|
+
${BRAND.cliName} gateway setup googlechat ตั้งค่า Google Chat bot send
|
|
155
|
+
${BRAND.cliName} gateway setup bluebubbles ตั้งค่า BlueBubbles/iMessage send
|
|
156
|
+
${BRAND.cliName} gateway setup teams ตั้งค่า Microsoft Teams delivery
|
|
157
|
+
${BRAND.cliName} gateway setup webhooks เปิด generic webhook routes + HMAC
|
|
158
|
+
${BRAND.cliName} gateway run [--port 8787] [--model spec] เปิด gateway (เหมือน serve)
|
|
159
|
+
${BRAND.cliName} gateway start [--port 8787] เปิด gateway เป็น background process
|
|
160
|
+
${BRAND.cliName} gateway stop|restart|install จัดการ gateway service
|
|
161
|
+
${BRAND.cliName} gateway status ดู config/status gateway
|
|
162
|
+
${BRAND.cliName} gateway doctor ตรวจ token/webhook/allowlist ของ channels ที่ตั้งไว้
|
|
163
|
+
${BRAND.cliName} send --to telegram|discord|slack|mattermost|homeassistant|email|line|sms|ntfy|signal|whatsapp|matrix|googlechat|bluebubbles|teams[:target] "msg" ส่งข้อความออก platform โดยไม่เรียก LLM
|
|
164
|
+
${BRAND.cliName} webhook subscribe <route> [--prompt "..."] [--to telegram|slack|mattermost|homeassistant|sms|ntfy|signal|whatsapp|matrix|googlechat|bluebubbles|teams]
|
|
165
|
+
รับ event จาก GitHub/GitLab/Jira/Stripe แล้ว trigger agent/delivery
|
|
166
|
+
${BRAND.cliName} send --list [platform] ดู messaging targets ที่ตั้งค่าไว้
|
|
167
|
+
${BRAND.cliName} serve [--port 8787] [--model spec] เปิด gateway (OpenAI-compat /v1/chat/completions + scheduler)
|
|
168
|
+
${BRAND.cliName} cron add "<when>" "<task>" [--to <target>] [--model <model>]
|
|
169
|
+
ตั้งงานล่วงหน้า + ส่งผลลัพธ์กลับ messaging target ได้
|
|
134
170
|
${BRAND.cliName} cron list ดู task ทั้งหมด
|
|
135
171
|
${BRAND.cliName} cron rm <id> ลบ task
|
|
136
172
|
|
|
137
173
|
skills (built-in + ติดตั้งเพิ่มได้):
|
|
138
174
|
${BRAND.cliName} skill list ดู skill ทั้งหมด
|
|
175
|
+
${BRAND.cliName} skill install <name|path> ติดตั้ง skill จาก bundled catalog / local path
|
|
139
176
|
${BRAND.cliName} skill add <user/repo|url|path> ติดตั้ง skill จาก GitHub / URL / local
|
|
140
177
|
${BRAND.cliName} skill remove <name> ลบ skill ที่ติดตั้ง
|
|
141
178
|
${BRAND.cliName} models [provider] ดู/verify model id (เทียบ provider จริงถ้ามี key)
|
|
142
179
|
|
|
180
|
+
project setup:
|
|
181
|
+
${BRAND.cliName} init [--trust] scaffold .sanook/commands + onboarding hints
|
|
182
|
+
|
|
143
183
|
second brain (Obsidian workspace สำหรับจัดเก็บงาน + ความจำ AI):
|
|
144
184
|
${BRAND.cliName} brain init [path] สร้างโครงสร้าง second-brain ที่ path (ไม่ใส่ = ถาม)
|
|
185
|
+
${BRAND.cliName} brain doctor ตรวจ health ของ second-brain ที่ config.brainPath
|
|
186
|
+
${BRAND.cliName} brain context [--task "..."] [--project <slug>] แสดง context ที่ Sanook จะ inject (+ project auto-detect จาก cwd)
|
|
187
|
+
${BRAND.cliName} brain projects list แสดง Projects/<slug>/ + repo_path mapping
|
|
188
|
+
${BRAND.cliName} brain eval รัน second-brain benchmark sanity checks
|
|
189
|
+
${BRAND.cliName} brain review curator review: inbox, packs, sessions, evals, note hygiene
|
|
190
|
+
${BRAND.cliName} brain pack list|show <name> ดู context packs ใน Shared/Context-Packs/
|
|
191
|
+
${BRAND.cliName} brain new project [--title "..."] [--repo /path] [--verify "..."] scaffold Projects/<slug>/ workspace
|
|
192
|
+
${BRAND.cliName} brain repair [--dry-run] แก้ one-line fixes หลัง doctor/review
|
|
193
|
+
${BRAND.cliName} brain consolidate [--apply] sleep-time consolidation (inbox, stale, retrieval; dry-run default)
|
|
194
|
+
${BRAND.cliName} brain metrics [--no-retrieval] vault counts, stale notes, index freshness, retrieval coverage
|
|
195
|
+
${BRAND.cliName} brain final --task "..." สร้าง final gate note ใน Sessions พร้อม evidence scaffold
|
|
145
196
|
|
|
146
197
|
search (BM25 + optional BYOK semantic เหนือ vault + memory + sessions + skills):
|
|
147
198
|
${BRAND.cliName} index (re)index vault+memory แบบ incremental (O(delta))
|
|
148
199
|
${BRAND.cliName} search "<query>" [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,memory]
|
|
200
|
+
${BRAND.cliName} web status [--json] [--probe] ตรวจ true web/search readiness ผ่าน MCP (local search ไม่ใช่ internet)
|
|
201
|
+
${BRAND.cliName} web doctor [--json] probe web/search/fetch MCP candidates
|
|
202
|
+
${BRAND.cliName} web fetch <url> [--json] ดึงหน้าเว็บสาธารณะ + สรุปโครงสร้าง (fallback ladder ที่ถูกกติกา)
|
|
203
|
+
${BRAND.cliName} web search "<q>" [--limit N] ค้นเว็บผ่าน Tavily (ต้องมี TAVILY_API_KEY)
|
|
204
|
+
${BRAND.cliName} web setup tavily [--api-key K] ตั้งค่า Tavily (เขียน MCP + เก็บ key 0600)
|
|
149
205
|
${BRAND.cliName} mcp serve expose brain เป็น MCP server (stdio) ให้ Claude Desktop/Cursor
|
|
150
206
|
|
|
151
207
|
config & mcp:
|
|
152
|
-
${BRAND.cliName}
|
|
153
|
-
${BRAND.cliName}
|
|
208
|
+
${BRAND.cliName} status ดู provider/key/brain/gateway status แบบ redacted
|
|
209
|
+
${BRAND.cliName} auth [list|status|add|remove] จัดการ API keys ของ providers (BYOK, redacted)
|
|
210
|
+
${BRAND.cliName} sessions [list|latest|show|rm] จัดการ saved sessions
|
|
211
|
+
${BRAND.cliName} insights [--days N] [--all] ดู usage/session insights ในเครื่อง
|
|
212
|
+
${BRAND.cliName} memory [stats|log "<q>"] ดู memory: สถิติ + วิวัฒนาการของ belief (เคยเชื่ออะไร ถูก supersede ตอนไหน)
|
|
213
|
+
${BRAND.cliName} dump [--show-keys] diagnostic/support dump แบบไม่โชว์ raw secret
|
|
214
|
+
${BRAND.cliName} prompt-size [--json] ดู system prompt / skills / brain / tools token budget แบบ offline
|
|
215
|
+
${BRAND.cliName} runtimes [--json] ดู Python/Rust optional runtime + บทบาทใน Sanook
|
|
216
|
+
${BRAND.cliName} web status [--json] ดู web-search/fetch readiness และ grounding policy
|
|
217
|
+
${BRAND.cliName} tools ดู tool surface ที่ agent ใช้ได้
|
|
218
|
+
${BRAND.cliName} config [get|set <k> <v>] ดู/แก้ ${appHomePath('config.json')} (model/budgetUsd/permissionMode/cacheTtl/compaction/contextCompression/thinking/embeddingModel)
|
|
219
|
+
${BRAND.cliName} mcp [search|info|install|test|doctor|enable|disable|preset|list|add|remove] จัดการ MCP servers
|
|
154
220
|
${BRAND.cliName} trust [status|add|remove] อนุญาต/ยกเลิก project .sanook mcp/hooks/skills/commands
|
|
155
221
|
|
|
156
222
|
flags:
|
|
157
|
-
-m, --model <spec> sonnet/opus/haiku/fable · gpt/codex · gemini · grok ·
|
|
158
|
-
or "provider:model-id" (e.g. openai:gpt-5-codex, groq:fast, google:gemini-2.5-flash)
|
|
223
|
+
-m, --model <spec> sonnet/opus/haiku/fable · gpt/codex · gemini · grok · mistral · groq · ollama/lmstudio
|
|
224
|
+
or "provider:model-id" (e.g. openai:gpt-5.3-codex, groq:fast, google:gemini-2.5-flash)
|
|
159
225
|
-b, --budget <usd> stop when estimated cost exceeds this
|
|
160
226
|
-c, --continue resume the latest session ของ project นี้
|
|
227
|
+
-r, --resume <id> resume a specific saved session
|
|
161
228
|
--continue-any resume latest session ข้าม project (explicit)
|
|
162
229
|
--plan plan mode — สำรวจ+วางแผนเท่านั้น ไม่แก้ไฟล์ (read-only)
|
|
163
230
|
-y, --yes อนุมัติ tool อัตโนมัติ (ข้าม ask-mode permission)
|
|
231
|
+
--yolo alias ของ --yes (compat)
|
|
164
232
|
--json machine-readable JSONL output
|
|
165
233
|
-v, --version
|
|
166
234
|
-h, --help
|
|
@@ -170,41 +238,2719 @@ env (BYOK — direct API key only):
|
|
|
170
238
|
${BRAND_ENV.disableUpdateCheck}=1 disable interactive update prompts`;
|
|
171
239
|
/** sanook serve [--port N] [--model spec] — เปิด gateway (HTTP loopback + cron scheduler) อยู่ยาว */
|
|
172
240
|
async function runServe(args) {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
241
|
+
const parsed = parseServeArgs(args);
|
|
242
|
+
if (parsed.portError) {
|
|
243
|
+
console.error(`port ไม่ถูกต้อง: ${parsed.portError}`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
if (parsed.modelError) {
|
|
247
|
+
console.error(`model ไม่ถูกต้อง: ${parsed.modelError}`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
const config = await loadConfig({ model: parsed.model });
|
|
251
|
+
const { startGateway } = await import('./gateway/serve.js');
|
|
252
|
+
process.stdout.write(`${DIM}${BRAND.productName} gateway — model: ${config.model}${RESET}\n`);
|
|
253
|
+
const stop = await startGateway({
|
|
254
|
+
port: parsed.port,
|
|
255
|
+
model: config.model,
|
|
256
|
+
budgetUsd: config.budgetUsd,
|
|
257
|
+
permissionMode: envFlag(BRAND_ENV.gatewayAllowWrite) ? 'auto' : config.permissionMode,
|
|
258
|
+
onLog: (m) => process.stdout.write(`${DIM}[gateway] ${m}${RESET}\n`),
|
|
259
|
+
});
|
|
260
|
+
const shutdown = () => {
|
|
261
|
+
stop();
|
|
262
|
+
process.stdout.write('\n[gateway] หยุดแล้ว\n');
|
|
263
|
+
process.exit(0);
|
|
264
|
+
};
|
|
265
|
+
process.on('SIGINT', shutdown);
|
|
266
|
+
process.on('SIGTERM', shutdown);
|
|
267
|
+
// server + scheduler interval ถือ event loop ไว้ → process อยู่ยาวจนกด Ctrl-C
|
|
268
|
+
}
|
|
269
|
+
async function startModelSetup() {
|
|
270
|
+
const config = await loadConfig({});
|
|
271
|
+
const { startApp } = await import('./ui/render.js');
|
|
272
|
+
startApp({
|
|
273
|
+
needsSetup: true,
|
|
274
|
+
appProps: {
|
|
275
|
+
initialModel: config.model,
|
|
276
|
+
fallbackModel: config.fallbackModel,
|
|
277
|
+
budgetUsd: config.budgetUsd,
|
|
278
|
+
permissionMode: config.permissionMode,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
async function runDashboard(args = []) {
|
|
283
|
+
let port = 9119;
|
|
284
|
+
for (let i = 0; i < args.length; i++) {
|
|
285
|
+
if (args[i] === '--port' && args[i + 1]) {
|
|
286
|
+
port = Number(args[++i]);
|
|
287
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
288
|
+
console.error(`${BRAND.cliName}: --port ต้องเป็นจำนวนบวก`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else if (args[i] === '-h' || args[i] === '--help') {
|
|
293
|
+
console.log(`ใช้: ${BRAND.cliName} dashboard [--port 9119]`);
|
|
294
|
+
console.log('เปิด Sanook Dashboard (local web UI — Hermes-style admin panel)');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const { startDashboardServer } = await import('./dashboard/server.js');
|
|
299
|
+
const stop = await startDashboardServer({
|
|
300
|
+
port,
|
|
301
|
+
onLog: (m) => console.log(m),
|
|
302
|
+
});
|
|
303
|
+
const shutdown = () => {
|
|
304
|
+
stop();
|
|
305
|
+
process.stdout.write('\n[dashboard] stopped\n');
|
|
306
|
+
process.exit(0);
|
|
307
|
+
};
|
|
308
|
+
process.on('SIGINT', shutdown);
|
|
309
|
+
process.on('SIGTERM', shutdown);
|
|
310
|
+
}
|
|
311
|
+
async function runTools(_args = []) {
|
|
312
|
+
const { tools } = await import('./tools/index.js');
|
|
313
|
+
const names = Object.keys(tools).sort();
|
|
314
|
+
console.log(`${BRAND.productName} tools (${names.length})`);
|
|
315
|
+
console.log(names.map((n) => ` ${n}`).join('\n'));
|
|
316
|
+
console.log(`\nจัดการ MCP เพิ่มเติม: ${BRAND.cliName} mcp add <name> <command> [args...]`);
|
|
317
|
+
}
|
|
318
|
+
async function runPromptSize(args = []) {
|
|
319
|
+
const allowed = new Set(['--json']);
|
|
320
|
+
const unknown = args.find((arg) => !allowed.has(arg));
|
|
321
|
+
if (unknown) {
|
|
322
|
+
console.error(`ไม่รู้จัก option: ${unknown}`);
|
|
323
|
+
console.error(`ใช้: ${BRAND.cliName} prompt-size [--json]`);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
}
|
|
326
|
+
const { buildPromptSizeBreakdown, renderPromptSizeBreakdown } = await import('./prompt-size.js');
|
|
327
|
+
const report = await buildPromptSizeBreakdown();
|
|
328
|
+
if (args.includes('--json'))
|
|
329
|
+
console.log(JSON.stringify(report, null, 2));
|
|
330
|
+
else
|
|
331
|
+
process.stdout.write(renderPromptSizeBreakdown(report));
|
|
332
|
+
}
|
|
333
|
+
async function runRuntimes(args = []) {
|
|
334
|
+
const allowed = new Set(['--json']);
|
|
335
|
+
const unknown = args.find((arg) => !allowed.has(arg));
|
|
336
|
+
if (unknown) {
|
|
337
|
+
console.error(`ไม่รู้จัก option: ${unknown}`);
|
|
338
|
+
console.error(`ใช้: ${BRAND.cliName} runtimes [--json]`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
const { inspectPolyglotRuntimes, renderPolyglotReport } = await import('./polyglot.js');
|
|
342
|
+
const report = await inspectPolyglotRuntimes();
|
|
343
|
+
if (args.includes('--json'))
|
|
344
|
+
console.log(JSON.stringify(report, null, 2));
|
|
345
|
+
else
|
|
346
|
+
process.stdout.write(renderPolyglotReport(report));
|
|
347
|
+
}
|
|
348
|
+
async function runWeb(args = []) {
|
|
349
|
+
const action = args[0] && !args[0].startsWith('--') ? args[0] : 'status';
|
|
350
|
+
const rest = action === args[0] ? args.slice(1) : args;
|
|
351
|
+
if (action === 'fetch')
|
|
352
|
+
return runWebFetch(rest);
|
|
353
|
+
if (action === 'search')
|
|
354
|
+
return runWebSearch(rest);
|
|
355
|
+
if (action === 'setup')
|
|
356
|
+
return runWebSetup(rest);
|
|
357
|
+
const flags = rest;
|
|
358
|
+
const allowed = new Set(['--json', '--probe']);
|
|
359
|
+
const unknown = flags.find((arg) => !allowed.has(arg));
|
|
360
|
+
if (!['status', 'doctor'].includes(action) || unknown) {
|
|
361
|
+
if (unknown)
|
|
362
|
+
console.error(`ไม่รู้จัก option: ${unknown}`);
|
|
363
|
+
console.error(`ใช้: ${BRAND.cliName} web status [--json] [--probe]`);
|
|
364
|
+
console.error(` ${BRAND.cliName} web doctor [--json]`);
|
|
365
|
+
console.error(` ${BRAND.cliName} web fetch <url> [--json] [--no-reader] [--no-archive] [--no-robots] [--allow-private]`);
|
|
366
|
+
console.error(` ${BRAND.cliName} web search "<query>" [--json] [--limit N]`);
|
|
367
|
+
console.error(` ${BRAND.cliName} web setup tavily [--api-key <key>]`);
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
const { inspectWebSurface, renderWebSurfaceReport } = await import('./web-surface.js');
|
|
371
|
+
const report = await inspectWebSurface({ cwd: process.cwd(), probe: action === 'doctor' || flags.includes('--probe') });
|
|
372
|
+
if (flags.includes('--json'))
|
|
373
|
+
console.log(JSON.stringify(report, null, 2));
|
|
374
|
+
else
|
|
375
|
+
process.stdout.write(renderWebSurfaceReport(report));
|
|
376
|
+
if (action === 'doctor' && report.webCandidates.some((candidate) => candidate.probe && !candidate.probe.ok))
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
/** TAVILY_API_KEY from env first, then any MCP server env in ~/.sanook/mcp.json. */
|
|
380
|
+
async function resolveTavilyKey() {
|
|
381
|
+
const fromEnv = process.env.TAVILY_API_KEY?.trim();
|
|
382
|
+
if (fromEnv)
|
|
383
|
+
return fromEnv;
|
|
384
|
+
try {
|
|
385
|
+
const { loadMcpConfig } = await import('./mcp.js');
|
|
386
|
+
const cfg = await loadMcpConfig();
|
|
387
|
+
for (const server of Object.values(cfg)) {
|
|
388
|
+
const key = server.env?.TAVILY_API_KEY?.trim();
|
|
389
|
+
if (key)
|
|
390
|
+
return key;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
/* no mcp config */
|
|
395
|
+
}
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
async function runWebFetch(args) {
|
|
399
|
+
const url = args.find((a) => !a.startsWith('--'));
|
|
400
|
+
if (!url) {
|
|
401
|
+
console.error(`ใช้: ${BRAND.cliName} web fetch <url> [--json] [--no-reader] [--no-archive] [--no-robots] [--allow-private]`);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
const { fetchWeb, renderWebFetchResult } = await import('./web-fetch.js');
|
|
405
|
+
const result = await fetchWeb(url, {
|
|
406
|
+
allowReader: !args.includes('--no-reader'),
|
|
407
|
+
allowArchive: !args.includes('--no-archive'),
|
|
408
|
+
respectRobots: !args.includes('--no-robots'),
|
|
409
|
+
allowPrivateHosts: args.includes('--allow-private'),
|
|
410
|
+
tavilyApiKey: await resolveTavilyKey(),
|
|
411
|
+
});
|
|
412
|
+
if (args.includes('--json'))
|
|
413
|
+
console.log(JSON.stringify(result, null, 2));
|
|
414
|
+
else
|
|
415
|
+
process.stdout.write(`${renderWebFetchResult(result)}\n`);
|
|
416
|
+
if (!result.ok)
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
async function runWebSearch(args) {
|
|
420
|
+
let limit = 5;
|
|
421
|
+
const terms = [];
|
|
422
|
+
for (let i = 0; i < args.length; i++) {
|
|
423
|
+
const a = args[i];
|
|
424
|
+
if (a === '--json')
|
|
425
|
+
continue;
|
|
426
|
+
if (a === '--limit') {
|
|
427
|
+
limit = Number(args[++i]) || limit;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (a.startsWith('--limit=')) {
|
|
431
|
+
limit = Number(a.slice('--limit='.length)) || limit;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (a.startsWith('--'))
|
|
435
|
+
continue;
|
|
436
|
+
terms.push(a);
|
|
437
|
+
}
|
|
438
|
+
const query = terms.join(' ').trim();
|
|
439
|
+
if (!query) {
|
|
440
|
+
console.error(`ใช้: ${BRAND.cliName} web search "<query>" [--json] [--limit N]`);
|
|
441
|
+
process.exit(1);
|
|
442
|
+
}
|
|
443
|
+
const apiKey = await resolveTavilyKey();
|
|
444
|
+
if (!apiKey) {
|
|
445
|
+
console.error(`web search ต้องมี Tavily API key — ตั้งค่า: ${BRAND.cliName} web setup tavily`);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
const { tavilySearch } = await import('./web-fetch.js');
|
|
449
|
+
try {
|
|
450
|
+
const hits = await tavilySearch(query, { apiKey, maxResults: Math.min(Math.max(limit, 1), 20) });
|
|
451
|
+
if (args.includes('--json')) {
|
|
452
|
+
console.log(JSON.stringify(hits, null, 2));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (!hits.length) {
|
|
456
|
+
console.log('(no results)');
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
for (const h of hits)
|
|
460
|
+
console.log(`- ${h.title}\n ${h.url}\n ${h.content.slice(0, 200)}`);
|
|
461
|
+
}
|
|
462
|
+
catch (e) {
|
|
463
|
+
console.error(`web search ล้มเหลว: ${e.message}`);
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async function runWebSetup(args) {
|
|
468
|
+
const provider = args.find((a) => !a.startsWith('--'));
|
|
469
|
+
if (provider !== 'tavily') {
|
|
470
|
+
console.error(`ใช้: ${BRAND.cliName} web setup tavily [--api-key <key>]`);
|
|
471
|
+
process.exit(1);
|
|
472
|
+
}
|
|
473
|
+
let apiKey = (argValue(args, '--api-key') ?? process.env.TAVILY_API_KEY ?? '').trim();
|
|
474
|
+
if (!apiKey)
|
|
475
|
+
apiKey = (await askText('Tavily API key (tvly-...): ')).trim();
|
|
476
|
+
if (!apiKey) {
|
|
477
|
+
console.error('ต้องระบุ Tavily API key');
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
const mcpPath = appHomePath('mcp.json');
|
|
481
|
+
let cfg;
|
|
482
|
+
try {
|
|
483
|
+
const parsed = JSON.parse(await readFile(mcpPath, 'utf8'));
|
|
484
|
+
cfg = { mcpServers: parsed.mcpServers ?? {} };
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
cfg = { mcpServers: {} };
|
|
488
|
+
}
|
|
489
|
+
cfg.mcpServers.tavily = { command: 'npx', args: ['-y', 'tavily-mcp'], env: { TAVILY_API_KEY: apiKey } };
|
|
490
|
+
await mkdir(dirname(mcpPath), { recursive: true });
|
|
491
|
+
await writeFile(mcpPath, `${JSON.stringify(cfg, null, 2)}\n`, { mode: 0o600 });
|
|
492
|
+
await chmod(mcpPath, 0o600).catch(() => { });
|
|
493
|
+
console.log(`ตั้งค่า Tavily แล้ว → ${mcpPath} (chmod 600; key เก็บแบบ env ไม่ echo ออกจอ)`);
|
|
494
|
+
console.log(` • agent runtime ใช้ผ่าน MCP "tavily" (search/extract)`);
|
|
495
|
+
console.log(` • ${BRAND.cliName} web fetch <url> และ ${BRAND.cliName} web search "<q>" จะหยิบ key นี้อัตโนมัติ`);
|
|
496
|
+
console.log(`ทดสอบ: ${BRAND.cliName} mcp test tavily`);
|
|
497
|
+
}
|
|
498
|
+
/** sanook memory [log "<query>" | stats] — read-only view over the bi-temporal memory store */
|
|
499
|
+
async function runMemory(args = []) {
|
|
500
|
+
const action = args[0] && !args[0].startsWith('--') ? args[0] : undefined;
|
|
501
|
+
const rest = action ? args.slice(1) : args;
|
|
502
|
+
const json = args.includes('--json');
|
|
503
|
+
const { loadStore } = await import('./memory-store.js');
|
|
504
|
+
const { memoryLog, renderMemoryLog, memoryStats, renderMemoryStats } = await import('./memory-log.js');
|
|
505
|
+
const store = await loadStore();
|
|
506
|
+
if (action === 'log') {
|
|
507
|
+
const query = rest.filter((a) => !a.startsWith('--')).join(' ').trim();
|
|
508
|
+
const entries = memoryLog(store, query);
|
|
509
|
+
if (json)
|
|
510
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
511
|
+
else
|
|
512
|
+
process.stdout.write(`${renderMemoryLog(entries, query)}\n`);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
// default + `stats`: overview of what's remembered (active / superseded / archived)
|
|
516
|
+
const stats = memoryStats(store);
|
|
517
|
+
console.log(json ? JSON.stringify(stats, null, 2) : renderMemoryStats(stats));
|
|
518
|
+
}
|
|
519
|
+
async function runAgentSetupSummary() {
|
|
520
|
+
const cfg = await loadConfig({});
|
|
521
|
+
console.log(`${BRAND.productName} agent settings`);
|
|
522
|
+
console.log(` model: ${cfg.model}`);
|
|
523
|
+
console.log(` fallbackModel: ${cfg.fallbackModel ?? '(not set)'}`);
|
|
524
|
+
console.log(` personality: ${cfg.personality ?? '(none)'}`);
|
|
525
|
+
console.log(` permissionMode: ${cfg.permissionMode}`);
|
|
526
|
+
console.log(` maxSteps: ${cfg.maxSteps}`);
|
|
527
|
+
console.log(` budgetUsd: ${cfg.budgetUsd ?? '(not set)'}`);
|
|
528
|
+
console.log(` brainPath: ${cfg.brainPath ?? '(not set)'}`);
|
|
529
|
+
console.log(` insights: ${BRAND.cliName} insights [--days N]`);
|
|
530
|
+
console.log('\nแก้ค่าได้ด้วย:');
|
|
531
|
+
console.log(` ${BRAND.cliName} config set personality concise`);
|
|
532
|
+
console.log(` ${BRAND.cliName} config set permissionMode ask`);
|
|
533
|
+
console.log(` ${BRAND.cliName} config set budgetUsd 0.25`);
|
|
534
|
+
console.log(` ${BRAND.cliName} config set fallbackModel haiku`);
|
|
535
|
+
}
|
|
536
|
+
async function runGatewayDoctor() {
|
|
537
|
+
const { checkGateway, formatGatewayDoctorReport } = await import('./gateway/doctor.js');
|
|
538
|
+
const report = await checkGateway();
|
|
539
|
+
console.log(formatGatewayDoctorReport(report));
|
|
540
|
+
if (!report.ok)
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
async function runGatewayStatus() {
|
|
544
|
+
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');
|
|
545
|
+
const cfg = await readGatewayConfig();
|
|
546
|
+
const telegram = resolveTelegramConfig(cfg);
|
|
547
|
+
const discord = resolveDiscordConfig(cfg);
|
|
548
|
+
const slack = resolveSlackConfig(cfg);
|
|
549
|
+
const email = resolveEmailConfig(cfg);
|
|
550
|
+
const homeassistant = resolveHomeAssistantConfig(cfg);
|
|
551
|
+
const line = resolveLineConfig(cfg);
|
|
552
|
+
const mattermost = resolveMattermostConfig(cfg);
|
|
553
|
+
const sms = resolveSmsConfig(cfg);
|
|
554
|
+
const ntfy = resolveNtfyConfig(cfg);
|
|
555
|
+
const signal = resolveSignalConfig(cfg);
|
|
556
|
+
const whatsapp = resolveWhatsAppConfig(cfg);
|
|
557
|
+
const matrix = resolveMatrixConfig(cfg);
|
|
558
|
+
const googleChat = resolveGoogleChatConfig(cfg);
|
|
559
|
+
const bluebubbles = resolveBlueBubblesConfig(cfg);
|
|
560
|
+
const teams = resolveTeamsConfig(cfg);
|
|
561
|
+
const webhooks = resolveWebhookConfig(cfg);
|
|
562
|
+
const { redactSignalId } = await import('./gateway/signal.js');
|
|
563
|
+
const { redactWhatsAppId } = await import('./gateway/whatsapp.js');
|
|
564
|
+
console.log(`${BRAND.productName} gateway`);
|
|
565
|
+
console.log(` config: ${gatewayConfigPath()}`);
|
|
566
|
+
console.log(` token: ${appHomePath('gateway', 'token')} (HTTP bearer, auto-created on run)`);
|
|
567
|
+
const { gatewayServiceStatus } = await import('./gateway/service.js');
|
|
568
|
+
const service = await gatewayServiceStatus();
|
|
569
|
+
console.log(` service: ${service.running ? `running (pid ${service.state?.pid})` : service.state ? `stopped (last pid ${service.state.pid})` : 'not started'}`);
|
|
570
|
+
console.log(` log: ${service.logPath}`);
|
|
571
|
+
console.log(` telegram: ${telegram.token ? `configured via ${telegram.source}` : 'not configured'}`);
|
|
572
|
+
if (telegram.token) {
|
|
573
|
+
console.log(` enabled: ${telegram.enabled}`);
|
|
574
|
+
console.log(` allowed chats: ${telegram.allowedChatIds.length ? telegram.allowedChatIds.join(', ') : '(none — fail closed)'}`);
|
|
575
|
+
console.log(` allow write: ${telegram.allowWrite ? 'yes' : 'no'}`);
|
|
576
|
+
}
|
|
577
|
+
console.log(` discord: ${discord.token ? `configured via ${discord.source}` : 'not configured'}`);
|
|
578
|
+
if (discord.token) {
|
|
579
|
+
console.log(` default channel: ${discord.defaultChannelId ?? '(not set)'}`);
|
|
580
|
+
console.log(` allowed channels: ${discord.allowedChannelIds.length ? discord.allowedChannelIds.join(', ') : '(none)'}`);
|
|
581
|
+
}
|
|
582
|
+
console.log(` slack: ${slack.botToken ? `configured via ${slack.source}` : 'not configured'}`);
|
|
583
|
+
if (slack.botToken) {
|
|
584
|
+
console.log(` app token: ${slack.appToken ? 'set' : '(not set — needed for future Socket Mode gateway)'}`);
|
|
585
|
+
console.log(` default channel: ${slack.defaultChannelId ?? '(not set)'}`);
|
|
586
|
+
console.log(` allowed channels: ${slack.allowedChannelIds.length ? slack.allowedChannelIds.join(', ') : '(none)'}`);
|
|
587
|
+
}
|
|
588
|
+
console.log(` mattermost: ${mattermost.serverUrl || mattermost.token ? `configured via ${mattermost.source}` : 'not configured'}`);
|
|
589
|
+
if (mattermost.serverUrl || mattermost.token) {
|
|
590
|
+
console.log(` server url: ${mattermost.serverUrl ?? '(not set)'}`);
|
|
591
|
+
console.log(` token: ${mattermost.token ? 'set' : '(not set)'}`);
|
|
592
|
+
console.log(` home channel: ${mattermost.homeChannel ?? '(not set)'}`);
|
|
593
|
+
console.log(` allowed users: ${mattermost.allowedUsers.length ? mattermost.allowedUsers.join(', ') : mattermost.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
594
|
+
console.log(` allowed channels: ${mattermost.allowedChannels.length ? mattermost.allowedChannels.join(', ') : '(none)'}`);
|
|
595
|
+
console.log(` free channels: ${mattermost.freeResponseChannels.length ? mattermost.freeResponseChannels.join(', ') : '(none)'}`);
|
|
596
|
+
console.log(` require mention: ${mattermost.requireMention ? 'yes' : 'no'}`);
|
|
597
|
+
console.log(` reply mode: ${mattermost.replyMode}`);
|
|
598
|
+
}
|
|
599
|
+
console.log(` homeassistant: ${homeassistant.token || homeassistant.url !== 'http://homeassistant.local:8123' ? `configured via ${homeassistant.source}` : 'not configured'}`);
|
|
600
|
+
if (homeassistant.token || homeassistant.url !== 'http://homeassistant.local:8123') {
|
|
601
|
+
console.log(` url: ${homeassistant.url}`);
|
|
602
|
+
console.log(` token: ${homeassistant.token ? 'set' : '(not set)'}`);
|
|
603
|
+
console.log(` home channel: ${homeassistant.homeChannel ?? '(not set)'}`);
|
|
604
|
+
console.log(` watch domains: ${homeassistant.watchDomains.length ? homeassistant.watchDomains.join(', ') : '(none)'}`);
|
|
605
|
+
console.log(` watch entities: ${homeassistant.watchEntities.length ? homeassistant.watchEntities.join(', ') : '(none)'}`);
|
|
606
|
+
console.log(` ignore entities: ${homeassistant.ignoreEntities.length ? homeassistant.ignoreEntities.join(', ') : '(none)'}`);
|
|
607
|
+
console.log(` watch all: ${homeassistant.watchAll ? 'yes' : 'no'}`);
|
|
608
|
+
console.log(` cooldown: ${homeassistant.cooldownSeconds}s`);
|
|
609
|
+
}
|
|
610
|
+
console.log(` email: ${email.address ? `configured via ${email.source}` : 'not configured'}`);
|
|
611
|
+
if (email.address) {
|
|
612
|
+
console.log(` address: ${email.address}`);
|
|
613
|
+
console.log(` smtp: ${email.smtpHost ?? '(not set)'}:${email.smtpPort}`);
|
|
614
|
+
console.log(` imap: ${email.imapHost ?? '(not set)'}:${email.imapPort}`);
|
|
615
|
+
console.log(` home address: ${email.homeAddress ?? '(not set)'}`);
|
|
616
|
+
console.log(` allowed senders: ${email.allowedUsers.length ? email.allowedUsers.join(', ') : email.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
617
|
+
}
|
|
618
|
+
console.log(` line: ${line.channelAccessToken ? `configured via ${line.source}` : 'not configured'}`);
|
|
619
|
+
if (line.channelAccessToken) {
|
|
620
|
+
console.log(` channel secret: ${line.channelSecret ? 'set' : '(not set — needed for webhook replies)'}`);
|
|
621
|
+
console.log(` home channel: ${line.homeChannel ?? '(not set)'}`);
|
|
622
|
+
console.log(` allowed users: ${line.allowedUsers.length ? line.allowedUsers.join(', ') : line.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
623
|
+
console.log(` allowed groups: ${line.allowedGroups.length ? line.allowedGroups.join(', ') : '(none)'}`);
|
|
624
|
+
console.log(` allowed rooms: ${line.allowedRooms.length ? line.allowedRooms.join(', ') : '(none)'}`);
|
|
625
|
+
console.log(` public url: ${line.publicUrl ?? '(not set)'}`);
|
|
626
|
+
}
|
|
627
|
+
console.log(` sms: ${sms.accountSid || sms.authToken || sms.phoneNumber ? `configured via ${sms.source}` : 'not configured'}`);
|
|
628
|
+
if (sms.accountSid || sms.authToken || sms.phoneNumber) {
|
|
629
|
+
console.log(` account sid: ${sms.accountSid ? 'set' : '(not set)'}`);
|
|
630
|
+
console.log(` auth token: ${sms.authToken ? 'set' : '(not set)'}`);
|
|
631
|
+
console.log(` phone number: ${sms.phoneNumber ?? '(not set)'}`);
|
|
632
|
+
console.log(` home channel: ${sms.homeChannel ?? '(not set)'}`);
|
|
633
|
+
console.log(` allowed users: ${sms.allowedUsers.length ? sms.allowedUsers.join(', ') : sms.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
634
|
+
console.log(` webhook url: ${sms.webhookUrl ?? (sms.insecureNoSignature ? '(signature disabled)' : '(not set)')}`);
|
|
635
|
+
}
|
|
636
|
+
console.log(` ntfy: ${ntfy.topic || ntfy.token ? `configured via ${ntfy.source}` : 'not configured'}`);
|
|
637
|
+
if (ntfy.topic || ntfy.token) {
|
|
638
|
+
console.log(` server url: ${ntfy.serverUrl}`);
|
|
639
|
+
console.log(` topic: ${ntfy.topic ?? '(not set)'}`);
|
|
640
|
+
console.log(` publish topic: ${ntfy.publishTopic ?? '(same as topic)'}`);
|
|
641
|
+
console.log(` home channel: ${ntfy.homeChannel ?? '(not set)'}`);
|
|
642
|
+
console.log(` allowed topics: ${ntfy.allowedUsers.length ? ntfy.allowedUsers.join(', ') : ntfy.allowAllUsers ? '(all topics)' : '(none)'}`);
|
|
643
|
+
console.log(` token: ${ntfy.token ? 'set' : '(not set)'}`);
|
|
644
|
+
console.log(` markdown: ${ntfy.markdown ? 'yes' : 'no'}`);
|
|
645
|
+
}
|
|
646
|
+
console.log(` signal: ${signal.account ? `configured via ${signal.source}` : 'not configured'}`);
|
|
647
|
+
if (signal.account) {
|
|
648
|
+
console.log(` http url: ${signal.httpUrl}`);
|
|
649
|
+
console.log(` account: ${redactSignalId(signal.account)}`);
|
|
650
|
+
console.log(` home channel: ${redactSignalId(signal.homeChannel)}`);
|
|
651
|
+
console.log(` allowed users: ${signal.allowedUsers.length ? signal.allowedUsers.map(redactSignalId).join(', ') : signal.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
652
|
+
console.log(` allowed groups: ${signal.groupAllowedUsers.length ? signal.groupAllowedUsers.map(redactSignalId).join(', ') : '(none)'}`);
|
|
653
|
+
console.log(` require mention: ${signal.requireMention ? 'yes' : 'no'}`);
|
|
654
|
+
}
|
|
655
|
+
console.log(` whatsapp: ${whatsapp.phoneNumberId || whatsapp.accessToken ? `configured via ${whatsapp.source}` : 'not configured'}`);
|
|
656
|
+
if (whatsapp.phoneNumberId || whatsapp.accessToken) {
|
|
657
|
+
console.log(` phone number id: ${whatsapp.phoneNumberId ? 'set' : '(not set)'}`);
|
|
658
|
+
console.log(` access token: ${whatsapp.accessToken ? 'set' : '(not set)'}`);
|
|
659
|
+
console.log(` app secret: ${whatsapp.appSecret ? 'set' : '(not set — needed for webhook)'}`);
|
|
660
|
+
console.log(` verify token: ${whatsapp.verifyToken ? 'set' : '(not set — needed for webhook verify)'}`);
|
|
661
|
+
console.log(` home channel: ${redactWhatsAppId(whatsapp.homeChannel)}`);
|
|
662
|
+
console.log(` allowed users: ${whatsapp.allowedUsers.length ? whatsapp.allowedUsers.map(redactWhatsAppId).join(', ') : whatsapp.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
663
|
+
console.log(` public url: ${whatsapp.publicUrl ?? '(not set)'}`);
|
|
664
|
+
console.log(` api version: ${whatsapp.apiVersion}`);
|
|
665
|
+
}
|
|
666
|
+
console.log(` matrix: ${matrix.homeserver || matrix.accessToken || matrix.userId ? `configured via ${matrix.source}` : 'not configured'}`);
|
|
667
|
+
if (matrix.homeserver || matrix.accessToken || matrix.userId) {
|
|
668
|
+
console.log(` homeserver: ${matrix.homeserver ?? '(not set)'}`);
|
|
669
|
+
console.log(` access token: ${matrix.accessToken ? 'set' : '(not set)'}`);
|
|
670
|
+
console.log(` user id: ${matrix.userId ?? '(not set)'}`);
|
|
671
|
+
console.log(` password: ${matrix.password ? 'set' : '(not set)'}`);
|
|
672
|
+
console.log(` home room: ${matrix.homeRoom ?? '(not set)'}`);
|
|
673
|
+
console.log(` allowed users: ${matrix.allowedUsers.length ? matrix.allowedUsers.join(', ') : matrix.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
674
|
+
console.log(` allowed rooms: ${matrix.allowedRooms.length ? matrix.allowedRooms.join(', ') : '(none)'}`);
|
|
675
|
+
console.log(` free rooms: ${matrix.freeResponseRooms.length ? matrix.freeResponseRooms.join(', ') : '(none)'}`);
|
|
676
|
+
console.log(` require mention: ${matrix.requireMention ? 'yes' : 'no'}`);
|
|
677
|
+
console.log(` auto join: ${matrix.autoJoin ? 'yes' : 'no'}`);
|
|
678
|
+
}
|
|
679
|
+
console.log(` googlechat: ${googleChat.serviceAccountJson || googleChat.incomingWebhookUrl ? `configured via ${googleChat.source}` : 'not configured'}`);
|
|
680
|
+
if (googleChat.serviceAccountJson || googleChat.incomingWebhookUrl) {
|
|
681
|
+
console.log(` project id: ${googleChat.projectId ?? '(not set)'}`);
|
|
682
|
+
console.log(` subscription: ${googleChat.subscriptionName ? 'set' : '(not set — needed for Pub/Sub inbound)'}`);
|
|
683
|
+
console.log(` service account: ${googleChat.serviceAccountJson ? 'set' : '(not set)'}`);
|
|
684
|
+
console.log(` api base url: ${googleChat.apiBaseUrl}`);
|
|
685
|
+
console.log(` webhook url: ${googleChat.incomingWebhookUrl ? 'set' : '(not set)'}`);
|
|
686
|
+
console.log(` home channel: ${googleChat.homeChannel ?? '(not set)'}`);
|
|
687
|
+
console.log(` allowed spaces: ${googleChat.allowedSpaces.length ? googleChat.allowedSpaces.join(', ') : googleChat.allowAllSpaces ? '(all spaces)' : '(none)'}`);
|
|
688
|
+
console.log(` allowed users: ${googleChat.allowedUsers.length ? googleChat.allowedUsers.join(', ') : googleChat.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
689
|
+
console.log(` free spaces: ${googleChat.freeResponseSpaces.length ? googleChat.freeResponseSpaces.join(', ') : '(none)'}`);
|
|
690
|
+
console.log(` flow control: messages=${googleChat.maxMessages}, bytes=${googleChat.maxBytes}`);
|
|
691
|
+
}
|
|
692
|
+
console.log(` bluebubbles: ${bluebubbles.serverUrl || bluebubbles.password ? `configured via ${bluebubbles.source}` : 'not configured'}`);
|
|
693
|
+
if (bluebubbles.serverUrl || bluebubbles.password) {
|
|
694
|
+
console.log(` server url: ${bluebubbles.serverUrl ?? '(not set)'}`);
|
|
695
|
+
console.log(` password: ${bluebubbles.password ? 'set' : '(not set)'}`);
|
|
696
|
+
console.log(` webhook: ${bluebubbles.webhookHost}:${bluebubbles.webhookPort}${bluebubbles.webhookPath}`);
|
|
697
|
+
console.log(` home channel: ${bluebubbles.homeChannel ?? '(not set)'}`);
|
|
698
|
+
console.log(` allowed targets: ${bluebubbles.allowedUsers.length ? bluebubbles.allowedUsers.join(', ') : bluebubbles.allowAllUsers ? '(all targets)' : '(none)'}`);
|
|
699
|
+
console.log(` require mention: ${bluebubbles.requireMention ? 'yes' : 'no'}`);
|
|
700
|
+
}
|
|
701
|
+
console.log(` teams: ${teams.incomingWebhookUrl || teams.graphAccessToken || teams.clientId ? `configured via ${teams.source}` : 'not configured'}`);
|
|
702
|
+
if (teams.incomingWebhookUrl || teams.graphAccessToken || teams.clientId) {
|
|
703
|
+
console.log(` delivery mode: ${teams.deliveryMode}`);
|
|
704
|
+
console.log(` webhook url: ${teams.incomingWebhookUrl ? 'set' : '(not set)'}`);
|
|
705
|
+
console.log(` graph token: ${teams.graphAccessToken ? 'set' : '(not set)'}`);
|
|
706
|
+
console.log(` chat id: ${teams.chatId ?? '(not set)'}`);
|
|
707
|
+
console.log(` team/channel: ${teams.teamId && teams.channelId ? `${teams.teamId}/${teams.channelId}` : '(not set)'}`);
|
|
708
|
+
console.log(` home channel: ${teams.homeChannel ?? '(not set)'}`);
|
|
709
|
+
console.log(` bot app: ${teams.clientId && teams.tenantId ? 'set' : '(not set)'}`);
|
|
710
|
+
console.log(` allowed users: ${teams.allowedUsers.length ? teams.allowedUsers.join(', ') : teams.allowAllUsers ? '(all users)' : '(none)'}`);
|
|
711
|
+
console.log(` webhook port: ${teams.port}`);
|
|
712
|
+
}
|
|
713
|
+
console.log(` webhooks: ${webhooks.enabled ? `enabled via ${webhooks.source}` : 'not enabled'} (${Object.keys(webhooks.routes).length} route${Object.keys(webhooks.routes).length === 1 ? '' : 's'})`);
|
|
714
|
+
if (webhooks.enabled) {
|
|
715
|
+
console.log(` global secret: ${webhooks.secret ? 'set' : '(not set)'}`);
|
|
716
|
+
console.log(` public url: ${webhooks.publicUrl ?? '(not set)'}`);
|
|
717
|
+
console.log(` rate limit: ${webhooks.rateLimitPerMinute}/minute`);
|
|
718
|
+
}
|
|
719
|
+
const { checkGateway, formatGatewayDoctorStatus, listPendingCronJobs, listRecentDeliveryFailures, summarizeChannelHealth, } = await import('./gateway/doctor.js');
|
|
720
|
+
const pendingCron = await listPendingCronJobs();
|
|
721
|
+
console.log(`\ncron (pending): ${pendingCron.length}`);
|
|
722
|
+
if (pendingCron.length) {
|
|
723
|
+
for (const task of pendingCron.slice(0, 10)) {
|
|
724
|
+
const when = new Date(task.runAt).toISOString();
|
|
725
|
+
const deliver = task.deliver ? ` → ${task.deliver}` : '';
|
|
726
|
+
console.log(` ${task.id} ${when} ${task.schedule ?? 'once'} ${task.spec.slice(0, 60)}${deliver}`);
|
|
727
|
+
}
|
|
728
|
+
if (pendingCron.length > 10)
|
|
729
|
+
console.log(` … และอีก ${pendingCron.length - 10} งาน`);
|
|
730
|
+
}
|
|
731
|
+
const deliveryFailures = await listRecentDeliveryFailures(5);
|
|
732
|
+
console.log(`\ndelivery failures (recent): ${deliveryFailures.length ? deliveryFailures.length : 'none'}`);
|
|
733
|
+
for (const failure of deliveryFailures) {
|
|
734
|
+
const when = failure.lastRun ? new Date(failure.lastRun).toISOString() : '(unknown)';
|
|
735
|
+
console.log(` ${failure.taskId} ${when} ${failure.deliver} ${failure.error.slice(0, 120)}`);
|
|
736
|
+
}
|
|
737
|
+
const healthReport = await checkGateway({ config: cfg, skipNetwork: true });
|
|
738
|
+
const health = summarizeChannelHealth(healthReport.checks).filter((item) => item.status !== 'skip');
|
|
739
|
+
console.log(`\nchannel health (config): ${health.length ? '' : 'no configured channels'}`);
|
|
740
|
+
for (const item of health) {
|
|
741
|
+
console.log(` ${item.channel.padEnd(12)} ${formatGatewayDoctorStatus(item.status)}`);
|
|
742
|
+
}
|
|
743
|
+
console.log(` (live token/webhook probes: ${BRAND.cliName} gateway doctor)`);
|
|
744
|
+
console.log(`\nredacted config:\n${JSON.stringify(redactGatewayConfig(cfg), null, 2)}`);
|
|
745
|
+
}
|
|
746
|
+
async function runGatewaySetup(args) {
|
|
747
|
+
const platformArgProvided = Boolean(args[0] && !args[0].startsWith('--'));
|
|
748
|
+
let platform = platformArgProvided ? args[0] : undefined;
|
|
749
|
+
const rest = platformArgProvided ? args.slice(1) : args;
|
|
750
|
+
if (!platform) {
|
|
751
|
+
if (!process.stdin.isTTY) {
|
|
752
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup <telegram|discord|slack|mattermost|homeassistant|email|line|sms|ntfy|signal|whatsapp|matrix|googlechat|bluebubbles|teams|webhooks> [options]`);
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
const { readGatewayConfig, resolveBlueBubblesConfig, resolveDiscordConfig, resolveEmailConfig, resolveGoogleChatConfig, resolveHomeAssistantConfig, resolveLineConfig, resolveMattermostConfig, resolveMatrixConfig, resolveNtfyConfig, resolveSignalConfig, resolveSlackConfig, resolveSmsConfig, resolveTelegramConfig, resolveTeamsConfig, resolveWhatsAppConfig, resolveWebhookConfig, } = await import('./gateway/config.js');
|
|
756
|
+
const cfg = await readGatewayConfig();
|
|
757
|
+
const options = [
|
|
758
|
+
{ id: 'telegram', label: `Telegram ${resolveTelegramConfig(cfg).token ? '(configured)' : ''}` },
|
|
759
|
+
{ id: 'discord', label: `Discord ${resolveDiscordConfig(cfg).token ? '(configured)' : ''}` },
|
|
760
|
+
{ id: 'slack', label: `Slack ${resolveSlackConfig(cfg).botToken ? '(configured)' : ''}` },
|
|
761
|
+
{ id: 'mattermost', label: `Mattermost ${resolveMattermostConfig(cfg).serverUrl ? '(configured)' : ''}` },
|
|
762
|
+
{ id: 'homeassistant', label: `Home Assistant ${resolveHomeAssistantConfig(cfg).token ? '(configured)' : ''}` },
|
|
763
|
+
{ id: 'email', label: `Email ${resolveEmailConfig(cfg).address ? '(configured)' : ''}` },
|
|
764
|
+
{ id: 'line', label: `LINE ${resolveLineConfig(cfg).channelAccessToken ? '(configured)' : ''}` },
|
|
765
|
+
{ id: 'sms', label: `SMS/Twilio ${resolveSmsConfig(cfg).accountSid ? '(configured)' : ''}` },
|
|
766
|
+
{ id: 'ntfy', label: `ntfy ${resolveNtfyConfig(cfg).topic ? '(configured)' : ''}` },
|
|
767
|
+
{ id: 'signal', label: `Signal ${resolveSignalConfig(cfg).account ? '(configured)' : ''}` },
|
|
768
|
+
{ id: 'whatsapp', label: `WhatsApp Cloud ${resolveWhatsAppConfig(cfg).phoneNumberId ? '(configured)' : ''}` },
|
|
769
|
+
{ id: 'matrix', label: `Matrix ${resolveMatrixConfig(cfg).homeserver ? '(configured)' : ''}` },
|
|
770
|
+
{ id: 'googlechat', label: `Google Chat ${resolveGoogleChatConfig(cfg).serviceAccountJson || resolveGoogleChatConfig(cfg).incomingWebhookUrl ? '(configured)' : ''}` },
|
|
771
|
+
{ id: 'bluebubbles', label: `BlueBubbles/iMessage ${resolveBlueBubblesConfig(cfg).serverUrl ? '(configured)' : ''}` },
|
|
772
|
+
{ id: 'teams', label: `Microsoft Teams ${resolveTeamsConfig(cfg).incomingWebhookUrl || resolveTeamsConfig(cfg).graphAccessToken ? '(configured)' : ''}` },
|
|
773
|
+
{ id: 'webhooks', label: `Webhooks ${resolveWebhookConfig(cfg).enabled ? '(configured)' : ''}` },
|
|
774
|
+
];
|
|
775
|
+
console.log(`${BRAND.productName} gateway setup`);
|
|
776
|
+
for (const [i, option] of options.entries())
|
|
777
|
+
console.log(` ${i + 1}. ${option.label}`);
|
|
778
|
+
const answer = await askText('เลือก platform [1-16]: ');
|
|
779
|
+
const index = Number(answer || '1') - 1;
|
|
780
|
+
platform = options[index]?.id;
|
|
781
|
+
}
|
|
782
|
+
if (platform === 'whatsapp-cloud')
|
|
783
|
+
platform = 'whatsapp';
|
|
784
|
+
if (platform === 'msteams' || platform === 'ms-teams' || platform === 'microsoft-teams')
|
|
785
|
+
platform = 'teams';
|
|
786
|
+
if (platform === 'google-chat' || platform === 'google_chat' || platform === 'gchat')
|
|
787
|
+
platform = 'googlechat';
|
|
788
|
+
if (platform === 'blue-bubbles' || platform === 'blue_bubbles' || platform === 'imessage')
|
|
789
|
+
platform = 'bluebubbles';
|
|
790
|
+
if (!platform ||
|
|
791
|
+
![
|
|
792
|
+
'telegram',
|
|
793
|
+
'discord',
|
|
794
|
+
'slack',
|
|
795
|
+
'mattermost',
|
|
796
|
+
'homeassistant',
|
|
797
|
+
'hass',
|
|
798
|
+
'email',
|
|
799
|
+
'line',
|
|
800
|
+
'sms',
|
|
801
|
+
'ntfy',
|
|
802
|
+
'signal',
|
|
803
|
+
'whatsapp',
|
|
804
|
+
'matrix',
|
|
805
|
+
'googlechat',
|
|
806
|
+
'bluebubbles',
|
|
807
|
+
'teams',
|
|
808
|
+
'webhooks',
|
|
809
|
+
].includes(platform)) {
|
|
810
|
+
console.error(`ตอนนี้ setup อัตโนมัติรองรับ telegram / discord / slack / mattermost / homeassistant / email / line / sms / ntfy / signal / whatsapp / matrix / googlechat / bluebubbles / teams / webhooks — ได้ "${platform ?? ''}"`);
|
|
811
|
+
process.exit(1);
|
|
812
|
+
}
|
|
813
|
+
if (platform === 'discord')
|
|
814
|
+
return runDiscordGatewaySetup(rest);
|
|
815
|
+
if (platform === 'slack')
|
|
816
|
+
return runSlackGatewaySetup(rest);
|
|
817
|
+
if (platform === 'mattermost')
|
|
818
|
+
return runMattermostGatewaySetup(rest);
|
|
819
|
+
if (platform === 'homeassistant' || platform === 'hass')
|
|
820
|
+
return runHomeAssistantGatewaySetup(rest);
|
|
821
|
+
if (platform === 'email')
|
|
822
|
+
return runEmailGatewaySetup(rest);
|
|
823
|
+
if (platform === 'line')
|
|
824
|
+
return runLineGatewaySetup(rest);
|
|
825
|
+
if (platform === 'sms')
|
|
826
|
+
return runSmsGatewaySetup(rest);
|
|
827
|
+
if (platform === 'ntfy')
|
|
828
|
+
return runNtfyGatewaySetup(rest);
|
|
829
|
+
if (platform === 'signal')
|
|
830
|
+
return runSignalGatewaySetup(rest);
|
|
831
|
+
if (platform === 'whatsapp')
|
|
832
|
+
return runWhatsAppGatewaySetup(rest);
|
|
833
|
+
if (platform === 'matrix')
|
|
834
|
+
return runMatrixGatewaySetup(rest);
|
|
835
|
+
if (platform === 'googlechat')
|
|
836
|
+
return runGoogleChatGatewaySetup(rest);
|
|
837
|
+
if (platform === 'bluebubbles')
|
|
838
|
+
return runBlueBubblesGatewaySetup(rest);
|
|
839
|
+
if (platform === 'teams')
|
|
840
|
+
return runTeamsGatewaySetup(rest);
|
|
841
|
+
if (platform === 'webhooks')
|
|
842
|
+
return runWebhookGatewaySetup(rest);
|
|
843
|
+
let token = argValue(rest, '--bot-token', '--token');
|
|
844
|
+
let allowedRaw = argValue(rest, '--allowed-chats', '--chat-ids');
|
|
845
|
+
const allowWrite = rest.includes('--allow-write');
|
|
846
|
+
if (!token) {
|
|
847
|
+
if (!process.stdin.isTTY) {
|
|
848
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup telegram --bot-token <token> --allowed-chats <chat_id[,chat_id]>`);
|
|
849
|
+
process.exit(1);
|
|
850
|
+
}
|
|
851
|
+
console.log(`${BRAND.productName} Telegram setup`);
|
|
852
|
+
console.log(`สร้าง bot ผ่าน @BotFather แล้ววาง token ที่นี่ (จะเก็บใน ${appHomePath('gateway', 'config.json')} chmod 600)`);
|
|
853
|
+
token = await askText('Telegram bot token: ');
|
|
854
|
+
}
|
|
855
|
+
if (!allowedRaw) {
|
|
856
|
+
if (!process.stdin.isTTY) {
|
|
857
|
+
console.error('ต้องระบุ --allowed-chats <chat_id[,chat_id]> เพื่อ fail-closed');
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
allowedRaw = await askText('Allowed private chat IDs (comma-separated): ');
|
|
861
|
+
}
|
|
862
|
+
const { parseAllowedChats } = await import('./gateway/telegram.js');
|
|
863
|
+
const allowedChatIds = parseAllowedChats(allowedRaw);
|
|
864
|
+
if (!token.trim() || !allowedChatIds.length) {
|
|
865
|
+
console.error('Telegram setup ต้องมี bot token และ allowed chat id อย่างน้อย 1 ค่า');
|
|
866
|
+
process.exit(1);
|
|
867
|
+
}
|
|
868
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
869
|
+
await patchGatewayConfig({
|
|
870
|
+
telegram: {
|
|
871
|
+
enabled: true,
|
|
872
|
+
botToken: token.trim(),
|
|
873
|
+
allowedChatIds,
|
|
874
|
+
allowWrite,
|
|
875
|
+
},
|
|
876
|
+
});
|
|
877
|
+
console.log(`บันทึก Telegram gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
878
|
+
console.log(`รัน: ${BRAND.cliName} gateway run`);
|
|
879
|
+
}
|
|
880
|
+
function parseStringCsv(raw) {
|
|
881
|
+
if (!raw)
|
|
882
|
+
return [];
|
|
883
|
+
return raw
|
|
884
|
+
.split(',')
|
|
885
|
+
.map((s) => s.trim())
|
|
886
|
+
.filter(Boolean);
|
|
887
|
+
}
|
|
888
|
+
async function runDiscordGatewaySetup(args) {
|
|
889
|
+
let token = argValue(args, '--bot-token', '--token');
|
|
890
|
+
let defaultChannel = argValue(args, '--channel', '--default-channel');
|
|
891
|
+
let allowedRaw = argValue(args, '--allowed-channels', '--channel-ids');
|
|
892
|
+
const allowWrite = args.includes('--allow-write');
|
|
893
|
+
if (!token) {
|
|
894
|
+
if (!process.stdin.isTTY) {
|
|
895
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup discord --bot-token <token> --channel <channel_id>`);
|
|
896
|
+
process.exit(1);
|
|
897
|
+
}
|
|
898
|
+
console.log(`${BRAND.productName} Discord setup`);
|
|
899
|
+
console.log('สร้าง bot ใน Discord Developer Portal, เปิด Message Content Intent, แล้ววาง Bot Token ที่นี่');
|
|
900
|
+
token = await askText('Discord bot token: ');
|
|
901
|
+
}
|
|
902
|
+
if (!defaultChannel && !allowedRaw) {
|
|
903
|
+
if (!process.stdin.isTTY) {
|
|
904
|
+
console.error('ต้องระบุ --channel <channel_id> หรือ --allowed-channels <id[,id]>');
|
|
905
|
+
process.exit(1);
|
|
906
|
+
}
|
|
907
|
+
defaultChannel = await askText('Default Discord channel ID: ');
|
|
908
|
+
}
|
|
909
|
+
const allowedChannelIds = parseStringCsv(allowedRaw ?? defaultChannel);
|
|
910
|
+
if (!token.trim() || !allowedChannelIds.length) {
|
|
911
|
+
console.error('Discord setup ต้องมี bot token และ channel id อย่างน้อย 1 ค่า');
|
|
912
|
+
process.exit(1);
|
|
913
|
+
}
|
|
914
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
915
|
+
await patchGatewayConfig({
|
|
916
|
+
discord: {
|
|
917
|
+
enabled: true,
|
|
918
|
+
botToken: token.trim(),
|
|
919
|
+
defaultChannelId: (defaultChannel ?? allowedChannelIds[0]).trim(),
|
|
920
|
+
allowedChannelIds,
|
|
921
|
+
allowWrite,
|
|
922
|
+
},
|
|
923
|
+
});
|
|
924
|
+
console.log(`บันทึก Discord gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
925
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to discord "hello"`);
|
|
926
|
+
}
|
|
927
|
+
async function runSlackGatewaySetup(args) {
|
|
928
|
+
let botToken = argValue(args, '--bot-token', '--token');
|
|
929
|
+
let appToken = argValue(args, '--app-token');
|
|
930
|
+
let defaultChannel = argValue(args, '--channel', '--default-channel');
|
|
931
|
+
let allowedRaw = argValue(args, '--allowed-channels', '--channel-ids');
|
|
932
|
+
const allowWrite = args.includes('--allow-write');
|
|
933
|
+
if (!botToken) {
|
|
934
|
+
if (!process.stdin.isTTY) {
|
|
935
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup slack --bot-token <xoxb-token> --app-token <xapp-token> --channel <channel_id>`);
|
|
936
|
+
process.exit(1);
|
|
937
|
+
}
|
|
938
|
+
console.log(`${BRAND.productName} Slack setup`);
|
|
939
|
+
console.log('สร้าง Slack app, เปิด Socket Mode, เพิ่ม scopes แล้ววาง Bot Token (xoxb-) ที่นี่');
|
|
940
|
+
botToken = await askText('Slack bot token (xoxb-): ');
|
|
941
|
+
}
|
|
942
|
+
if (!appToken && process.stdin.isTTY) {
|
|
943
|
+
appToken = await askText('Slack app token (xapp-, optional for outbound send but needed for gateway Socket Mode): ');
|
|
944
|
+
}
|
|
945
|
+
if (!defaultChannel && !allowedRaw) {
|
|
946
|
+
if (!process.stdin.isTTY) {
|
|
947
|
+
console.error('ต้องระบุ --channel <channel_id> หรือ --allowed-channels <id[,id]>');
|
|
948
|
+
process.exit(1);
|
|
949
|
+
}
|
|
950
|
+
defaultChannel = await askText('Default Slack channel ID: ');
|
|
951
|
+
}
|
|
952
|
+
const allowedChannelIds = parseStringCsv(allowedRaw ?? defaultChannel);
|
|
953
|
+
if (!botToken.trim() || !allowedChannelIds.length) {
|
|
954
|
+
console.error('Slack setup ต้องมี bot token และ channel id อย่างน้อย 1 ค่า');
|
|
955
|
+
process.exit(1);
|
|
956
|
+
}
|
|
957
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
958
|
+
await patchGatewayConfig({
|
|
959
|
+
slack: {
|
|
960
|
+
enabled: true,
|
|
961
|
+
botToken: botToken.trim(),
|
|
962
|
+
appToken: appToken?.trim() || undefined,
|
|
963
|
+
defaultChannelId: (defaultChannel ?? allowedChannelIds[0]).trim(),
|
|
964
|
+
allowedChannelIds,
|
|
965
|
+
allowWrite,
|
|
966
|
+
},
|
|
967
|
+
});
|
|
968
|
+
console.log(`บันทึก Slack gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
969
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to slack "hello"`);
|
|
970
|
+
}
|
|
971
|
+
async function runMattermostGatewaySetup(args) {
|
|
972
|
+
let serverUrl = argValue(args, '--url', '--server-url');
|
|
973
|
+
let token = argValue(args, '--token');
|
|
974
|
+
let homeChannel = argValue(args, '--home-channel', '--channel', '--to');
|
|
975
|
+
let allowedUsersRaw = argValue(args, '--allowed-users');
|
|
976
|
+
let allowedChannelsRaw = argValue(args, '--allowed-channels', '--channel-ids');
|
|
977
|
+
let freeResponseChannelsRaw = argValue(args, '--free-response-channels');
|
|
978
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
979
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
980
|
+
const requireMention = !args.includes('--no-require-mention');
|
|
981
|
+
const groupSessionsPerUser = !args.includes('--shared-channel-session');
|
|
982
|
+
const replyMode = args.includes('--thread-replies') || argValue(args, '--reply-mode') === 'thread' ? 'thread' : 'off';
|
|
983
|
+
if (!serverUrl) {
|
|
984
|
+
if (!process.stdin.isTTY) {
|
|
985
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup mattermost --url <https://mm.example.com> --token <token> --allowed-users <user_id[,user_id]> --home-channel <channel_id>`);
|
|
986
|
+
process.exit(1);
|
|
987
|
+
}
|
|
988
|
+
console.log(`${BRAND.productName} Mattermost setup`);
|
|
989
|
+
console.log('ใช้ Mattermost REST API v4 + WebSocket; แนะนำ token ของ dedicated bot account');
|
|
990
|
+
serverUrl = await askText('Mattermost server URL (เช่น https://mm.example.com): ');
|
|
991
|
+
}
|
|
992
|
+
if (!token) {
|
|
993
|
+
if (!process.stdin.isTTY) {
|
|
994
|
+
console.error('ต้องระบุ --token <Mattermost personal/bot access token>');
|
|
995
|
+
process.exit(1);
|
|
996
|
+
}
|
|
997
|
+
token = await askText('Mattermost token: ');
|
|
998
|
+
}
|
|
999
|
+
if (!allowedUsersRaw && !allowAllUsers) {
|
|
1000
|
+
if (!process.stdin.isTTY) {
|
|
1001
|
+
console.error('ต้องระบุ --allowed-users <user_id[,user_id]> เพื่อ fail-closed หรือ --allow-all-users');
|
|
1002
|
+
process.exit(1);
|
|
1003
|
+
}
|
|
1004
|
+
allowedUsersRaw = await askText('Allowed Mattermost user IDs (comma-separated): ');
|
|
1005
|
+
}
|
|
1006
|
+
if (!homeChannel && !allowedChannelsRaw) {
|
|
1007
|
+
if (!process.stdin.isTTY) {
|
|
1008
|
+
console.error('ต้องระบุ --home-channel <channel_id> หรือ --allowed-channels <channel_id[,id]> เพื่อส่งข้อความออกแบบ fail-closed');
|
|
1009
|
+
process.exit(1);
|
|
1010
|
+
}
|
|
1011
|
+
homeChannel = await askText('Mattermost home channel ID (blank = skip outbound home): ');
|
|
1012
|
+
}
|
|
1013
|
+
const { normalizeMattermostUrl } = await import('./gateway/mattermost.js');
|
|
1014
|
+
const cleanServerUrl = normalizeMattermostUrl(serverUrl);
|
|
1015
|
+
const cleanHome = homeChannel?.trim();
|
|
1016
|
+
const allowedUsers = parseStringCsv(allowedUsersRaw);
|
|
1017
|
+
const allowedChannels = parseStringCsv(allowedChannelsRaw ?? cleanHome);
|
|
1018
|
+
const freeResponseChannels = parseStringCsv(freeResponseChannelsRaw);
|
|
1019
|
+
if (!cleanServerUrl) {
|
|
1020
|
+
console.error('Mattermost setup ต้องมี server URL ที่ขึ้นต้นด้วย http:// หรือ https://');
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
if (!token.trim()) {
|
|
1024
|
+
console.error('Mattermost setup ต้องมี token');
|
|
1025
|
+
process.exit(1);
|
|
1026
|
+
}
|
|
1027
|
+
if (!allowAllUsers && !allowedUsers.length) {
|
|
1028
|
+
console.error('Mattermost setup ต้องมี allowed users อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
1029
|
+
process.exit(1);
|
|
1030
|
+
}
|
|
1031
|
+
if (!cleanHome && !allowedChannels.length) {
|
|
1032
|
+
console.error('Mattermost setup ต้องมี home channel/allowed channels อย่างน้อย 1 ค่า');
|
|
1033
|
+
process.exit(1);
|
|
1034
|
+
}
|
|
1035
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1036
|
+
await patchGatewayConfig({
|
|
1037
|
+
mattermost: {
|
|
1038
|
+
enabled: true,
|
|
1039
|
+
serverUrl: cleanServerUrl,
|
|
1040
|
+
token: token.trim(),
|
|
1041
|
+
homeChannel: cleanHome || allowedChannels[0],
|
|
1042
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
1043
|
+
allowedUsers,
|
|
1044
|
+
allowedChannels,
|
|
1045
|
+
freeResponseChannels,
|
|
1046
|
+
allowAllUsers,
|
|
1047
|
+
requireMention,
|
|
1048
|
+
groupSessionsPerUser,
|
|
1049
|
+
replyMode,
|
|
1050
|
+
},
|
|
1051
|
+
});
|
|
1052
|
+
console.log(`บันทึก Mattermost gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1053
|
+
console.log(`Mattermost websocket: ${cleanServerUrl}/api/v4/websocket`);
|
|
1054
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to mattermost "hello"`);
|
|
1055
|
+
}
|
|
1056
|
+
async function runHomeAssistantGatewaySetup(args) {
|
|
1057
|
+
const url = argValue(args, '--url')?.trim() || 'http://homeassistant.local:8123';
|
|
1058
|
+
let token = argValue(args, '--token');
|
|
1059
|
+
let homeChannel = argValue(args, '--home-channel', '--notification-id', '--to');
|
|
1060
|
+
let watchDomainsRaw = argValue(args, '--watch-domains', '--domains');
|
|
1061
|
+
let watchEntitiesRaw = argValue(args, '--watch-entities', '--entities');
|
|
1062
|
+
let ignoreEntitiesRaw = argValue(args, '--ignore-entities');
|
|
1063
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
1064
|
+
const watchAll = args.includes('--watch-all');
|
|
1065
|
+
const cooldownSecondsRaw = argValue(args, '--cooldown-seconds', '--cooldown');
|
|
1066
|
+
if (!token) {
|
|
1067
|
+
if (!process.stdin.isTTY) {
|
|
1068
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup homeassistant --token <long-lived-token> [--url http://homeassistant.local:8123] --watch-domains climate,binary_sensor`);
|
|
1069
|
+
process.exit(1);
|
|
1070
|
+
}
|
|
1071
|
+
console.log(`${BRAND.productName} Home Assistant setup`);
|
|
1072
|
+
console.log('สร้าง Long-Lived Access Token จาก Home Assistant Profile แล้ววาง token ที่นี่');
|
|
1073
|
+
token = await askText('Home Assistant long-lived access token: ');
|
|
1074
|
+
}
|
|
1075
|
+
if (!homeChannel && process.stdin.isTTY) {
|
|
1076
|
+
homeChannel = (await askText('Persistent notification id (blank = sanook_agent): ')) || 'sanook_agent';
|
|
1077
|
+
}
|
|
1078
|
+
if (!watchDomainsRaw && !watchEntitiesRaw && !watchAll) {
|
|
1079
|
+
if (!process.stdin.isTTY) {
|
|
1080
|
+
console.error('ต้องระบุ --watch-domains, --watch-entities หรือ --watch-all เพื่อรับ state_changed events');
|
|
1081
|
+
process.exit(1);
|
|
1082
|
+
}
|
|
1083
|
+
watchDomainsRaw = await askText('Watch domains (comma-separated; เช่น climate,binary_sensor,alarm_control_panel): ');
|
|
1084
|
+
}
|
|
1085
|
+
const { homeAssistantWebSocketUrl, normalizeHomeAssistantUrl } = await import('./gateway/homeassistant.js');
|
|
1086
|
+
const cleanUrl = normalizeHomeAssistantUrl(url);
|
|
1087
|
+
const watchDomains = parseStringCsv(watchDomainsRaw);
|
|
1088
|
+
const watchEntities = parseStringCsv(watchEntitiesRaw);
|
|
1089
|
+
const ignoreEntities = parseStringCsv(ignoreEntitiesRaw);
|
|
1090
|
+
const cooldownSeconds = cooldownSecondsRaw ? Number(cooldownSecondsRaw) : undefined;
|
|
1091
|
+
if (!cleanUrl) {
|
|
1092
|
+
console.error('Home Assistant setup ต้องมี URL ที่ขึ้นต้นด้วย http:// หรือ https://');
|
|
1093
|
+
process.exit(1);
|
|
1094
|
+
}
|
|
1095
|
+
if (!token.trim()) {
|
|
1096
|
+
console.error('Home Assistant setup ต้องมี token');
|
|
1097
|
+
process.exit(1);
|
|
1098
|
+
}
|
|
1099
|
+
if (!watchAll && !watchDomains.length && !watchEntities.length) {
|
|
1100
|
+
console.error('Home Assistant setup ต้องมี watch domains/entities อย่างน้อย 1 ค่า หรือระบุ --watch-all');
|
|
1101
|
+
process.exit(1);
|
|
1102
|
+
}
|
|
1103
|
+
if (cooldownSecondsRaw && (!Number.isInteger(cooldownSeconds) || Number(cooldownSeconds) <= 0)) {
|
|
1104
|
+
console.error('--cooldown-seconds ต้องเป็น integer มากกว่า 0');
|
|
1105
|
+
process.exit(1);
|
|
1106
|
+
}
|
|
1107
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1108
|
+
await patchGatewayConfig({
|
|
1109
|
+
homeassistant: {
|
|
1110
|
+
enabled: true,
|
|
1111
|
+
url: cleanUrl,
|
|
1112
|
+
token: token.trim(),
|
|
1113
|
+
homeChannel: homeChannel?.trim() || 'sanook_agent',
|
|
1114
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
1115
|
+
watchDomains,
|
|
1116
|
+
watchEntities,
|
|
1117
|
+
ignoreEntities,
|
|
1118
|
+
watchAll,
|
|
1119
|
+
cooldownSeconds,
|
|
1120
|
+
},
|
|
1121
|
+
});
|
|
1122
|
+
console.log(`บันทึก Home Assistant gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1123
|
+
console.log(`Home Assistant websocket: ${homeAssistantWebSocketUrl(cleanUrl)}`);
|
|
1124
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to homeassistant "hello"`);
|
|
1125
|
+
}
|
|
1126
|
+
async function runLineGatewaySetup(args) {
|
|
1127
|
+
let channelAccessToken = argValue(args, '--channel-access-token', '--access-token', '--token');
|
|
1128
|
+
let channelSecret = argValue(args, '--channel-secret', '--secret');
|
|
1129
|
+
let homeChannel = argValue(args, '--home-channel', '--to');
|
|
1130
|
+
let allowedUsersRaw = argValue(args, '--allowed-users');
|
|
1131
|
+
let allowedGroupsRaw = argValue(args, '--allowed-groups');
|
|
1132
|
+
let allowedRoomsRaw = argValue(args, '--allowed-rooms');
|
|
1133
|
+
const publicUrl = argValue(args, '--public-url');
|
|
1134
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1135
|
+
if (!channelAccessToken) {
|
|
1136
|
+
if (!process.stdin.isTTY) {
|
|
1137
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup line --channel-access-token <token> --home-channel <U/C/R-id>`);
|
|
1138
|
+
process.exit(1);
|
|
1139
|
+
}
|
|
1140
|
+
console.log(`${BRAND.productName} LINE setup`);
|
|
1141
|
+
console.log('สร้าง LINE Messaging API channel แล้ววาง long-lived Channel access token ที่นี่');
|
|
1142
|
+
channelAccessToken = await askText('LINE channel access token: ');
|
|
1143
|
+
}
|
|
1144
|
+
if (!channelSecret && process.stdin.isTTY) {
|
|
1145
|
+
channelSecret = await askText('LINE channel secret (needed for webhook replies): ');
|
|
1146
|
+
}
|
|
1147
|
+
if (!homeChannel && !allowedUsersRaw && !allowedGroupsRaw && !allowedRoomsRaw && !allowAllUsers) {
|
|
1148
|
+
if (!process.stdin.isTTY) {
|
|
1149
|
+
console.error('ต้องระบุ --home-channel <U/C/R-id> หรือ allowed list อย่างน้อยหนึ่งชุด เพื่อ fail-closed');
|
|
1150
|
+
process.exit(1);
|
|
1151
|
+
}
|
|
1152
|
+
homeChannel = await askText('LINE home channel ID (U user / C group / R room): ');
|
|
1153
|
+
}
|
|
1154
|
+
const home = homeChannel?.trim();
|
|
1155
|
+
const allowedUsers = parseStringCsv(allowedUsersRaw);
|
|
1156
|
+
const allowedGroups = parseStringCsv(allowedGroupsRaw);
|
|
1157
|
+
const allowedRooms = parseStringCsv(allowedRoomsRaw);
|
|
1158
|
+
if (!allowAllUsers && !home && !allowedUsers.length && !allowedGroups.length && !allowedRooms.length) {
|
|
1159
|
+
console.error('LINE setup ต้องมี home channel/allowlist อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
1160
|
+
process.exit(1);
|
|
1161
|
+
}
|
|
1162
|
+
if (!channelAccessToken.trim()) {
|
|
1163
|
+
console.error('LINE setup ต้องมี channel access token');
|
|
1164
|
+
process.exit(1);
|
|
1165
|
+
}
|
|
1166
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1167
|
+
await patchGatewayConfig({
|
|
1168
|
+
line: {
|
|
1169
|
+
enabled: true,
|
|
1170
|
+
channelAccessToken: channelAccessToken.trim(),
|
|
1171
|
+
channelSecret: channelSecret?.trim() || undefined,
|
|
1172
|
+
homeChannel: home || allowedUsers[0] || allowedGroups[0] || allowedRooms[0],
|
|
1173
|
+
allowedUsers,
|
|
1174
|
+
allowedGroups,
|
|
1175
|
+
allowedRooms,
|
|
1176
|
+
allowAllUsers,
|
|
1177
|
+
publicUrl: publicUrl?.trim() || undefined,
|
|
1178
|
+
},
|
|
1179
|
+
});
|
|
1180
|
+
console.log(`บันทึก LINE gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1181
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to line "hello"`);
|
|
1182
|
+
}
|
|
1183
|
+
async function runSmsGatewaySetup(args) {
|
|
1184
|
+
let accountSid = argValue(args, '--account-sid', '--sid');
|
|
1185
|
+
let authToken = argValue(args, '--auth-token', '--token');
|
|
1186
|
+
let phoneNumber = argValue(args, '--phone-number', '--from');
|
|
1187
|
+
let homeChannel = argValue(args, '--home-channel', '--to');
|
|
1188
|
+
let allowedRaw = argValue(args, '--allowed-users', '--allowed-numbers');
|
|
1189
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
1190
|
+
const webhookUrl = argValue(args, '--webhook-url');
|
|
1191
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1192
|
+
const insecureNoSignature = args.includes('--insecure-no-signature');
|
|
1193
|
+
if (!accountSid) {
|
|
1194
|
+
if (!process.stdin.isTTY) {
|
|
1195
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup sms --account-sid <AC...> --auth-token <token> --phone-number <+1555...> --home-channel <+1555...> --webhook-url <https://.../sms/webhook>`);
|
|
1196
|
+
process.exit(1);
|
|
1197
|
+
}
|
|
1198
|
+
console.log(`${BRAND.productName} SMS/Twilio setup`);
|
|
1199
|
+
console.log('ใช้ Twilio Programmable Messaging; inbound webhook ต้องตั้ง URL เดียวกันใน Twilio Console');
|
|
1200
|
+
accountSid = await askText('Twilio Account SID: ');
|
|
1201
|
+
}
|
|
1202
|
+
if (!authToken) {
|
|
1203
|
+
if (!process.stdin.isTTY) {
|
|
1204
|
+
console.error('ต้องระบุ --auth-token <token>');
|
|
1205
|
+
process.exit(1);
|
|
1206
|
+
}
|
|
1207
|
+
authToken = await askText('Twilio Auth Token: ');
|
|
1208
|
+
}
|
|
1209
|
+
if (!phoneNumber) {
|
|
1210
|
+
if (!process.stdin.isTTY) {
|
|
1211
|
+
console.error('ต้องระบุ --phone-number <E.164 Twilio number>');
|
|
1212
|
+
process.exit(1);
|
|
1213
|
+
}
|
|
1214
|
+
phoneNumber = await askText('Twilio phone number (+1555...): ');
|
|
1215
|
+
}
|
|
1216
|
+
if (!homeChannel && !allowedRaw && !allowAllUsers) {
|
|
1217
|
+
if (!process.stdin.isTTY) {
|
|
1218
|
+
console.error('ต้องระบุ --home-channel <phone> หรือ --allowed-users <phone[,phone]> เพื่อ fail-closed');
|
|
1219
|
+
process.exit(1);
|
|
1220
|
+
}
|
|
1221
|
+
homeChannel = await askText('Home/allowed phone number (+1555...): ');
|
|
1222
|
+
}
|
|
1223
|
+
const { normalizeSmsPhone } = await import('./gateway/sms.js');
|
|
1224
|
+
const from = normalizeSmsPhone(phoneNumber);
|
|
1225
|
+
const home = normalizeSmsPhone(homeChannel);
|
|
1226
|
+
const allowedUsers = parseStringCsv(allowedRaw ?? home).map((phone) => normalizeSmsPhone(phone)).filter((phone) => Boolean(phone));
|
|
1227
|
+
if (!accountSid.trim() || !authToken.trim() || !from) {
|
|
1228
|
+
console.error('SMS setup ต้องมี account sid, auth token และ Twilio phone number');
|
|
1229
|
+
process.exit(1);
|
|
1230
|
+
}
|
|
1231
|
+
if (!allowAllUsers && !home && !allowedUsers.length) {
|
|
1232
|
+
console.error('SMS setup ต้องมี home channel/allowlist อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
1233
|
+
process.exit(1);
|
|
1234
|
+
}
|
|
1235
|
+
if (!webhookUrl && !insecureNoSignature) {
|
|
1236
|
+
if (!process.stdin.isTTY) {
|
|
1237
|
+
console.error('ต้องระบุ --webhook-url <https://.../sms/webhook> เพื่อ verify Twilio signature หรือ --insecure-no-signature สำหรับ local dev');
|
|
1238
|
+
process.exit(1);
|
|
1239
|
+
}
|
|
1240
|
+
console.log('ยังไม่ได้ตั้ง webhook URL; inbound SMS จะไม่เริ่มจนกว่าจะตั้ง SMS_WEBHOOK_URL หรือรัน setup ใหม่พร้อม --webhook-url');
|
|
1241
|
+
}
|
|
1242
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1243
|
+
await patchGatewayConfig({
|
|
1244
|
+
sms: {
|
|
1245
|
+
enabled: true,
|
|
1246
|
+
accountSid: accountSid.trim(),
|
|
1247
|
+
authToken: authToken.trim(),
|
|
1248
|
+
phoneNumber: from,
|
|
1249
|
+
homeChannel: home || allowedUsers[0],
|
|
1250
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
1251
|
+
allowedUsers,
|
|
1252
|
+
allowAllUsers,
|
|
1253
|
+
webhookUrl: webhookUrl?.trim() || undefined,
|
|
1254
|
+
insecureNoSignature,
|
|
1255
|
+
},
|
|
1256
|
+
});
|
|
1257
|
+
console.log(`บันทึก SMS/Twilio gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1258
|
+
console.log(`ตั้ง Twilio webhook เป็น: ${webhookUrl?.trim() || `http://127.0.0.1:<port>/sms/webhook`}`);
|
|
1259
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to sms "hello"`);
|
|
1260
|
+
}
|
|
1261
|
+
async function runSignalGatewaySetup(args) {
|
|
1262
|
+
const httpUrl = argValue(args, '--http-url', '--url')?.trim() || 'http://127.0.0.1:8080';
|
|
1263
|
+
let account = argValue(args, '--account', '--phone-number');
|
|
1264
|
+
let homeChannel = argValue(args, '--home-channel', '--to');
|
|
1265
|
+
let allowedRaw = argValue(args, '--allowed-users', '--allowed-numbers');
|
|
1266
|
+
let groupAllowedRaw = argValue(args, '--group-allowed-users', '--allowed-groups');
|
|
1267
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
1268
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1269
|
+
const requireMention = args.includes('--require-mention');
|
|
1270
|
+
if (!account) {
|
|
1271
|
+
if (!process.stdin.isTTY) {
|
|
1272
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup signal --account <+1555...> --home-channel <+1555...> [--http-url http://127.0.0.1:8080]`);
|
|
1273
|
+
process.exit(1);
|
|
1274
|
+
}
|
|
1275
|
+
console.log(`${BRAND.productName} Signal setup`);
|
|
1276
|
+
console.log('ต้องมี signal-cli daemon --http รันอยู่; Sanook ใช้ JSON-RPC /api/v1/rpc และ SSE /api/v1/events');
|
|
1277
|
+
account = await askText('Signal account (+E.164): ');
|
|
1278
|
+
}
|
|
1279
|
+
if (!homeChannel && !allowedRaw && !groupAllowedRaw && !allowAllUsers) {
|
|
1280
|
+
if (process.stdin.isTTY) {
|
|
1281
|
+
homeChannel = await askText('Signal home/allowed user (+E.164 หรือ UUID; blank = account/Note to Self): ');
|
|
1282
|
+
}
|
|
1283
|
+
if (!homeChannel)
|
|
1284
|
+
homeChannel = account;
|
|
1285
|
+
}
|
|
1286
|
+
if (!allowedRaw && !allowAllUsers && homeChannel && !homeChannel.trim().toLowerCase().startsWith('group:')) {
|
|
1287
|
+
allowedRaw = homeChannel;
|
|
1288
|
+
}
|
|
1289
|
+
if (!groupAllowedRaw && homeChannel?.trim().toLowerCase().startsWith('group:')) {
|
|
1290
|
+
groupAllowedRaw = homeChannel;
|
|
1291
|
+
}
|
|
1292
|
+
const { normalizeSignalId } = await import('./gateway/signal.js');
|
|
1293
|
+
const cleanAccount = normalizeSignalId(account);
|
|
1294
|
+
const cleanHome = normalizeSignalId(homeChannel);
|
|
1295
|
+
const allowedUsers = parseStringCsv(allowedRaw).map(normalizeSignalId).filter((id) => Boolean(id));
|
|
1296
|
+
const groupAllowedUsers = parseStringCsv(groupAllowedRaw)
|
|
1297
|
+
.map((id) => {
|
|
1298
|
+
if (id.trim() === '*')
|
|
1299
|
+
return '*';
|
|
1300
|
+
const normalized = normalizeSignalId(id);
|
|
1301
|
+
return normalized?.startsWith('group:') ? normalized : normalized ? `group:${normalized}` : undefined;
|
|
1302
|
+
})
|
|
1303
|
+
.filter((id) => Boolean(id));
|
|
1304
|
+
if (!cleanAccount) {
|
|
1305
|
+
console.error('Signal setup ต้องมี account (+E.164 หรือ account id)');
|
|
1306
|
+
process.exit(1);
|
|
1307
|
+
}
|
|
1308
|
+
if (!allowAllUsers && !cleanHome && !allowedUsers.length && !groupAllowedUsers.length) {
|
|
1309
|
+
console.error('Signal setup ต้องมี home channel/allowlist อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
1310
|
+
process.exit(1);
|
|
1311
|
+
}
|
|
1312
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1313
|
+
await patchGatewayConfig({
|
|
1314
|
+
signal: {
|
|
1315
|
+
enabled: true,
|
|
1316
|
+
httpUrl,
|
|
1317
|
+
account: cleanAccount,
|
|
1318
|
+
homeChannel: cleanHome || allowedUsers[0] || groupAllowedUsers[0] || cleanAccount,
|
|
1319
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
1320
|
+
allowedUsers,
|
|
1321
|
+
groupAllowedUsers,
|
|
1322
|
+
allowAllUsers,
|
|
1323
|
+
requireMention,
|
|
1324
|
+
},
|
|
1325
|
+
});
|
|
1326
|
+
console.log(`บันทึก Signal gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1327
|
+
console.log(`ตรวจ signal-cli daemon: ${httpUrl}/api/v1/check`);
|
|
1328
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to signal "hello"`);
|
|
1329
|
+
}
|
|
1330
|
+
async function runWhatsAppGatewaySetup(args) {
|
|
1331
|
+
let phoneNumberId = argValue(args, '--phone-number-id', '--phone-id');
|
|
1332
|
+
let accessToken = argValue(args, '--access-token', '--token');
|
|
1333
|
+
let appSecret = argValue(args, '--app-secret', '--secret');
|
|
1334
|
+
let verifyToken = argValue(args, '--verify-token');
|
|
1335
|
+
let homeChannel = argValue(args, '--home-channel', '--to');
|
|
1336
|
+
let allowedRaw = argValue(args, '--allowed-users', '--allowed-numbers');
|
|
1337
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
1338
|
+
const publicUrl = argValue(args, '--public-url');
|
|
1339
|
+
const apiVersion = argValue(args, '--api-version');
|
|
1340
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1341
|
+
if (!phoneNumberId) {
|
|
1342
|
+
if (!process.stdin.isTTY) {
|
|
1343
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup whatsapp --phone-number-id <id> --access-token <EAA...> --app-secret <secret> --home-channel <wa_id>`);
|
|
1344
|
+
process.exit(1);
|
|
1345
|
+
}
|
|
1346
|
+
console.log(`${BRAND.productName} WhatsApp Cloud setup`);
|
|
1347
|
+
console.log('ใช้ Meta WhatsApp Business Cloud API: ต้องมี Phone Number ID, Access Token, App Secret และ public HTTPS webhook URL');
|
|
1348
|
+
phoneNumberId = await askText('WhatsApp Phone Number ID (ตัวเลขจาก Meta API Setup ไม่ใช่เบอร์โทร): ');
|
|
1349
|
+
}
|
|
1350
|
+
if (!accessToken) {
|
|
1351
|
+
if (!process.stdin.isTTY) {
|
|
1352
|
+
console.error('ต้องระบุ --access-token <Meta WhatsApp Cloud token>');
|
|
1353
|
+
process.exit(1);
|
|
1354
|
+
}
|
|
1355
|
+
accessToken = await askText('WhatsApp Cloud access token: ');
|
|
1356
|
+
}
|
|
1357
|
+
if (!appSecret) {
|
|
1358
|
+
if (!process.stdin.isTTY) {
|
|
1359
|
+
console.error('ต้องระบุ --app-secret <Meta app secret> เพื่อ verify X-Hub-Signature-256');
|
|
1360
|
+
process.exit(1);
|
|
1361
|
+
}
|
|
1362
|
+
appSecret = await askText('Meta app secret (Settings > Basic): ');
|
|
1363
|
+
}
|
|
1364
|
+
if (!homeChannel && !allowedRaw && !allowAllUsers) {
|
|
1365
|
+
if (!process.stdin.isTTY) {
|
|
1366
|
+
console.error('ต้องระบุ --home-channel <wa_id> หรือ --allowed-users <wa_id[,wa_id]> เพื่อ fail-closed');
|
|
1367
|
+
process.exit(1);
|
|
1368
|
+
}
|
|
1369
|
+
homeChannel = await askText('WhatsApp home/allowed wa_id (country code, no +): ');
|
|
1370
|
+
}
|
|
1371
|
+
const { randomBytes } = await import('node:crypto');
|
|
1372
|
+
const { normalizeWhatsAppId } = await import('./gateway/whatsapp.js');
|
|
1373
|
+
const cleanPhoneNumberId = phoneNumberId.trim();
|
|
1374
|
+
const cleanHome = normalizeWhatsAppId(homeChannel);
|
|
1375
|
+
const allowedUsers = parseStringCsv(allowedRaw ?? cleanHome).map(normalizeWhatsAppId).filter((id) => Boolean(id));
|
|
1376
|
+
if (!verifyToken)
|
|
1377
|
+
verifyToken = randomBytes(24).toString('base64url');
|
|
1378
|
+
if (!cleanPhoneNumberId || !accessToken.trim() || !appSecret.trim()) {
|
|
1379
|
+
console.error('WhatsApp setup ต้องมี phone number id, access token และ app secret');
|
|
1380
|
+
process.exit(1);
|
|
1381
|
+
}
|
|
1382
|
+
if (!allowAllUsers && !cleanHome && !allowedUsers.length) {
|
|
1383
|
+
console.error('WhatsApp setup ต้องมี home channel/allowlist อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
1384
|
+
process.exit(1);
|
|
1385
|
+
}
|
|
1386
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1387
|
+
await patchGatewayConfig({
|
|
1388
|
+
whatsapp: {
|
|
1389
|
+
enabled: true,
|
|
1390
|
+
phoneNumberId: cleanPhoneNumberId,
|
|
1391
|
+
accessToken: accessToken.trim(),
|
|
1392
|
+
appSecret: appSecret.trim(),
|
|
1393
|
+
verifyToken: verifyToken.trim(),
|
|
1394
|
+
homeChannel: cleanHome || allowedUsers[0],
|
|
1395
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
1396
|
+
allowedUsers,
|
|
1397
|
+
allowAllUsers,
|
|
1398
|
+
publicUrl: publicUrl?.trim() || undefined,
|
|
1399
|
+
apiVersion: apiVersion?.trim() || undefined,
|
|
1400
|
+
},
|
|
1401
|
+
});
|
|
1402
|
+
const callback = publicUrl?.trim() ? `${publicUrl.trim().replace(/\/+$/, '')}/whatsapp/webhook` : `https://<your-tunnel>/whatsapp/webhook`;
|
|
1403
|
+
console.log(`บันทึก WhatsApp Cloud gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1404
|
+
console.log(`Meta webhook callback URL: ${callback}`);
|
|
1405
|
+
console.log(`Meta verify token: ${verifyToken.trim()}`);
|
|
1406
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to whatsapp "hello"`);
|
|
1407
|
+
}
|
|
1408
|
+
async function runMatrixGatewaySetup(args) {
|
|
1409
|
+
let homeserver = argValue(args, '--homeserver', '--server', '--url');
|
|
1410
|
+
let accessToken = argValue(args, '--access-token', '--token');
|
|
1411
|
+
let userId = argValue(args, '--user-id', '--user');
|
|
1412
|
+
let password = argValue(args, '--password');
|
|
1413
|
+
let homeRoom = argValue(args, '--home-room', '--room', '--to');
|
|
1414
|
+
let allowedUsersRaw = argValue(args, '--allowed-users');
|
|
1415
|
+
let allowedRoomsRaw = argValue(args, '--allowed-rooms');
|
|
1416
|
+
let freeResponseRoomsRaw = argValue(args, '--free-response-rooms');
|
|
1417
|
+
const homeRoomName = argValue(args, '--home-room-name');
|
|
1418
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1419
|
+
const requireMention = !args.includes('--no-require-mention');
|
|
1420
|
+
const groupSessionsPerUser = !args.includes('--shared-room-session');
|
|
1421
|
+
const autoJoin = !args.includes('--no-auto-join');
|
|
1422
|
+
const pollTimeoutMs = argValue(args, '--poll-timeout-ms');
|
|
1423
|
+
if (!homeserver) {
|
|
1424
|
+
if (!process.stdin.isTTY) {
|
|
1425
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup matrix --homeserver <https://matrix.org> --access-token <token> --allowed-users <@you:server> [--home-room '!room:server']`);
|
|
1426
|
+
process.exit(1);
|
|
1427
|
+
}
|
|
1428
|
+
console.log(`${BRAND.productName} Matrix setup`);
|
|
1429
|
+
console.log('ใช้ Matrix Client-Server API: ต้องมี homeserver URL และ access token หรือ user/password ของ bot account');
|
|
1430
|
+
homeserver = await askText('Matrix homeserver URL (เช่น https://matrix.org): ');
|
|
1431
|
+
}
|
|
1432
|
+
if (!accessToken && (!userId || !password)) {
|
|
1433
|
+
if (!process.stdin.isTTY) {
|
|
1434
|
+
console.error('ต้องระบุ --access-token <token> หรือ --user-id <@bot:server> --password <password>');
|
|
1435
|
+
process.exit(1);
|
|
1436
|
+
}
|
|
1437
|
+
accessToken = await askText('Matrix access token (แนะนำ; blank = ใช้ user/password): ');
|
|
1438
|
+
if (!accessToken) {
|
|
1439
|
+
userId = await askText('Matrix bot user id (@bot:server): ');
|
|
1440
|
+
password = await askText('Matrix bot password: ');
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (!allowedUsersRaw && !allowAllUsers) {
|
|
1444
|
+
if (!process.stdin.isTTY) {
|
|
1445
|
+
console.error('ต้องระบุ --allowed-users <@user:server[,user]> เพื่อ fail-closed หรือ --allow-all-users');
|
|
1446
|
+
process.exit(1);
|
|
1447
|
+
}
|
|
1448
|
+
allowedUsersRaw = await askText('Allowed Matrix user IDs (comma-separated): ');
|
|
1449
|
+
}
|
|
1450
|
+
if (!homeRoom && process.stdin.isTTY) {
|
|
1451
|
+
homeRoom = await askText('Matrix home room id/alias (!room:server หรือ #room:server; blank = skip): ');
|
|
1452
|
+
}
|
|
1453
|
+
const { normalizeMatrixHomeserver, normalizeMatrixRoomId, normalizeMatrixUserId } = await import('./gateway/matrix.js');
|
|
1454
|
+
const cleanHomeserver = normalizeMatrixHomeserver(homeserver);
|
|
1455
|
+
const cleanUserId = normalizeMatrixUserId(userId);
|
|
1456
|
+
const cleanHomeRoom = normalizeMatrixRoomId(homeRoom);
|
|
1457
|
+
const allowedUsers = parseStringCsv(allowedUsersRaw).map(normalizeMatrixUserId).filter((id) => Boolean(id));
|
|
1458
|
+
const allowedRooms = parseStringCsv(allowedRoomsRaw).map(normalizeMatrixRoomId).filter((id) => Boolean(id));
|
|
1459
|
+
const freeResponseRooms = parseStringCsv(freeResponseRoomsRaw).map(normalizeMatrixRoomId).filter((id) => Boolean(id));
|
|
1460
|
+
const timeout = pollTimeoutMs ? Number(pollTimeoutMs) : undefined;
|
|
1461
|
+
if (!cleanHomeserver) {
|
|
1462
|
+
console.error('Matrix setup ต้องมี homeserver URL ที่ขึ้นต้นด้วย http:// หรือ https://');
|
|
1463
|
+
process.exit(1);
|
|
1464
|
+
}
|
|
1465
|
+
if (!accessToken?.trim() && (!cleanUserId || !password?.trim())) {
|
|
1466
|
+
console.error('Matrix setup ต้องมี access token หรือ user id/password');
|
|
1467
|
+
process.exit(1);
|
|
1468
|
+
}
|
|
1469
|
+
if (!allowAllUsers && !allowedUsers.length) {
|
|
1470
|
+
console.error('Matrix setup ต้องมี allowed users อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
1471
|
+
process.exit(1);
|
|
1472
|
+
}
|
|
1473
|
+
if (homeRoom?.trim() && !cleanHomeRoom) {
|
|
1474
|
+
console.error('Matrix home room ต้องเป็น room id/alias เช่น !abc123:matrix.org หรือ #room:matrix.org');
|
|
1475
|
+
process.exit(1);
|
|
1476
|
+
}
|
|
1477
|
+
if (pollTimeoutMs && (!Number.isInteger(timeout) || Number(timeout) <= 0)) {
|
|
1478
|
+
console.error('--poll-timeout-ms ต้องเป็น integer มากกว่า 0');
|
|
1479
|
+
process.exit(1);
|
|
1480
|
+
}
|
|
1481
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1482
|
+
await patchGatewayConfig({
|
|
1483
|
+
matrix: {
|
|
1484
|
+
enabled: true,
|
|
1485
|
+
homeserver: cleanHomeserver,
|
|
1486
|
+
accessToken: accessToken?.trim() || undefined,
|
|
1487
|
+
userId: cleanUserId,
|
|
1488
|
+
password: password?.trim() || undefined,
|
|
1489
|
+
homeRoom: cleanHomeRoom || allowedRooms[0],
|
|
1490
|
+
homeRoomName: homeRoomName?.trim() || undefined,
|
|
1491
|
+
allowedUsers,
|
|
1492
|
+
allowedRooms,
|
|
1493
|
+
freeResponseRooms,
|
|
1494
|
+
allowAllUsers,
|
|
1495
|
+
requireMention,
|
|
1496
|
+
groupSessionsPerUser,
|
|
1497
|
+
autoJoin,
|
|
1498
|
+
pollTimeoutMs: timeout,
|
|
1499
|
+
},
|
|
1500
|
+
});
|
|
1501
|
+
console.log(`บันทึก Matrix gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1502
|
+
console.log(`Matrix sync: ${cleanHomeserver}/_matrix/client/v3/sync`);
|
|
1503
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to matrix "hello"${cleanHomeRoom ? '' : ` หรือ ${BRAND.cliName} send --to matrix:!room:server "hello"`}`);
|
|
1504
|
+
}
|
|
1505
|
+
async function runGoogleChatGatewaySetup(args) {
|
|
1506
|
+
const projectId = argValue(args, '--project-id');
|
|
1507
|
+
const subscriptionName = argValue(args, '--subscription-name', '--subscription');
|
|
1508
|
+
let serviceAccountJson = argValue(args, '--service-account-json', '--service-account', '--credentials');
|
|
1509
|
+
const apiBaseUrl = argValue(args, '--api-base-url', '--base-url');
|
|
1510
|
+
let incomingWebhookUrl = argValue(args, '--incoming-webhook-url', '--webhook-url', '--url');
|
|
1511
|
+
let homeChannel = argValue(args, '--home-channel', '--space', '--to');
|
|
1512
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
1513
|
+
const allowedUsersRaw = argValue(args, '--allowed-users');
|
|
1514
|
+
const allowedSpacesRaw = argValue(args, '--allowed-spaces', '--spaces');
|
|
1515
|
+
const freeResponseSpacesRaw = argValue(args, '--free-response-spaces');
|
|
1516
|
+
const maxMessagesRaw = argValue(args, '--max-messages');
|
|
1517
|
+
const maxBytesRaw = argValue(args, '--max-bytes');
|
|
1518
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1519
|
+
const allowAllSpaces = args.includes('--allow-all-spaces');
|
|
1520
|
+
if ((!incomingWebhookUrl && !serviceAccountJson) || (!incomingWebhookUrl && !homeChannel && !allowedSpacesRaw && !allowAllSpaces)) {
|
|
1521
|
+
if (!process.stdin.isTTY) {
|
|
1522
|
+
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?...>`);
|
|
1523
|
+
process.exit(1);
|
|
1524
|
+
}
|
|
1525
|
+
console.log(`${BRAND.productName} Google Chat setup`);
|
|
1526
|
+
console.log('ใช้ Service Account JSON + Chat REST API สำหรับ bot app หรือ incoming webhook URL สำหรับส่งง่าย ๆ');
|
|
1527
|
+
serviceAccountJson ||= await askText('Service Account JSON path (blank = webhook mode): ');
|
|
1528
|
+
if (serviceAccountJson) {
|
|
1529
|
+
homeChannel ||= await askText('Home space (spaces/AAA...; blank = skip): ');
|
|
1530
|
+
}
|
|
1531
|
+
else {
|
|
1532
|
+
incomingWebhookUrl ||= await askText('Google Chat incoming webhook URL: ');
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
const { normalizeGoogleChatApiBaseUrl, normalizeGoogleChatWebhookUrl, parseGoogleChatTarget } = await import('./gateway/googlechat.js');
|
|
1536
|
+
const cleanApiBaseUrl = normalizeGoogleChatApiBaseUrl(apiBaseUrl);
|
|
1537
|
+
const cleanWebhookUrl = normalizeGoogleChatWebhookUrl(incomingWebhookUrl);
|
|
1538
|
+
const cleanServiceAccountJson = serviceAccountJson?.trim();
|
|
1539
|
+
const cleanHomeChannel = homeChannel?.trim();
|
|
1540
|
+
const maxMessages = maxMessagesRaw ? Number(maxMessagesRaw) : undefined;
|
|
1541
|
+
const maxBytes = maxBytesRaw ? Number(maxBytesRaw) : undefined;
|
|
1542
|
+
if (!cleanApiBaseUrl) {
|
|
1543
|
+
console.error('Google Chat API base URL ต้องเป็น https:// URL');
|
|
1544
|
+
process.exit(1);
|
|
1545
|
+
}
|
|
1546
|
+
if (incomingWebhookUrl?.trim() && !cleanWebhookUrl) {
|
|
1547
|
+
console.error('Google Chat incoming webhook URL ต้องเป็น https:// URL');
|
|
1548
|
+
process.exit(1);
|
|
1549
|
+
}
|
|
1550
|
+
if (!cleanWebhookUrl && !cleanServiceAccountJson) {
|
|
1551
|
+
console.error('Google Chat setup ต้องมี service account JSON หรือ incoming webhook URL');
|
|
1552
|
+
process.exit(1);
|
|
1553
|
+
}
|
|
1554
|
+
if (cleanHomeChannel) {
|
|
1555
|
+
try {
|
|
1556
|
+
parseGoogleChatTarget({
|
|
1557
|
+
apiBaseUrl: cleanApiBaseUrl,
|
|
1558
|
+
homeChannel: cleanHomeChannel,
|
|
1559
|
+
allowedUsers: [],
|
|
1560
|
+
allowedSpaces: [],
|
|
1561
|
+
freeResponseSpaces: [],
|
|
1562
|
+
allowAllUsers: false,
|
|
1563
|
+
allowAllSpaces: false,
|
|
1564
|
+
maxMessages: 1,
|
|
1565
|
+
maxBytes: 16_777_216,
|
|
1566
|
+
enabled: true,
|
|
1567
|
+
source: 'config',
|
|
1568
|
+
serviceAccountJson: cleanServiceAccountJson,
|
|
1569
|
+
incomingWebhookUrl: cleanWebhookUrl,
|
|
1570
|
+
}, cleanHomeChannel);
|
|
1571
|
+
}
|
|
1572
|
+
catch (e) {
|
|
1573
|
+
console.error(e instanceof Error ? e.message : 'Google Chat home channel ไม่ถูกต้อง');
|
|
1574
|
+
process.exit(1);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
if (!cleanWebhookUrl && !cleanHomeChannel && !allowedSpacesRaw?.trim() && !allowAllSpaces) {
|
|
1578
|
+
console.error('Google Chat service-account setup ต้องมี home channel, allowed spaces หรือ --allow-all-spaces');
|
|
1579
|
+
process.exit(1);
|
|
1580
|
+
}
|
|
1581
|
+
if (maxMessagesRaw && (!Number.isInteger(maxMessages) || Number(maxMessages) <= 0)) {
|
|
1582
|
+
console.error('--max-messages ต้องเป็น integer มากกว่า 0');
|
|
1583
|
+
process.exit(1);
|
|
1584
|
+
}
|
|
1585
|
+
if (maxBytesRaw && (!Number.isInteger(maxBytes) || Number(maxBytes) <= 0)) {
|
|
1586
|
+
console.error('--max-bytes ต้องเป็น integer มากกว่า 0');
|
|
1587
|
+
process.exit(1);
|
|
1588
|
+
}
|
|
1589
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1590
|
+
await patchGatewayConfig({
|
|
1591
|
+
googleChat: {
|
|
1592
|
+
enabled: true,
|
|
1593
|
+
projectId: projectId?.trim() || undefined,
|
|
1594
|
+
subscriptionName: subscriptionName?.trim() || undefined,
|
|
1595
|
+
serviceAccountJson: cleanServiceAccountJson || undefined,
|
|
1596
|
+
apiBaseUrl: cleanApiBaseUrl,
|
|
1597
|
+
incomingWebhookUrl: cleanWebhookUrl,
|
|
1598
|
+
homeChannel: cleanHomeChannel || (cleanWebhookUrl ? 'webhook' : undefined),
|
|
1599
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
1600
|
+
allowedUsers: parseStringCsv(allowedUsersRaw),
|
|
1601
|
+
allowedSpaces: parseStringCsv(allowedSpacesRaw),
|
|
1602
|
+
freeResponseSpaces: parseStringCsv(freeResponseSpacesRaw),
|
|
1603
|
+
allowAllUsers,
|
|
1604
|
+
allowAllSpaces,
|
|
1605
|
+
maxMessages,
|
|
1606
|
+
maxBytes,
|
|
1607
|
+
},
|
|
1608
|
+
});
|
|
1609
|
+
console.log(`บันทึก Google Chat gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1610
|
+
console.log(cleanWebhookUrl ? 'Google Chat delivery mode: incoming webhook' : 'Google Chat delivery mode: Chat REST API');
|
|
1611
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to googlechat "hello"${cleanHomeChannel ? '' : ` หรือ ${BRAND.cliName} send --to googlechat:spaces/<space> "hello"`}`);
|
|
1612
|
+
}
|
|
1613
|
+
async function runBlueBubblesGatewaySetup(args) {
|
|
1614
|
+
let serverUrl = argValue(args, '--server-url', '--url');
|
|
1615
|
+
let password = argValue(args, '--password', '--token', '--guid');
|
|
1616
|
+
const webhookHost = argValue(args, '--webhook-host');
|
|
1617
|
+
const webhookPortRaw = argValue(args, '--webhook-port');
|
|
1618
|
+
const webhookPath = argValue(args, '--webhook-path');
|
|
1619
|
+
let homeChannel = argValue(args, '--home-channel', '--chat-guid', '--to');
|
|
1620
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
1621
|
+
let allowedUsersRaw = argValue(args, '--allowed-users', '--allowed-targets');
|
|
1622
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1623
|
+
const requireMention = args.includes('--require-mention');
|
|
1624
|
+
const mentionPatternsRaw = argValue(args, '--mention-patterns');
|
|
1625
|
+
const sendReadReceipts = !args.includes('--no-read-receipts');
|
|
1626
|
+
if (!serverUrl || !password || (!homeChannel && !allowedUsersRaw && !allowAllUsers)) {
|
|
1627
|
+
if (!process.stdin.isTTY) {
|
|
1628
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup bluebubbles --server-url <http://mac:1234> --password <server-password> --home-channel <chat-guid|email|phone>`);
|
|
1629
|
+
process.exit(1);
|
|
1630
|
+
}
|
|
1631
|
+
console.log(`${BRAND.productName} BlueBubbles/iMessage setup`);
|
|
1632
|
+
console.log('ต้องมี BlueBubbles Server URL + server password; outbound ใช้ REST API /api/v1/message/text');
|
|
1633
|
+
serverUrl ||= await askText('BlueBubbles server URL (เช่น http://localhost:1234): ');
|
|
1634
|
+
password ||= await askText('BlueBubbles server password: ');
|
|
1635
|
+
homeChannel ||= await askText('Home chat GUID/email/phone (blank = explicit targets only): ');
|
|
1636
|
+
}
|
|
1637
|
+
if (!allowedUsersRaw && homeChannel && !allowAllUsers)
|
|
1638
|
+
allowedUsersRaw = homeChannel;
|
|
1639
|
+
const { normalizeBlueBubblesServerUrl, normalizeBlueBubblesWebhookPath } = await import('./gateway/bluebubbles.js');
|
|
1640
|
+
const cleanServerUrl = normalizeBlueBubblesServerUrl(serverUrl);
|
|
1641
|
+
const cleanPassword = password?.trim();
|
|
1642
|
+
const cleanHomeChannel = homeChannel?.trim();
|
|
1643
|
+
const webhookPort = webhookPortRaw ? Number(webhookPortRaw) : undefined;
|
|
1644
|
+
if (!cleanServerUrl) {
|
|
1645
|
+
console.error('BlueBubbles server URL ต้องเป็น http:// หรือ https:// URL');
|
|
1646
|
+
process.exit(1);
|
|
1647
|
+
}
|
|
1648
|
+
if (!cleanPassword) {
|
|
1649
|
+
console.error('BlueBubbles setup ต้องมี server password');
|
|
1650
|
+
process.exit(1);
|
|
1651
|
+
}
|
|
1652
|
+
if (webhookPortRaw && (!Number.isInteger(webhookPort) || Number(webhookPort) <= 0 || Number(webhookPort) > 65535)) {
|
|
1653
|
+
console.error('--webhook-port ต้องเป็น port 1-65535');
|
|
1654
|
+
process.exit(1);
|
|
1655
|
+
}
|
|
1656
|
+
const allowedUsers = parseStringCsv(allowedUsersRaw);
|
|
1657
|
+
if (!allowAllUsers && !cleanHomeChannel && !allowedUsers.length) {
|
|
1658
|
+
console.error('BlueBubbles setup ต้องมี home channel/allowlist อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
1659
|
+
process.exit(1);
|
|
1660
|
+
}
|
|
1661
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1662
|
+
await patchGatewayConfig({
|
|
1663
|
+
bluebubbles: {
|
|
1664
|
+
enabled: true,
|
|
1665
|
+
serverUrl: cleanServerUrl,
|
|
1666
|
+
password: cleanPassword,
|
|
1667
|
+
webhookHost: webhookHost?.trim() || undefined,
|
|
1668
|
+
webhookPort,
|
|
1669
|
+
webhookPath: normalizeBlueBubblesWebhookPath(webhookPath),
|
|
1670
|
+
homeChannel: cleanHomeChannel || allowedUsers[0],
|
|
1671
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
1672
|
+
allowedUsers,
|
|
1673
|
+
allowAllUsers,
|
|
1674
|
+
requireMention,
|
|
1675
|
+
mentionPatterns: parseStringCsv(mentionPatternsRaw),
|
|
1676
|
+
sendReadReceipts,
|
|
1677
|
+
},
|
|
1678
|
+
});
|
|
1679
|
+
console.log(`บันทึก BlueBubbles gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1680
|
+
console.log(`BlueBubbles REST: ${cleanServerUrl}/api/v1/message/text`);
|
|
1681
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to bluebubbles "hello"${cleanHomeChannel ? '' : ` หรือ ${BRAND.cliName} send --to bluebubbles:<chat-guid|email|phone> "hello"`}`);
|
|
1682
|
+
}
|
|
1683
|
+
async function runTeamsGatewaySetup(args) {
|
|
1684
|
+
let incomingWebhookUrl = argValue(args, '--incoming-webhook-url', '--webhook-url', '--url');
|
|
1685
|
+
const graphAccessToken = argValue(args, '--graph-access-token', '--access-token', '--token');
|
|
1686
|
+
const teamId = argValue(args, '--team-id');
|
|
1687
|
+
const channelId = argValue(args, '--channel-id');
|
|
1688
|
+
const chatId = argValue(args, '--chat-id');
|
|
1689
|
+
let homeChannel = argValue(args, '--home-channel', '--to');
|
|
1690
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
1691
|
+
const clientId = argValue(args, '--client-id');
|
|
1692
|
+
const clientSecret = argValue(args, '--client-secret');
|
|
1693
|
+
const tenantId = argValue(args, '--tenant-id');
|
|
1694
|
+
const allowedUsersRaw = argValue(args, '--allowed-users');
|
|
1695
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1696
|
+
const portRaw = argValue(args, '--port');
|
|
1697
|
+
const rawMode = argValue(args, '--delivery-mode', '--mode');
|
|
1698
|
+
const deliveryMode = rawMode === 'graph' || (!rawMode && graphAccessToken) ? 'graph' : 'incoming_webhook';
|
|
1699
|
+
if (deliveryMode === 'incoming_webhook' && !incomingWebhookUrl) {
|
|
1700
|
+
if (!process.stdin.isTTY) {
|
|
1701
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup teams --incoming-webhook-url <https://...>`);
|
|
1702
|
+
process.exit(1);
|
|
1703
|
+
}
|
|
1704
|
+
console.log(`${BRAND.productName} Microsoft Teams setup`);
|
|
1705
|
+
console.log('โหมดง่าย: สร้าง Incoming Webhook ใน Teams channel แล้ววาง URL ที่นี่');
|
|
1706
|
+
incomingWebhookUrl = await askText('Teams incoming webhook URL: ');
|
|
1707
|
+
homeChannel ||= (await askText('Teams home target label (blank = webhook): ')) || 'webhook';
|
|
1708
|
+
}
|
|
1709
|
+
if (deliveryMode === 'graph' && (!graphAccessToken || (!chatId && !homeChannel && (!teamId || !channelId)))) {
|
|
1710
|
+
if (!process.stdin.isTTY) {
|
|
1711
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup teams --delivery-mode graph --graph-access-token <token> (--chat-id <id> หรือ --team-id <id> --channel-id <id>)`);
|
|
1712
|
+
process.exit(1);
|
|
1713
|
+
}
|
|
1714
|
+
console.log(`${BRAND.productName} Microsoft Teams Graph setup`);
|
|
1715
|
+
console.log('ต้องมี Microsoft Graph token และ chat id หรือ team/channel id สำหรับ proactive delivery');
|
|
1716
|
+
}
|
|
1717
|
+
const { normalizeTeamsWebhookUrl } = await import('./gateway/teams.js');
|
|
1718
|
+
const cleanWebhookUrl = normalizeTeamsWebhookUrl(incomingWebhookUrl);
|
|
1719
|
+
const cleanPort = portRaw ? Number(portRaw) : undefined;
|
|
1720
|
+
if (deliveryMode === 'incoming_webhook' && !cleanWebhookUrl) {
|
|
1721
|
+
console.error('Microsoft Teams incoming webhook URL ต้องเป็น https:// URL');
|
|
1722
|
+
process.exit(1);
|
|
1723
|
+
}
|
|
1724
|
+
if (deliveryMode === 'graph' && !graphAccessToken?.trim()) {
|
|
1725
|
+
console.error('Microsoft Teams Graph mode ต้องมี --graph-access-token');
|
|
1726
|
+
process.exit(1);
|
|
1727
|
+
}
|
|
1728
|
+
if (deliveryMode === 'graph' && !chatId?.trim() && !homeChannel?.trim() && (!teamId?.trim() || !channelId?.trim())) {
|
|
1729
|
+
console.error('Microsoft Teams Graph mode ต้องมี --chat-id หรือ --team-id + --channel-id');
|
|
1730
|
+
process.exit(1);
|
|
1731
|
+
}
|
|
1732
|
+
if (portRaw && (!Number.isInteger(cleanPort) || Number(cleanPort) <= 0)) {
|
|
1733
|
+
console.error('--port ต้องเป็น integer มากกว่า 0');
|
|
1734
|
+
process.exit(1);
|
|
1735
|
+
}
|
|
1736
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1737
|
+
const graphHome = homeChannel?.trim() || chatId?.trim() || (teamId?.trim() && channelId?.trim() ? `team/${teamId.trim()}/channel/${channelId.trim()}` : undefined);
|
|
1738
|
+
await patchGatewayConfig({
|
|
1739
|
+
teams: {
|
|
1740
|
+
enabled: true,
|
|
1741
|
+
deliveryMode,
|
|
1742
|
+
incomingWebhookUrl: cleanWebhookUrl,
|
|
1743
|
+
graphAccessToken: graphAccessToken?.trim() || undefined,
|
|
1744
|
+
teamId: teamId?.trim() || undefined,
|
|
1745
|
+
channelId: channelId?.trim() || undefined,
|
|
1746
|
+
chatId: chatId?.trim() || undefined,
|
|
1747
|
+
homeChannel: graphHome || (cleanWebhookUrl ? 'webhook' : undefined),
|
|
1748
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
1749
|
+
clientId: clientId?.trim() || undefined,
|
|
1750
|
+
clientSecret: clientSecret?.trim() || undefined,
|
|
1751
|
+
tenantId: tenantId?.trim() || undefined,
|
|
1752
|
+
allowedUsers: parseStringCsv(allowedUsersRaw),
|
|
1753
|
+
allowAllUsers,
|
|
1754
|
+
port: cleanPort,
|
|
1755
|
+
},
|
|
1756
|
+
});
|
|
1757
|
+
console.log(`บันทึก Microsoft Teams gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1758
|
+
console.log(`Teams delivery mode: ${deliveryMode}`);
|
|
1759
|
+
if (deliveryMode === 'incoming_webhook')
|
|
1760
|
+
console.log('ส่งผ่าน Incoming Webhook ที่ตั้งไว้');
|
|
1761
|
+
else
|
|
1762
|
+
console.log(`Graph target: ${graphHome}`);
|
|
1763
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to teams "hello"`);
|
|
1764
|
+
}
|
|
1765
|
+
async function runNtfyGatewaySetup(args) {
|
|
1766
|
+
let topic = argValue(args, '--topic');
|
|
1767
|
+
const serverUrl = argValue(args, '--server-url') ?? argValue(args, '--url');
|
|
1768
|
+
const token = argValue(args, '--token');
|
|
1769
|
+
const publishTopic = argValue(args, '--publish-topic');
|
|
1770
|
+
let homeChannel = argValue(args, '--home-channel', '--to');
|
|
1771
|
+
let allowedRaw = argValue(args, '--allowed-users', '--allowed-topics');
|
|
1772
|
+
const homeChannelName = argValue(args, '--home-channel-name');
|
|
1773
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1774
|
+
const markdown = args.includes('--markdown');
|
|
1775
|
+
if (!topic) {
|
|
1776
|
+
if (!process.stdin.isTTY) {
|
|
1777
|
+
console.error(`ใช้: ${BRAND.cliName} gateway setup ntfy --topic <topic> [--token <tk_...|user:pass>]`);
|
|
1778
|
+
process.exit(1);
|
|
1779
|
+
}
|
|
1780
|
+
console.log(`${BRAND.productName} ntfy setup`);
|
|
1781
|
+
console.log('เลือก topic ยาว/เดายาก แล้ว subscribe topic นี้ใน ntfy mobile app หรือ self-hosted ntfy');
|
|
1782
|
+
topic = await askText('ntfy topic: ');
|
|
1783
|
+
}
|
|
1784
|
+
const cleanTopic = topic.trim();
|
|
1785
|
+
if (!cleanTopic) {
|
|
1786
|
+
console.error('ntfy setup ต้องมี topic');
|
|
1787
|
+
process.exit(1);
|
|
1788
|
+
}
|
|
1789
|
+
if (!homeChannel)
|
|
1790
|
+
homeChannel = cleanTopic;
|
|
1791
|
+
if (!allowedRaw && !allowAllUsers)
|
|
1792
|
+
allowedRaw = cleanTopic;
|
|
1793
|
+
const allowedUsers = parseStringCsv(allowedRaw);
|
|
1794
|
+
if (!allowAllUsers && !allowedUsers.includes(cleanTopic) && homeChannel !== cleanTopic) {
|
|
1795
|
+
console.error('ntfy setup ต้องมี topic ใน --allowed-users หรือใช้ --allow-all-users เพื่อรับ inbound');
|
|
1796
|
+
process.exit(1);
|
|
1797
|
+
}
|
|
1798
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1799
|
+
await patchGatewayConfig({
|
|
1800
|
+
ntfy: {
|
|
1801
|
+
enabled: true,
|
|
1802
|
+
serverUrl: serverUrl?.trim() || undefined,
|
|
1803
|
+
topic: cleanTopic,
|
|
1804
|
+
publishTopic: publishTopic?.trim() || undefined,
|
|
1805
|
+
token: token?.trim() || undefined,
|
|
1806
|
+
homeChannel: homeChannel?.trim() || cleanTopic,
|
|
1807
|
+
homeChannelName: homeChannelName?.trim() || undefined,
|
|
1808
|
+
allowedUsers,
|
|
1809
|
+
allowAllUsers,
|
|
1810
|
+
markdown,
|
|
1811
|
+
},
|
|
1812
|
+
});
|
|
1813
|
+
console.log(`บันทึก ntfy gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1814
|
+
console.log(`subscribe topic ในแอป ntfy: ${cleanTopic}`);
|
|
1815
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to ntfy "hello"`);
|
|
1816
|
+
}
|
|
1817
|
+
async function runWebhookGatewaySetup(args) {
|
|
1818
|
+
let secret = argValue(args, '--secret', '--webhook-secret');
|
|
1819
|
+
const publicUrl = argValue(args, '--public-url');
|
|
1820
|
+
const rateLimitRaw = argValue(args, '--rate-limit', '--rate-limit-per-minute');
|
|
1821
|
+
const insecureNoAuth = args.includes('--insecure-no-auth');
|
|
1822
|
+
if (!secret && !insecureNoAuth) {
|
|
1823
|
+
if (process.stdin.isTTY) {
|
|
1824
|
+
const { generateWebhookSecret } = await import('./gateway/webhooks.js');
|
|
1825
|
+
console.log(`${BRAND.productName} Webhooks setup`);
|
|
1826
|
+
console.log('ตั้ง global HMAC secret สำหรับ route ที่ไม่ได้ระบุ secret เอง');
|
|
1827
|
+
secret = (await askText('Webhook global secret (blank = auto-generate): ')) || generateWebhookSecret();
|
|
1828
|
+
}
|
|
1829
|
+
else {
|
|
1830
|
+
const { generateWebhookSecret } = await import('./gateway/webhooks.js');
|
|
1831
|
+
secret = generateWebhookSecret();
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
const rateLimitPerMinute = rateLimitRaw ? parsePort(rateLimitRaw, 30, 'webhook rate limit') : undefined;
|
|
1835
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1836
|
+
await patchGatewayConfig({
|
|
1837
|
+
webhooks: {
|
|
1838
|
+
enabled: true,
|
|
1839
|
+
secret: insecureNoAuth ? 'INSECURE_NO_AUTH' : secret?.trim(),
|
|
1840
|
+
publicUrl: publicUrl?.trim() || undefined,
|
|
1841
|
+
rateLimitPerMinute,
|
|
1842
|
+
},
|
|
1843
|
+
});
|
|
1844
|
+
console.log(`บันทึก Webhooks gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1845
|
+
console.log(`เพิ่ม route ได้ด้วย: ${BRAND.cliName} webhook subscribe github-issues --events issues --prompt "New issue: {issue.title}" --to telegram`);
|
|
1846
|
+
}
|
|
1847
|
+
function parsePort(raw, fallback, label) {
|
|
1848
|
+
if (!raw)
|
|
1849
|
+
return fallback;
|
|
1850
|
+
const n = Number(raw);
|
|
1851
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
1852
|
+
console.error(`${label} ต้องเป็น port 1-65535`);
|
|
1853
|
+
process.exit(1);
|
|
1854
|
+
}
|
|
1855
|
+
return n;
|
|
1856
|
+
}
|
|
1857
|
+
async function runEmailGatewaySetup(args) {
|
|
1858
|
+
let address = argValue(args, '--address', '--email');
|
|
1859
|
+
let password = argValue(args, '--password', '--app-password');
|
|
1860
|
+
let imapHost = argValue(args, '--imap-host');
|
|
1861
|
+
let smtpHost = argValue(args, '--smtp-host');
|
|
1862
|
+
let homeAddress = argValue(args, '--home-address', '--to');
|
|
1863
|
+
let allowedRaw = argValue(args, '--allowed-users', '--allowed-senders');
|
|
1864
|
+
const imapPort = parsePort(argValue(args, '--imap-port'), 993, 'imap port');
|
|
1865
|
+
const smtpPort = parsePort(argValue(args, '--smtp-port'), 587, 'smtp port');
|
|
1866
|
+
const pollIntervalSeconds = parsePort(argValue(args, '--poll-interval'), 15, 'poll interval');
|
|
1867
|
+
const allowAllUsers = args.includes('--allow-all-users');
|
|
1868
|
+
if (!address) {
|
|
1869
|
+
if (!process.stdin.isTTY) {
|
|
1870
|
+
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`);
|
|
1871
|
+
process.exit(1);
|
|
1872
|
+
}
|
|
1873
|
+
console.log(`${BRAND.productName} Email setup`);
|
|
1874
|
+
console.log('แนะนำให้ใช้ dedicated mailbox + app password ไม่ใช่บัญชีส่วนตัวหลัก');
|
|
1875
|
+
address = await askText('Email address ของ bot: ');
|
|
1876
|
+
}
|
|
1877
|
+
if (!password) {
|
|
1878
|
+
if (!process.stdin.isTTY) {
|
|
1879
|
+
console.error('ต้องระบุ --password <app-password>');
|
|
1880
|
+
process.exit(1);
|
|
1881
|
+
}
|
|
1882
|
+
password = await askText('Email app password: ');
|
|
1883
|
+
}
|
|
1884
|
+
if (!imapHost) {
|
|
1885
|
+
if (!process.stdin.isTTY) {
|
|
1886
|
+
console.error('ต้องระบุ --imap-host <host>');
|
|
1887
|
+
process.exit(1);
|
|
1888
|
+
}
|
|
1889
|
+
imapHost = await askText('IMAP host (เช่น imap.gmail.com): ');
|
|
1890
|
+
}
|
|
1891
|
+
if (!smtpHost) {
|
|
1892
|
+
if (!process.stdin.isTTY) {
|
|
1893
|
+
console.error('ต้องระบุ --smtp-host <host>');
|
|
1894
|
+
process.exit(1);
|
|
1895
|
+
}
|
|
1896
|
+
smtpHost = await askText('SMTP host (เช่น smtp.gmail.com): ');
|
|
1897
|
+
}
|
|
1898
|
+
if (!homeAddress && !allowedRaw && !allowAllUsers) {
|
|
1899
|
+
if (!process.stdin.isTTY) {
|
|
1900
|
+
console.error('ต้องระบุ --home-address <email> หรือ --allowed-users <email[,email]> เพื่อ fail-closed');
|
|
1901
|
+
process.exit(1);
|
|
1902
|
+
}
|
|
1903
|
+
homeAddress = await askText('Home/allowed email address: ');
|
|
1904
|
+
}
|
|
1905
|
+
const allowedUsers = parseStringCsv(allowedRaw ?? homeAddress).map((s) => s.toLowerCase());
|
|
1906
|
+
if (!allowAllUsers && !allowedUsers.length) {
|
|
1907
|
+
console.error('Email setup ต้องมี allowed sender/home address อย่างน้อย 1 ค่า หรือระบุ --allow-all-users');
|
|
1908
|
+
process.exit(1);
|
|
1909
|
+
}
|
|
1910
|
+
const { patchGatewayConfig, gatewayConfigPath } = await import('./gateway/config.js');
|
|
1911
|
+
await patchGatewayConfig({
|
|
1912
|
+
email: {
|
|
1913
|
+
enabled: true,
|
|
1914
|
+
address: address.trim(),
|
|
1915
|
+
password: password.trim(),
|
|
1916
|
+
imapHost: imapHost.trim(),
|
|
1917
|
+
imapPort,
|
|
1918
|
+
smtpHost: smtpHost.trim(),
|
|
1919
|
+
smtpPort,
|
|
1920
|
+
homeAddress: homeAddress?.trim() || allowedUsers[0],
|
|
1921
|
+
allowedUsers,
|
|
1922
|
+
allowAllUsers,
|
|
1923
|
+
pollIntervalSeconds,
|
|
1924
|
+
},
|
|
1925
|
+
});
|
|
1926
|
+
console.log(`บันทึก Email gateway config แล้ว: ${gatewayConfigPath()}`);
|
|
1927
|
+
console.log(`ส่งทดสอบได้ด้วย: ${BRAND.cliName} send --to email:${homeAddress?.trim() || allowedUsers[0]} "hello"`);
|
|
1928
|
+
}
|
|
1929
|
+
async function runGateway(args) {
|
|
1930
|
+
const [action, ...rest] = args;
|
|
1931
|
+
if (!action || action === 'status' || action === 'list')
|
|
1932
|
+
return runGatewayStatus();
|
|
1933
|
+
if (action === 'doctor')
|
|
1934
|
+
return runGatewayDoctor();
|
|
1935
|
+
if (action === 'setup')
|
|
1936
|
+
return runGatewaySetup(rest);
|
|
1937
|
+
if (action === 'run') {
|
|
1938
|
+
if (!hasServeCommandRequest(['serve', ...rest])) {
|
|
1939
|
+
console.error(`ไม่รู้จัก: gateway run ${rest.join(' ')} — ใช้ gateway run [--port N] [--model spec]`);
|
|
1940
|
+
process.exit(1);
|
|
1941
|
+
}
|
|
1942
|
+
return runServe(rest);
|
|
1943
|
+
}
|
|
1944
|
+
if (action === 'start') {
|
|
1945
|
+
const { startGatewayService } = await import('./gateway/service.js');
|
|
1946
|
+
const res = await startGatewayService({ entrypoint: resolve(process.argv[1]), gatewayArgs: rest });
|
|
1947
|
+
console.log(res.started
|
|
1948
|
+
? `เริ่ม ${BRAND.cliName} gateway background แล้ว (pid ${res.state.pid})`
|
|
1949
|
+
: `${BRAND.cliName} gateway รันอยู่แล้ว (pid ${res.state.pid})`);
|
|
1950
|
+
console.log(`log: ${res.state.logPath}`);
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
if (action === 'stop') {
|
|
1954
|
+
const { stopGatewayService } = await import('./gateway/service.js');
|
|
1955
|
+
const res = await stopGatewayService();
|
|
1956
|
+
console.log(res.state ? (res.stopped ? `หยุด gateway pid ${res.state.pid} แล้ว` : `gateway ไม่ได้รันอยู่ (last pid ${res.state.pid})`) : 'ยังไม่มี gateway service state');
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
if (action === 'restart') {
|
|
1960
|
+
const { startGatewayService, stopGatewayService } = await import('./gateway/service.js');
|
|
1961
|
+
await stopGatewayService();
|
|
1962
|
+
const res = await startGatewayService({ entrypoint: resolve(process.argv[1]), gatewayArgs: rest });
|
|
1963
|
+
console.log(`restart gateway แล้ว (pid ${res.state.pid})`);
|
|
1964
|
+
console.log(`log: ${res.state.logPath}`);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
if (action === 'install') {
|
|
1968
|
+
const { installGatewayService } = await import('./gateway/service.js');
|
|
1969
|
+
const res = await installGatewayService(resolve(process.argv[1]));
|
|
1970
|
+
console.log(`ติดตั้ง service file แล้ว (${res.kind}): ${res.path}`);
|
|
1971
|
+
console.log('เริ่ม service ด้วย:');
|
|
1972
|
+
for (const line of res.instructions)
|
|
1973
|
+
console.log(` ${line}`);
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
if (action === 'uninstall' || action === 'remove-service') {
|
|
1977
|
+
const { uninstallGatewayService } = await import('./gateway/service.js');
|
|
1978
|
+
const removed = await uninstallGatewayService();
|
|
1979
|
+
console.log(removed.length ? `ลบ service files:\n${removed.map((p) => ` ${p}`).join('\n')}` : 'ไม่พบ service file ที่ต้องลบ');
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
console.error(`ไม่รู้จัก: gateway ${action} — ใช้ setup / run / start / stop / restart / install / doctor / status`);
|
|
1983
|
+
process.exit(1);
|
|
1984
|
+
}
|
|
1985
|
+
async function runStatus() {
|
|
1986
|
+
const cfg = await loadConfig({});
|
|
1987
|
+
const parsed = parseSpec(cfg.model);
|
|
1988
|
+
const provider = PROVIDERS[parsed.provider];
|
|
1989
|
+
const keyReady = provider ? (!provider.requiresKey || Boolean(resolveKeyFromEnv(provider.envVar, provider.envFallbacks))) : false;
|
|
1990
|
+
const { readGatewayConfig, resolveBlueBubblesConfig, resolveDiscordConfig, resolveEmailConfig, resolveGoogleChatConfig, resolveHomeAssistantConfig, resolveLineConfig, resolveMattermostConfig, resolveMatrixConfig, resolveNtfyConfig, resolveSignalConfig, resolveSlackConfig, resolveSmsConfig, resolveTelegramConfig, resolveTeamsConfig, resolveWhatsAppConfig, resolveWebhookConfig, } = await import('./gateway/config.js');
|
|
1991
|
+
const gatewayConfig = await readGatewayConfig();
|
|
1992
|
+
const telegram = resolveTelegramConfig(gatewayConfig);
|
|
1993
|
+
const discord = resolveDiscordConfig(gatewayConfig);
|
|
1994
|
+
const slack = resolveSlackConfig(gatewayConfig);
|
|
1995
|
+
const email = resolveEmailConfig(gatewayConfig);
|
|
1996
|
+
const homeassistant = resolveHomeAssistantConfig(gatewayConfig);
|
|
1997
|
+
const line = resolveLineConfig(gatewayConfig);
|
|
1998
|
+
const mattermost = resolveMattermostConfig(gatewayConfig);
|
|
1999
|
+
const sms = resolveSmsConfig(gatewayConfig);
|
|
2000
|
+
const ntfy = resolveNtfyConfig(gatewayConfig);
|
|
2001
|
+
const signal = resolveSignalConfig(gatewayConfig);
|
|
2002
|
+
const whatsapp = resolveWhatsAppConfig(gatewayConfig);
|
|
2003
|
+
const matrix = resolveMatrixConfig(gatewayConfig);
|
|
2004
|
+
const googleChat = resolveGoogleChatConfig(gatewayConfig);
|
|
2005
|
+
const bluebubbles = resolveBlueBubblesConfig(gatewayConfig);
|
|
2006
|
+
const teams = resolveTeamsConfig(gatewayConfig);
|
|
2007
|
+
const webhooks = resolveWebhookConfig(gatewayConfig);
|
|
2008
|
+
console.log(`${BRAND.productName} status`);
|
|
2009
|
+
console.log(` version: ${VERSION}`);
|
|
2010
|
+
console.log(` model: ${cfg.model}`);
|
|
2011
|
+
console.log(` provider: ${provider?.label ?? parsed.provider}`);
|
|
2012
|
+
console.log(` personality:${cfg.personality ? ` ${cfg.personality}` : ' none'}`);
|
|
2013
|
+
console.log(` key: ${keyReady ? 'ready' : provider?.requiresKey ? `missing (${provider.envVar})` : 'not required'}`);
|
|
2014
|
+
console.log(` brain: ${cfg.brainPath ?? '(not configured)'}`);
|
|
2015
|
+
console.log(' gateway: HTTP loopback + cron available');
|
|
2016
|
+
console.log(` telegram: ${telegram.token ? `configured (${telegram.allowedChatIds.length} allowed chat${telegram.allowedChatIds.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
2017
|
+
console.log(` discord: ${discord.token ? `configured (${discord.allowedChannelIds.length} allowed channel${discord.allowedChannelIds.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
2018
|
+
console.log(` slack: ${slack.botToken ? `configured (${slack.allowedChannelIds.length} allowed channel${slack.allowedChannelIds.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
2019
|
+
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'}`);
|
|
2020
|
+
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'}`);
|
|
2021
|
+
console.log(` email: ${email.address ? `configured (${email.allowedUsers.length} allowed sender${email.allowedUsers.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
2022
|
+
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'}`);
|
|
2023
|
+
console.log(` sms: ${sms.accountSid && sms.authToken && sms.phoneNumber ? `configured (${sms.allowedUsers.length} allowed sender${sms.allowedUsers.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
2024
|
+
console.log(` ntfy: ${ntfy.topic ? `configured (${ntfy.allowedUsers.length} allowed topic${ntfy.allowedUsers.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
2025
|
+
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'}`);
|
|
2026
|
+
console.log(` whatsapp: ${whatsapp.phoneNumberId && whatsapp.accessToken ? `configured (${whatsapp.allowedUsers.length} allowed user${whatsapp.allowedUsers.length === 1 ? '' : 's'})` : 'not configured'}`);
|
|
2027
|
+
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'}`);
|
|
2028
|
+
console.log(` googlechat:${googleChat.serviceAccountJson || googleChat.incomingWebhookUrl ? ` configured (${googleChat.serviceAccountJson ? 'chat api' : 'webhook'})` : ' not configured'}`);
|
|
2029
|
+
console.log(` bluebubbles:${bluebubbles.serverUrl && bluebubbles.password ? ` configured (${bluebubbles.allowedUsers.length} allowed target${bluebubbles.allowedUsers.length === 1 ? '' : 's'})` : ' not configured'}`);
|
|
2030
|
+
console.log(` teams: ${teams.incomingWebhookUrl || teams.graphAccessToken ? `configured (${teams.deliveryMode})` : 'not configured'}`);
|
|
2031
|
+
console.log(` webhooks: ${webhooks.enabled ? `enabled (${Object.keys(webhooks.routes).length} route${Object.keys(webhooks.routes).length === 1 ? '' : 's'})` : 'not enabled'}`);
|
|
2032
|
+
console.log(` config: ${appHomePath('config.json')}`);
|
|
2033
|
+
}
|
|
2034
|
+
function compactText(raw, max = 120) {
|
|
2035
|
+
const text = redactKey(raw).replace(/\s+/g, ' ').trim();
|
|
2036
|
+
return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text;
|
|
2037
|
+
}
|
|
2038
|
+
function messageContentText(content) {
|
|
2039
|
+
if (typeof content === 'string')
|
|
2040
|
+
return content;
|
|
2041
|
+
if (!Array.isArray(content))
|
|
2042
|
+
return '';
|
|
2043
|
+
return content
|
|
2044
|
+
.map((part) => {
|
|
2045
|
+
if (typeof part === 'string')
|
|
2046
|
+
return part;
|
|
2047
|
+
if (part && typeof part === 'object') {
|
|
2048
|
+
const record = part;
|
|
2049
|
+
if (typeof record.text === 'string')
|
|
2050
|
+
return record.text;
|
|
2051
|
+
if (typeof record.type === 'string')
|
|
2052
|
+
return `[${record.type}]`;
|
|
2053
|
+
}
|
|
2054
|
+
return '';
|
|
2055
|
+
})
|
|
2056
|
+
.filter(Boolean)
|
|
2057
|
+
.join(' ');
|
|
2058
|
+
}
|
|
2059
|
+
function sessionPreview(session) {
|
|
2060
|
+
if (session.title)
|
|
2061
|
+
return compactText(session.title);
|
|
2062
|
+
const firstUser = session.messages.find((m) => m.role === 'user') ?? session.messages[0];
|
|
2063
|
+
return firstUser ? compactText(messageContentText(firstUser.content)) : '';
|
|
2064
|
+
}
|
|
2065
|
+
function sessionUsage() {
|
|
2066
|
+
return `ใช้:
|
|
2067
|
+
${BRAND.cliName} sessions [list] [--all] [--limit N]
|
|
2068
|
+
${BRAND.cliName} sessions latest [--all]
|
|
2069
|
+
${BRAND.cliName} sessions show <id>
|
|
2070
|
+
${BRAND.cliName} sessions export <id> [--format json|markdown] [--output path]
|
|
2071
|
+
${BRAND.cliName} sessions rename <id> <title>
|
|
2072
|
+
${BRAND.cliName} sessions stats [--all]
|
|
2073
|
+
${BRAND.cliName} sessions prune --keep N [--all] [--yes]
|
|
2074
|
+
${BRAND.cliName} sessions rm <id>
|
|
2075
|
+
|
|
2076
|
+
resume:
|
|
2077
|
+
${BRAND.cliName} --resume <id> "<task>"
|
|
2078
|
+
${BRAND.cliName} -r <id> "<task>"`;
|
|
2079
|
+
}
|
|
2080
|
+
function parseLimit(args, fallback) {
|
|
2081
|
+
const provided = args.some((a) => a === '--limit' || a === '-n' || a.startsWith('--limit=') || a.startsWith('-n='));
|
|
2082
|
+
const raw = argValue(args, '--limit', '-n');
|
|
2083
|
+
if (!provided)
|
|
2084
|
+
return fallback;
|
|
2085
|
+
const n = Number(raw);
|
|
2086
|
+
if (!raw || !Number.isInteger(n) || n <= 0) {
|
|
2087
|
+
console.error('--limit ต้องเป็น integer บวก');
|
|
2088
|
+
process.exit(2);
|
|
2089
|
+
}
|
|
2090
|
+
return n;
|
|
2091
|
+
}
|
|
2092
|
+
function printSessionDetails(session) {
|
|
2093
|
+
const users = session.messages.filter((m) => m.role === 'user');
|
|
2094
|
+
const assistants = session.messages.filter((m) => m.role === 'assistant');
|
|
2095
|
+
const lastUser = users[users.length - 1];
|
|
2096
|
+
const lastAssistant = assistants[assistants.length - 1];
|
|
2097
|
+
console.log(`${BRAND.productName} session ${session.id}`);
|
|
2098
|
+
if (session.title)
|
|
2099
|
+
console.log(` title: ${redactKey(session.title)}`);
|
|
2100
|
+
console.log(` model: ${session.model}`);
|
|
2101
|
+
console.log(` cwd: ${session.cwd}`);
|
|
2102
|
+
console.log(` created: ${session.created}`);
|
|
2103
|
+
console.log(` updated: ${session.updated}`);
|
|
2104
|
+
console.log(` messages: ${session.messages.length} (${users.length} user, ${assistants.length} assistant)`);
|
|
2105
|
+
console.log(` preview: ${sessionPreview(session) || '(empty)'}`);
|
|
2106
|
+
if (lastUser)
|
|
2107
|
+
console.log(` last user: ${compactText(messageContentText(lastUser.content)) || '(empty)'}`);
|
|
2108
|
+
if (lastAssistant)
|
|
2109
|
+
console.log(` last ai: ${compactText(messageContentText(lastAssistant.content)) || '(empty)'}`);
|
|
2110
|
+
}
|
|
2111
|
+
function sessionToMarkdown(session) {
|
|
2112
|
+
const safe = sanitizeSessionForExport(session);
|
|
2113
|
+
const lines = [
|
|
2114
|
+
`# ${safe.title ? redactKey(safe.title) : `Session ${safe.id}`}`,
|
|
2115
|
+
'',
|
|
2116
|
+
`- id: ${safe.id}`,
|
|
2117
|
+
`- model: ${safe.model}`,
|
|
2118
|
+
`- cwd: ${safe.cwd}`,
|
|
2119
|
+
`- created: ${safe.created}`,
|
|
2120
|
+
`- updated: ${safe.updated}`,
|
|
2121
|
+
'',
|
|
2122
|
+
];
|
|
2123
|
+
for (const [i, msg] of safe.messages.entries()) {
|
|
2124
|
+
const role = msg.role ?? 'message';
|
|
2125
|
+
const text = compactText(messageContentText(msg.content), 20_000);
|
|
2126
|
+
lines.push(`## ${i + 1}. ${role}`, '', text || '(empty)', '');
|
|
2127
|
+
}
|
|
2128
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
2129
|
+
}
|
|
2130
|
+
function parseDateFlag(args, ...names) {
|
|
2131
|
+
const raw = argValue(args, ...names);
|
|
2132
|
+
if (!raw)
|
|
2133
|
+
return undefined;
|
|
2134
|
+
const d = new Date(raw);
|
|
2135
|
+
if (!Number.isFinite(d.getTime())) {
|
|
2136
|
+
console.error(`วันที่ไม่ถูกต้อง: ${raw}`);
|
|
2137
|
+
process.exit(2);
|
|
2138
|
+
}
|
|
2139
|
+
return d;
|
|
2140
|
+
}
|
|
2141
|
+
async function loadSessionOrExit(id) {
|
|
2142
|
+
const session = await loadSession(id);
|
|
2143
|
+
if (!session) {
|
|
2144
|
+
console.error(`ไม่เจอ session ${id}`);
|
|
2145
|
+
process.exit(1);
|
|
2146
|
+
}
|
|
2147
|
+
return session;
|
|
2148
|
+
}
|
|
2149
|
+
async function requestedResumeSession(rawArgs, resumeId) {
|
|
2150
|
+
const requested = hasResumeRequest(rawArgs);
|
|
2151
|
+
if (!requested)
|
|
2152
|
+
return null;
|
|
2153
|
+
if (!resumeId) {
|
|
2154
|
+
console.error(`ใช้: ${BRAND.cliName} --resume <session_id> "<task>"`);
|
|
2155
|
+
process.exit(2);
|
|
2156
|
+
}
|
|
2157
|
+
return loadSessionOrExit(resumeId);
|
|
2158
|
+
}
|
|
2159
|
+
async function requestedContinuationHistory(rawArgs) {
|
|
2160
|
+
if (!hasContinueRequest(rawArgs))
|
|
2161
|
+
return undefined;
|
|
2162
|
+
return (await latestSession(hasContinueAnyRequest(rawArgs) ? null : process.cwd()))?.messages;
|
|
2163
|
+
}
|
|
2164
|
+
function printSessionStats(sessions, scope) {
|
|
2165
|
+
const byModel = new Map();
|
|
2166
|
+
let messages = 0;
|
|
2167
|
+
for (const s of sessions) {
|
|
2168
|
+
byModel.set(s.model, (byModel.get(s.model) ?? 0) + 1);
|
|
2169
|
+
messages += s.messages.length;
|
|
2170
|
+
}
|
|
2171
|
+
console.log(`${BRAND.productName} session stats (${scope})`);
|
|
2172
|
+
console.log(` sessions: ${sessions.length}`);
|
|
2173
|
+
console.log(` messages: ${messages}`);
|
|
2174
|
+
if (sessions[0])
|
|
2175
|
+
console.log(` latest: ${sessions[0].id} (${sessions[0].updated})`);
|
|
2176
|
+
if (sessions[sessions.length - 1])
|
|
2177
|
+
console.log(` oldest: ${sessions[sessions.length - 1].id} (${sessions[sessions.length - 1].updated})`);
|
|
2178
|
+
console.log(' models:');
|
|
2179
|
+
for (const [model, count] of [...byModel.entries()].sort((a, b) => b[1] - a[1])) {
|
|
2180
|
+
console.log(` ${model}: ${count}`);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
async function runSessions(args) {
|
|
2184
|
+
if (args.includes('-h') || args.includes('--help') || args[0] === 'help') {
|
|
2185
|
+
console.log(sessionUsage());
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
const action = args[0] && !args[0].startsWith('-') ? args[0] : 'list';
|
|
2189
|
+
const rest = action === 'list' && (args[0]?.startsWith('-') || args[0] === undefined) ? args : args.slice(1);
|
|
2190
|
+
const all = rest.includes('--all') || rest.includes('-a');
|
|
2191
|
+
const cwd = all ? null : process.cwd();
|
|
2192
|
+
if (action === 'list' || action === 'ls') {
|
|
2193
|
+
const sessions = await listSessions({ cwd, limit: parseLimit(rest, 20) });
|
|
2194
|
+
if (!sessions.length) {
|
|
2195
|
+
console.log(`ยังไม่มี saved sessions${all ? '' : ' สำหรับ project นี้'} — store: ${sessionStorePath()}`);
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
console.log(`${BRAND.productName} sessions (${all ? 'all projects' : 'current project'})`);
|
|
2199
|
+
for (const s of sessions) {
|
|
2200
|
+
const cwdSuffix = all ? ` ${s.cwd}` : '';
|
|
2201
|
+
console.log(`${s.id} ${s.updated} ${s.model} ${s.messages.length} msg ${sessionPreview(s)}${cwdSuffix}`);
|
|
2202
|
+
}
|
|
2203
|
+
console.log(`\nstore: ${sessionStorePath()}`);
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
if (action === 'latest') {
|
|
2207
|
+
const session = (await listSessions({ cwd, limit: 1 }))[0];
|
|
2208
|
+
if (!session) {
|
|
2209
|
+
console.log(`ไม่เจอ session${all ? '' : ' สำหรับ project นี้'}`);
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
printSessionDetails(session);
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
if (action === 'show' || action === 'cat') {
|
|
2216
|
+
const id = positionalArgs(rest, ['--limit', '-n'])[0];
|
|
2217
|
+
if (!id) {
|
|
2218
|
+
console.error(`ใช้: ${BRAND.cliName} sessions show <id>`);
|
|
2219
|
+
process.exit(2);
|
|
2220
|
+
}
|
|
2221
|
+
printSessionDetails(await loadSessionOrExit(id));
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
if (action === 'export') {
|
|
2225
|
+
const id = positionalArgs(rest, ['--format', '--output', '-o'])[0];
|
|
2226
|
+
if (!id) {
|
|
2227
|
+
console.error(`ใช้: ${BRAND.cliName} sessions export <id> [--format json|markdown] [--output path]`);
|
|
2228
|
+
process.exit(2);
|
|
2229
|
+
}
|
|
2230
|
+
const format = argValue(rest, '--format') ?? 'markdown';
|
|
2231
|
+
if (format !== 'json' && format !== 'markdown' && format !== 'md') {
|
|
2232
|
+
console.error('--format ต้องเป็น json หรือ markdown');
|
|
2233
|
+
process.exit(2);
|
|
2234
|
+
}
|
|
2235
|
+
const session = await loadSessionOrExit(id);
|
|
2236
|
+
const out = format === 'json' ? `${JSON.stringify(sanitizeSessionForExport(session), null, 2)}\n` : sessionToMarkdown(session);
|
|
2237
|
+
const outputPath = argValue(rest, '--output', '-o');
|
|
2238
|
+
if (!outputPath || outputPath === '-') {
|
|
2239
|
+
process.stdout.write(out);
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
2243
|
+
await writeFile(outputPath, out, { mode: 0o600 });
|
|
2244
|
+
await chmod(outputPath, 0o600).catch(() => { });
|
|
2245
|
+
console.log(`exported session ${id} → ${outputPath}`);
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
if (action === 'rename' || action === 'title') {
|
|
2249
|
+
const [id, ...titleParts] = positionalArgs(rest, ['--limit', '-n']);
|
|
2250
|
+
const title = titleParts.join(' ').trim();
|
|
2251
|
+
if (!id || !title) {
|
|
2252
|
+
console.error(`ใช้: ${BRAND.cliName} sessions rename <id> <title>`);
|
|
2253
|
+
process.exit(2);
|
|
2254
|
+
}
|
|
2255
|
+
const next = await renameSession(id, title);
|
|
2256
|
+
if (!next) {
|
|
2257
|
+
console.error(`ไม่เจอ session ${id}`);
|
|
2258
|
+
process.exit(1);
|
|
2259
|
+
}
|
|
2260
|
+
console.log(`ตั้งชื่อ session ${id}: ${redactKey(next.title ?? '')}`);
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
if (action === 'stats') {
|
|
2264
|
+
printSessionStats(await listSessions({ cwd }), all ? 'all projects' : 'current project');
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
if (action === 'prune') {
|
|
2268
|
+
const keepRaw = argValue(rest, '--keep');
|
|
2269
|
+
const before = parseDateFlag(rest, '--before');
|
|
2270
|
+
if (!keepRaw && !before) {
|
|
2271
|
+
console.error(`ใช้: ${BRAND.cliName} sessions prune --keep N [--before YYYY-MM-DD] [--all] [--yes]`);
|
|
2272
|
+
process.exit(2);
|
|
2273
|
+
}
|
|
2274
|
+
const keep = keepRaw == null ? undefined : Number(keepRaw);
|
|
2275
|
+
if (keep != null && (!Number.isInteger(keep) || keep < 0)) {
|
|
2276
|
+
console.error('--keep ต้องเป็น integer >= 0');
|
|
2277
|
+
process.exit(2);
|
|
2278
|
+
}
|
|
2279
|
+
const candidates = await listSessions({ cwd });
|
|
2280
|
+
const candidateIds = new Set();
|
|
2281
|
+
if (keep != null)
|
|
2282
|
+
for (const s of candidates.slice(keep))
|
|
2283
|
+
candidateIds.add(s.id);
|
|
2284
|
+
if (before) {
|
|
2285
|
+
const beforeMs = before.getTime();
|
|
2286
|
+
for (const s of candidates) {
|
|
2287
|
+
const updatedMs = Date.parse(s.updated);
|
|
2288
|
+
if (Number.isFinite(updatedMs) && updatedMs < beforeMs)
|
|
2289
|
+
candidateIds.add(s.id);
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
if (!candidateIds.size) {
|
|
2293
|
+
console.log('ไม่มี session ที่ต้อง prune');
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
if (!rest.includes('--yes') && !rest.includes('-y')) {
|
|
2297
|
+
console.log(`จะลบ ${candidateIds.size} sessions (dry-run):`);
|
|
2298
|
+
for (const s of candidates.filter((x) => candidateIds.has(x.id)))
|
|
2299
|
+
console.log(` ${s.id} ${s.updated} ${sessionPreview(s)}`);
|
|
2300
|
+
console.log(`\nรันซ้ำพร้อม --yes เพื่อยืนยัน`);
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
const removed = await pruneSessions({ cwd, keep, before });
|
|
2304
|
+
console.log(`ลบ ${removed.length} sessions แล้ว`);
|
|
2305
|
+
return;
|
|
2306
|
+
}
|
|
2307
|
+
if (action === 'rm' || action === 'remove' || action === 'delete') {
|
|
2308
|
+
const id = positionalArgs(rest, ['--limit', '-n'])[0];
|
|
2309
|
+
if (!id) {
|
|
2310
|
+
console.error(`ใช้: ${BRAND.cliName} sessions rm <id>`);
|
|
2311
|
+
process.exit(2);
|
|
2312
|
+
}
|
|
2313
|
+
const ok = await removeSession(id);
|
|
2314
|
+
console.log(ok ? `ลบ session ${id} แล้ว` : `ไม่เจอ session ${id}`);
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
2317
|
+
console.error(`ไม่รู้จัก: sessions ${action}\n${sessionUsage()}`);
|
|
2318
|
+
process.exit(1);
|
|
2319
|
+
}
|
|
2320
|
+
async function runInsights(args) {
|
|
2321
|
+
const { parseInsightsArgs } = await import('./insights-args.js');
|
|
2322
|
+
const parsed = parseInsightsArgs(args);
|
|
2323
|
+
if (parsed === null) {
|
|
2324
|
+
console.error(`ใช้: ${BRAND.cliName} insights [--days N] [--all]`);
|
|
2325
|
+
process.exit(2);
|
|
2326
|
+
}
|
|
2327
|
+
const { renderInsights } = await import('./insights.js');
|
|
2328
|
+
console.log(await renderInsights({ days: parsed.days, cwd: parsed.all ? null : process.cwd(), includeGateway: true }));
|
|
2329
|
+
}
|
|
2330
|
+
async function runDump(args) {
|
|
2331
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
2332
|
+
console.log(`ใช้: ${BRAND.cliName} dump [--show-keys]\n\nสร้าง diagnostic/support dump โดย redact secret เสมอ`);
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
const { buildSupportDump } = await import('./support-dump.js');
|
|
2336
|
+
process.stdout.write(await buildSupportDump({
|
|
2337
|
+
showKeys: args.includes('--show-keys'),
|
|
2338
|
+
version: VERSION,
|
|
2339
|
+
packageName: PACKAGE_NAME,
|
|
2340
|
+
cwd: process.cwd(),
|
|
2341
|
+
}));
|
|
2342
|
+
}
|
|
2343
|
+
function providerIds() {
|
|
2344
|
+
return Object.keys(PROVIDERS).join(', ');
|
|
2345
|
+
}
|
|
2346
|
+
function findProviderId(raw) {
|
|
2347
|
+
if (!raw)
|
|
2348
|
+
return undefined;
|
|
2349
|
+
const lower = raw.toLowerCase();
|
|
2350
|
+
if (PROVIDERS[lower])
|
|
2351
|
+
return lower;
|
|
2352
|
+
for (const [id, cfg] of Object.entries(PROVIDERS)) {
|
|
2353
|
+
if (raw === cfg.envVar || cfg.envFallbacks?.includes(raw))
|
|
2354
|
+
return id;
|
|
2355
|
+
}
|
|
2356
|
+
return undefined;
|
|
2357
|
+
}
|
|
2358
|
+
function authEnvSource(providerId) {
|
|
2359
|
+
const cfg = PROVIDERS[providerId];
|
|
2360
|
+
if (!cfg)
|
|
2361
|
+
return undefined;
|
|
2362
|
+
for (const name of [cfg.envVar, ...(cfg.envFallbacks ?? [])]) {
|
|
2363
|
+
if (process.env[name]?.trim())
|
|
2364
|
+
return name;
|
|
2365
|
+
}
|
|
2366
|
+
return undefined;
|
|
2367
|
+
}
|
|
2368
|
+
function authUsage() {
|
|
2369
|
+
return `ใช้:
|
|
2370
|
+
${BRAND.cliName} auth list
|
|
2371
|
+
${BRAND.cliName} auth status <provider>
|
|
2372
|
+
${BRAND.cliName} auth add <provider> --api-key <key> [--use]
|
|
2373
|
+
${BRAND.cliName} auth remove <provider|ENV_VAR>
|
|
2374
|
+
${BRAND.cliName} auth reset [provider|ENV_VAR]
|
|
2375
|
+
|
|
2376
|
+
providers: ${providerIds()}`;
|
|
2377
|
+
}
|
|
2378
|
+
async function runAuth(args) {
|
|
2379
|
+
const action = args[0] ?? 'list';
|
|
2380
|
+
const rest = args.slice(action === 'list' && args[0] !== 'list' ? 0 : 1);
|
|
2381
|
+
const { authConfigPath, clearStoredAuth, readStoredAuthRaw, removeStoredKey, saveGlobalConfig, saveKey } = await import('./config.js');
|
|
2382
|
+
if (action === '-h' || action === '--help' || action === 'help') {
|
|
2383
|
+
console.log(authUsage());
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
if (action === 'list' || action === 'ls' || action === 'status-all') {
|
|
2387
|
+
const stored = await readStoredAuthRaw();
|
|
2388
|
+
console.log(`${BRAND.productName} auth`);
|
|
2389
|
+
console.log(` store: ${authConfigPath()}`);
|
|
2390
|
+
for (const [id, cfg] of Object.entries(PROVIDERS)) {
|
|
2391
|
+
if (!cfg.requiresKey) {
|
|
2392
|
+
console.log(` ${id.padEnd(10)} ${cfg.label} — no API key required`);
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
const key = resolveKeyFromEnv(cfg.envVar, cfg.envFallbacks);
|
|
2396
|
+
const source = authEnvSource(id);
|
|
2397
|
+
const saved = stored[cfg.envVar];
|
|
2398
|
+
const state = key ? `ready via ${source ?? cfg.envVar}` : `missing ${cfg.envVar}`;
|
|
2399
|
+
const savedText = saved ? ` · stored ${redactKey(saved)}` : '';
|
|
2400
|
+
console.log(` ${id.padEnd(10)} ${cfg.label} — ${state}${savedText}`);
|
|
2401
|
+
}
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
if (action === 'status') {
|
|
2405
|
+
const providerId = findProviderId(positionalArgs(rest, ['--api-key', '--key', '--token', '--model'])[0]);
|
|
2406
|
+
if (!providerId) {
|
|
2407
|
+
console.error(`ใช้: ${BRAND.cliName} auth status <provider>\nproviders: ${providerIds()}`);
|
|
2408
|
+
process.exit(1);
|
|
2409
|
+
}
|
|
2410
|
+
const cfg = PROVIDERS[providerId];
|
|
2411
|
+
const stored = await readStoredAuthRaw();
|
|
2412
|
+
const key = resolveKeyFromEnv(cfg.envVar, cfg.envFallbacks);
|
|
2413
|
+
const source = authEnvSource(providerId);
|
|
2414
|
+
console.log(`${cfg.label} (${providerId})`);
|
|
2415
|
+
console.log(` key required: ${cfg.requiresKey ? 'yes' : 'no'}`);
|
|
2416
|
+
console.log(` env var: ${cfg.envVar}${cfg.envFallbacks?.length ? ` (fallback: ${cfg.envFallbacks.join(', ')})` : ''}`);
|
|
2417
|
+
console.log(` stored: ${stored[cfg.envVar] ? redactKey(stored[cfg.envVar]) : '(not stored)'}`);
|
|
2418
|
+
console.log(` runtime: ${key ? `${redactKey(key)} via ${source ?? cfg.envVar}` : '(missing)'}`);
|
|
2419
|
+
const url = consoleUrl(providerId);
|
|
2420
|
+
if (url)
|
|
2421
|
+
console.log(` console: ${url}`);
|
|
2422
|
+
if (cfg.note)
|
|
2423
|
+
console.log(` note: ${cfg.note}`);
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
if (action === 'add' || action === 'login') {
|
|
2427
|
+
const providerId = findProviderId(positionalArgs(rest, ['--api-key', '--key', '--token', '--model'])[0]);
|
|
2428
|
+
if (!providerId) {
|
|
2429
|
+
console.error(`ใช้: ${BRAND.cliName} auth add <provider> --api-key <key>\nproviders: ${providerIds()}`);
|
|
2430
|
+
process.exit(1);
|
|
2431
|
+
}
|
|
2432
|
+
const cfg = PROVIDERS[providerId];
|
|
2433
|
+
if (!cfg.requiresKey) {
|
|
2434
|
+
console.log(`${cfg.label} ไม่ต้องเก็บ API key ใน Sanook`);
|
|
2435
|
+
if (cfg.note)
|
|
2436
|
+
console.log(cfg.note);
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
let key = argValue(rest, '--api-key', '--key', '--token');
|
|
2440
|
+
if (!key) {
|
|
2441
|
+
if (!process.stdin.isTTY) {
|
|
2442
|
+
console.error(`ใช้: ${BRAND.cliName} auth add ${providerId} --api-key <key>`);
|
|
2443
|
+
process.exit(1);
|
|
2444
|
+
}
|
|
2445
|
+
key = await askText(`${cfg.label} API key (${cfg.keyExample ?? cfg.envVar}): `);
|
|
2446
|
+
}
|
|
2447
|
+
try {
|
|
2448
|
+
assertDirectApiKey(cfg, key);
|
|
2449
|
+
}
|
|
2450
|
+
catch (e) {
|
|
2451
|
+
console.error(redactKey(e.message));
|
|
2452
|
+
process.exit(1);
|
|
2453
|
+
}
|
|
2454
|
+
await saveKey(cfg.envVar, key.trim());
|
|
2455
|
+
console.log(`บันทึก ${cfg.label} key แล้ว: ${cfg.envVar}=${redactKey(key.trim())}`);
|
|
2456
|
+
if (rest.includes('--use') || rest.includes('--default')) {
|
|
2457
|
+
const modelArg = argValue(rest, '--model', '-m') ?? 'default';
|
|
2458
|
+
const model = modelArg.includes(':') ? modelArg : `${providerId}:${cfg.models[modelArg] ?? modelArg}`;
|
|
2459
|
+
await saveGlobalConfig({ model, provider: providerId });
|
|
2460
|
+
console.log(`ตั้ง default model เป็น ${model}`);
|
|
2461
|
+
}
|
|
2462
|
+
else {
|
|
2463
|
+
console.log(`ใช้เป็น default ได้ด้วย: ${BRAND.cliName} auth add ${providerId} --api-key <key> --use`);
|
|
2464
|
+
}
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
if (action === 'remove' || action === 'rm' || action === 'logout') {
|
|
2468
|
+
const target = positionalArgs(rest, ['--api-key', '--key', '--token', '--model'])[0];
|
|
2469
|
+
if (!target && action !== 'logout') {
|
|
2470
|
+
console.error(`ใช้: ${BRAND.cliName} auth remove <provider|ENV_VAR>`);
|
|
2471
|
+
process.exit(1);
|
|
2472
|
+
}
|
|
2473
|
+
if (!target && action === 'logout') {
|
|
2474
|
+
await clearStoredAuth();
|
|
2475
|
+
console.log('ล้าง key ที่ Sanook เก็บไว้ทั้งหมดแล้ว');
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
const providerId = findProviderId(target);
|
|
2479
|
+
const envVar = providerId ? PROVIDERS[providerId].envVar : target;
|
|
2480
|
+
const ok = await removeStoredKey(envVar);
|
|
2481
|
+
console.log(ok ? `ลบ ${envVar} ออกจาก Sanook auth store แล้ว` : `ไม่เจอ ${envVar} ใน Sanook auth store`);
|
|
2482
|
+
return;
|
|
2483
|
+
}
|
|
2484
|
+
if (action === 'reset' || action === 'clear') {
|
|
2485
|
+
const target = positionalArgs(rest, ['--api-key', '--key', '--token', '--model'])[0];
|
|
2486
|
+
if (!target) {
|
|
2487
|
+
await clearStoredAuth();
|
|
2488
|
+
console.log('ล้าง key ที่ Sanook เก็บไว้ทั้งหมดแล้ว');
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
const providerId = findProviderId(target);
|
|
2492
|
+
const envVar = providerId ? PROVIDERS[providerId].envVar : target;
|
|
2493
|
+
const ok = await removeStoredKey(envVar);
|
|
2494
|
+
console.log(ok ? `ลบ ${envVar} ออกจาก Sanook auth store แล้ว` : `ไม่เจอ ${envVar} ใน Sanook auth store`);
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
console.error(`ไม่รู้จัก: auth ${action}\n${authUsage()}`);
|
|
2498
|
+
process.exit(1);
|
|
2499
|
+
}
|
|
2500
|
+
async function runSetup(args) {
|
|
2501
|
+
if (args.includes('-h') || args.includes('--help') || args[0] === 'help' || args[0] === 'list' || args[0] === 'status') {
|
|
2502
|
+
console.log(await setupOverview());
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
const section = args.find((a) => !a.startsWith('-')) ?? 'model';
|
|
2506
|
+
const start = args.indexOf(section);
|
|
2507
|
+
const rest = start === -1 ? [] : args.slice(start + 1);
|
|
2508
|
+
if (section === 'model')
|
|
2509
|
+
return startModelSetup();
|
|
2510
|
+
if (section === 'gateway')
|
|
2511
|
+
return runGateway(['setup', ...rest]);
|
|
2512
|
+
if (section === 'tools')
|
|
2513
|
+
return runTools(rest);
|
|
2514
|
+
if (section === 'agent')
|
|
2515
|
+
return runAgentSetupSummary();
|
|
2516
|
+
if (section === 'brain')
|
|
2517
|
+
return runBrain(['init', ...rest]);
|
|
2518
|
+
console.error(`ไม่รู้จัก setup section "${section}" — ใช้ model / gateway / tools / agent / brain`);
|
|
2519
|
+
process.exit(1);
|
|
2520
|
+
}
|
|
2521
|
+
async function setupOverview() {
|
|
2522
|
+
const cfg = await loadConfig({});
|
|
2523
|
+
const { readGatewayConfig, resolveBlueBubblesConfig, resolveDiscordConfig, resolveEmailConfig, resolveGoogleChatConfig, resolveHomeAssistantConfig, resolveLineConfig, resolveMattermostConfig, resolveMatrixConfig, resolveNtfyConfig, resolveSignalConfig, resolveSlackConfig, resolveSmsConfig, resolveTelegramConfig, resolveTeamsConfig, resolveWhatsAppConfig, resolveWebhookConfig, } = await import('./gateway/config.js');
|
|
2524
|
+
const gateway = await readGatewayConfig();
|
|
2525
|
+
const telegram = resolveTelegramConfig(gateway);
|
|
2526
|
+
const discord = resolveDiscordConfig(gateway);
|
|
2527
|
+
const slack = resolveSlackConfig(gateway);
|
|
2528
|
+
const mattermost = resolveMattermostConfig(gateway);
|
|
2529
|
+
const homeassistant = resolveHomeAssistantConfig(gateway);
|
|
2530
|
+
const email = resolveEmailConfig(gateway);
|
|
2531
|
+
const line = resolveLineConfig(gateway);
|
|
2532
|
+
const sms = resolveSmsConfig(gateway);
|
|
2533
|
+
const ntfy = resolveNtfyConfig(gateway);
|
|
2534
|
+
const signal = resolveSignalConfig(gateway);
|
|
2535
|
+
const whatsapp = resolveWhatsAppConfig(gateway);
|
|
2536
|
+
const matrix = resolveMatrixConfig(gateway);
|
|
2537
|
+
const googleChat = resolveGoogleChatConfig(gateway);
|
|
2538
|
+
const bluebubbles = resolveBlueBubblesConfig(gateway);
|
|
2539
|
+
const teams = resolveTeamsConfig(gateway);
|
|
2540
|
+
const webhooks = resolveWebhookConfig(gateway);
|
|
2541
|
+
const configuredPlatforms = [
|
|
2542
|
+
telegram.token ? 'telegram' : '',
|
|
2543
|
+
discord.token ? 'discord' : '',
|
|
2544
|
+
slack.botToken ? 'slack' : '',
|
|
2545
|
+
mattermost.serverUrl && mattermost.token ? 'mattermost' : '',
|
|
2546
|
+
homeassistant.token ? 'homeassistant' : '',
|
|
2547
|
+
email.address ? 'email' : '',
|
|
2548
|
+
line.channelAccessToken ? 'line' : '',
|
|
2549
|
+
sms.accountSid ? 'sms' : '',
|
|
2550
|
+
ntfy.topic ? 'ntfy' : '',
|
|
2551
|
+
signal.account ? 'signal' : '',
|
|
2552
|
+
whatsapp.phoneNumberId && whatsapp.accessToken ? 'whatsapp' : '',
|
|
2553
|
+
matrix.homeserver ? 'matrix' : '',
|
|
2554
|
+
googleChat.serviceAccountJson || googleChat.incomingWebhookUrl ? 'googlechat' : '',
|
|
2555
|
+
bluebubbles.serverUrl && bluebubbles.password ? 'bluebubbles' : '',
|
|
2556
|
+
teams.incomingWebhookUrl || teams.graphAccessToken ? 'teams' : '',
|
|
2557
|
+
webhooks.enabled ? 'webhooks' : '',
|
|
2558
|
+
].filter(Boolean);
|
|
2559
|
+
return [
|
|
2560
|
+
`${BRAND.productName} setup`,
|
|
2561
|
+
'',
|
|
2562
|
+
` model ${BRAND.cliName} setup model เลือก provider + model (current: ${cfg.model})`,
|
|
2563
|
+
` gateway ${BRAND.cliName} setup gateway เชื่อม messaging platforms (${configuredPlatforms.length ? configuredPlatforms.join(', ') : 'not configured'})`,
|
|
2564
|
+
` tools ${BRAND.cliName} setup tools ดู tool surface + MCP entry points`,
|
|
2565
|
+
` agent ${BRAND.cliName} setup agent ตั้ง permission/budget/personality/insights`,
|
|
2566
|
+
` brain ${BRAND.cliName} setup brain สร้าง Second Brain vault + AGENTS/GEMINI/SANOOK rules`,
|
|
2567
|
+
'',
|
|
2568
|
+
`เริ่มเร็ว: ${BRAND.cliName} setup model`,
|
|
2569
|
+
`ดูสถานะ: ${BRAND.cliName} status`,
|
|
2570
|
+
].join('\n');
|
|
2571
|
+
}
|
|
2572
|
+
function modelOverrideForProvider(providerArg, modelArg) {
|
|
2573
|
+
const providerId = findProviderId(providerArg);
|
|
2574
|
+
if (!providerArg)
|
|
2575
|
+
return modelArg;
|
|
2576
|
+
if (!providerId) {
|
|
2577
|
+
console.error(`ไม่รู้จัก provider "${providerArg}" — มี: ${providerIds()}`);
|
|
2578
|
+
process.exit(1);
|
|
2579
|
+
}
|
|
2580
|
+
if (!modelArg)
|
|
2581
|
+
return `${providerId}:${PROVIDERS[providerId].models.default}`;
|
|
2582
|
+
if (modelArg.includes(':'))
|
|
2583
|
+
return modelArg;
|
|
2584
|
+
return `${providerId}:${PROVIDERS[providerId].models[modelArg] ?? modelArg}`;
|
|
2585
|
+
}
|
|
2586
|
+
function appendPipedInput(prompt, piped) {
|
|
2587
|
+
return piped ? `${prompt}\n\n<stdin>\n${piped}\n</stdin>`.trim() : prompt;
|
|
2588
|
+
}
|
|
2589
|
+
async function runChat(args) {
|
|
2590
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
2591
|
+
console.log(`ใช้:
|
|
2592
|
+
${BRAND.cliName} chat -q "<query>" [--provider <provider>] [--model <alias|id>]
|
|
2593
|
+
${BRAND.cliName} chat "<query>" [--provider <provider>]
|
|
2594
|
+
${BRAND.cliName} chat เปิด interactive REPL
|
|
2595
|
+
|
|
2596
|
+
providers: ${providerIds()}`);
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
let split = extractValue(args, '-q', '--query');
|
|
2600
|
+
const query = split.value;
|
|
2601
|
+
split = extractValue(split.rest, '--provider');
|
|
2602
|
+
const provider = split.value;
|
|
2603
|
+
split = extractValue(split.rest, '--toolsets', '--tools');
|
|
2604
|
+
const toolsets = split.value;
|
|
2605
|
+
const safeMode = split.rest.includes('--safe-mode');
|
|
2606
|
+
const yolo = split.rest.includes('--yolo') || split.rest.includes('--dangerously-skip-permissions');
|
|
2607
|
+
const cleaned = stripBooleanFlags(split.rest, '--safe-mode', '--yolo', '--dangerously-skip-permissions');
|
|
2608
|
+
const parsed = parseArgs(yolo ? [...cleaned, '--yes'] : cleaned);
|
|
2609
|
+
const resumeSession = await requestedResumeSession(cleaned, parsed.resume);
|
|
2610
|
+
const budgetUsd = Number.isFinite(parsed.budget) ? parsed.budget : undefined;
|
|
2611
|
+
const model = modelOverrideForProvider(provider, parsed.model ?? (provider ? undefined : resumeSession?.model));
|
|
2612
|
+
const piped = process.stdin.isTTY ? '' : (await readStdin()).trim();
|
|
2613
|
+
const prompt = appendPipedInput(query ?? parsed.prompt, piped);
|
|
2614
|
+
if (toolsets && !parsed.quiet && !parsed.json) {
|
|
2615
|
+
process.stderr.write(`${DIM}(toolsets="${toolsets}" accepted; Sanook currently exposes the configured tool surface)${RESET}\n`);
|
|
2616
|
+
}
|
|
2617
|
+
if (!prompt) {
|
|
2618
|
+
const config = await loadConfig({ model, budgetUsd });
|
|
2619
|
+
const { startApp } = await import('./ui/render.js');
|
|
2620
|
+
startApp({
|
|
2621
|
+
needsSetup: false,
|
|
2622
|
+
appProps: {
|
|
2623
|
+
initialModel: config.model,
|
|
2624
|
+
fallbackModel: config.fallbackModel,
|
|
2625
|
+
budgetUsd: config.budgetUsd,
|
|
2626
|
+
permissionMode: parsed.yes || yolo ? 'auto' : safeMode ? 'ask' : config.permissionMode,
|
|
2627
|
+
initialHistory: resumeSession?.messages ?? (await requestedContinuationHistory(cleaned)),
|
|
2628
|
+
},
|
|
2629
|
+
});
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
const config = await loadConfig({ model, budgetUsd });
|
|
2633
|
+
const noKey = headlessKeyHint(config.model);
|
|
2634
|
+
if (noKey) {
|
|
2635
|
+
process.stderr.write(`${noKey}\n`);
|
|
2636
|
+
process.exit(1);
|
|
2637
|
+
}
|
|
2638
|
+
const history = resumeSession?.messages ?? (await requestedContinuationHistory(args));
|
|
2639
|
+
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, parsed.planMode ? prompt : undefined);
|
|
2640
|
+
}
|
|
2641
|
+
async function runPureOneShot(args) {
|
|
2642
|
+
const rest = args;
|
|
2643
|
+
const parsed = parseArgs(rest);
|
|
2644
|
+
const resumeSession = await requestedResumeSession(rest, parsed.resume);
|
|
2645
|
+
const budgetUsd = Number.isFinite(parsed.budget) ? parsed.budget : undefined;
|
|
2646
|
+
const piped = process.stdin.isTTY ? '' : (await readStdin()).trim();
|
|
2647
|
+
const prompt = appendPipedInput(parsed.prompt, piped);
|
|
2648
|
+
if (!prompt) {
|
|
2649
|
+
console.error(`ใช้: ${BRAND.cliName} -z "<task>"`);
|
|
2650
|
+
process.exit(1);
|
|
2651
|
+
}
|
|
2652
|
+
const config = await loadConfig({ model: parsed.model ?? resumeSession?.model, budgetUsd });
|
|
2653
|
+
const noKey = headlessKeyHint(config.model);
|
|
2654
|
+
if (noKey) {
|
|
2655
|
+
process.stderr.write(`${noKey}\n`);
|
|
2656
|
+
process.exit(1);
|
|
2657
|
+
}
|
|
2658
|
+
const history = resumeSession?.messages ?? (await requestedContinuationHistory(rest));
|
|
2659
|
+
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, parsed.json, history, parsed.planMode, parsed.yes ? 'auto' : config.permissionMode, true, config.fallbackModel, parsed.planMode ? prompt : undefined);
|
|
2660
|
+
}
|
|
2661
|
+
async function runSend(args) {
|
|
2662
|
+
const json = args.includes('--json');
|
|
2663
|
+
const quiet = args.includes('--quiet') || args.includes('-q');
|
|
2664
|
+
const wantsList = args.includes('--list') || args.includes('-l');
|
|
2665
|
+
const valueFlags = ['--to', '-t', '--file', '-f', '--subject', '-s'];
|
|
2666
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
2667
|
+
console.log(`ใช้:
|
|
2668
|
+
${BRAND.cliName} send --to telegram[:chat_id[:thread_id]] "message"
|
|
2669
|
+
${BRAND.cliName} send --to discord[:channel_id[:thread_id]] "message"
|
|
2670
|
+
${BRAND.cliName} send --to slack[:channel_id[:thread_ts]] "message"
|
|
2671
|
+
${BRAND.cliName} send --to mattermost[:channel_id[:root_post_id]] "message"
|
|
2672
|
+
${BRAND.cliName} send --to homeassistant[:notification_id] "message"
|
|
2673
|
+
${BRAND.cliName} send --to email[:recipient@example.com] --subject "[CI]" "message"
|
|
2674
|
+
${BRAND.cliName} send --to line[:U/C/R-id] "message"
|
|
2675
|
+
${BRAND.cliName} send --to sms[:+15558675310] "message"
|
|
2676
|
+
${BRAND.cliName} send --to ntfy[:topic] "message"
|
|
2677
|
+
${BRAND.cliName} send --to signal[:+15558675310|group:<id>] "message"
|
|
2678
|
+
${BRAND.cliName} send --to whatsapp[:15558675310] "message"
|
|
2679
|
+
${BRAND.cliName} send --to matrix[:!roomid:matrix.org] "message"
|
|
2680
|
+
${BRAND.cliName} send --to googlechat[:spaces/AAA|spaces/AAA/threads/BBB] "message"
|
|
2681
|
+
${BRAND.cliName} send --to bluebubbles[:chat-guid|email|phone] "message"
|
|
2682
|
+
${BRAND.cliName} send --to teams[:chat_id|team/<team-id>/channel/<channel-id>] "message"
|
|
2683
|
+
${BRAND.cliName} send --to slack --subject "[CI]" --file build.log
|
|
2684
|
+
echo "done" | ${BRAND.cliName} send --to telegram --quiet
|
|
2685
|
+
${BRAND.cliName} send --list [platform] [--json]`);
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2688
|
+
if (wantsList) {
|
|
2689
|
+
const { listConfiguredTargets } = await import('./gateway/targets.js');
|
|
2690
|
+
const { readGatewayConfig } = await import('./gateway/config.js');
|
|
2691
|
+
const filter = positionalArgs(args, valueFlags)[0];
|
|
2692
|
+
const targets = listConfiguredTargets(await readGatewayConfig()).filter((t) => !filter || t.platform === filter);
|
|
2693
|
+
if (json) {
|
|
2694
|
+
console.log(JSON.stringify({ targets }));
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
if (!targets.length) {
|
|
2698
|
+
console.log(filter ? `ยังไม่มี target สำหรับ ${filter}` : `ยังไม่มี messaging target — เริ่มด้วย: ${BRAND.cliName} gateway setup`);
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
for (const t of targets) {
|
|
2702
|
+
console.log(`${t.target.padEnd(24)} ${t.configured ? 'ready' : 'not-ready'} ${t.label}`);
|
|
2703
|
+
}
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
const to = argValue(args, '--to', '-t');
|
|
2707
|
+
if (!to) {
|
|
2708
|
+
console.error(`ใช้: ${BRAND.cliName} send --to <telegram|discord|slack|mattermost|homeassistant|email|line|sms|ntfy|signal|whatsapp|matrix|googlechat|bluebubbles|teams>[:target] "message"`);
|
|
2709
|
+
process.exit(2);
|
|
2710
|
+
}
|
|
2711
|
+
const file = argValue(args, '--file', '-f');
|
|
2712
|
+
const subject = argValue(args, '--subject', '-s');
|
|
2713
|
+
let message = positionalArgs(args, valueFlags).join(' ').trim();
|
|
2714
|
+
if (!message && file)
|
|
2715
|
+
message = file === '-' ? await readStdin() : await readFile(file, 'utf8');
|
|
2716
|
+
if (!message && !process.stdin.isTTY)
|
|
2717
|
+
message = (await readStdin()).trim();
|
|
2718
|
+
if (subject && message && !to.startsWith('email'))
|
|
2719
|
+
message = `${subject.trim()}\n\n${message.trim()}`;
|
|
2720
|
+
if (!message) {
|
|
2721
|
+
console.error('message ว่าง — ใส่ข้อความ, --file <path>, หรือ pipe stdin เข้ามา');
|
|
2722
|
+
process.exit(2);
|
|
2723
|
+
}
|
|
2724
|
+
const { parseSendTarget } = await import('./gateway/targets.js');
|
|
2725
|
+
try {
|
|
2726
|
+
parseSendTarget(to);
|
|
2727
|
+
}
|
|
2728
|
+
catch (e) {
|
|
2729
|
+
console.error(e.message);
|
|
2730
|
+
process.exit(2);
|
|
2731
|
+
}
|
|
2732
|
+
const { deliverToTarget } = await import('./gateway/deliver.js');
|
|
2733
|
+
try {
|
|
2734
|
+
const result = await deliverToTarget(to, message, { subject });
|
|
2735
|
+
if (json)
|
|
2736
|
+
console.log(JSON.stringify({ ok: true, ...result }));
|
|
2737
|
+
else if (!quiet)
|
|
2738
|
+
console.log(`sent ${result.target}`);
|
|
2739
|
+
}
|
|
2740
|
+
catch (e) {
|
|
2741
|
+
const msg = redactKey(e.message);
|
|
2742
|
+
if (json)
|
|
2743
|
+
console.log(JSON.stringify({ ok: false, error: msg }));
|
|
2744
|
+
else
|
|
2745
|
+
console.error(`ส่งไม่สำเร็จ: ${msg}`);
|
|
177
2746
|
process.exit(1);
|
|
178
2747
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
2748
|
+
}
|
|
2749
|
+
async function runWebhook(args) {
|
|
2750
|
+
const action = args[0] ?? 'list';
|
|
2751
|
+
const rest = action === 'list' && args[0] !== 'list' ? args : args.slice(1);
|
|
2752
|
+
const valueFlags = [
|
|
2753
|
+
'--events',
|
|
2754
|
+
'--prompt',
|
|
2755
|
+
'--to',
|
|
2756
|
+
'-t',
|
|
2757
|
+
'--deliver',
|
|
2758
|
+
'--deliver-chat-id',
|
|
2759
|
+
'--chat-id',
|
|
2760
|
+
'--secret',
|
|
2761
|
+
'--description',
|
|
2762
|
+
'--payload',
|
|
2763
|
+
'--public-url',
|
|
2764
|
+
'--rate-limit',
|
|
2765
|
+
'--rate-limit-per-minute',
|
|
2766
|
+
];
|
|
2767
|
+
if (args.includes('-h') || args.includes('--help') || action === 'help') {
|
|
2768
|
+
console.log(`ใช้:
|
|
2769
|
+
${BRAND.cliName} webhook subscribe <route> [--events issues,push] [--prompt "..."] [--to telegram|slack:C01|mattermost:chan|homeassistant|sms|ntfy|signal|whatsapp|matrix|googlechat|bluebubbles|teams]
|
|
2770
|
+
${BRAND.cliName} webhook subscribe <route> --deliver telegram --deliver-chat-id 123 --deliver-only --prompt "New event: {__raw__}"
|
|
2771
|
+
${BRAND.cliName} webhook list
|
|
2772
|
+
${BRAND.cliName} webhook remove <route>
|
|
2773
|
+
${BRAND.cliName} webhook test <route> --payload '{"event_type":"ping"}'
|
|
2774
|
+
|
|
2775
|
+
signature headers:
|
|
2776
|
+
GitHub: X-Hub-Signature-256: sha256=<hmac>
|
|
2777
|
+
GitLab: X-Gitlab-Token: <secret>
|
|
2778
|
+
Generic: X-Webhook-Signature: <hmac-hex>`);
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
if (action === 'subscribe' || action === 'add') {
|
|
2782
|
+
const name = positionalArgs(rest, valueFlags)[0];
|
|
2783
|
+
if (!name) {
|
|
2784
|
+
console.error(`ใช้: ${BRAND.cliName} webhook subscribe <route> [--prompt "..."] [--to <target>]`);
|
|
2785
|
+
process.exit(2);
|
|
2786
|
+
}
|
|
2787
|
+
const { isValidWebhookRouteName, generateWebhookSecret } = await import('./gateway/webhooks.js');
|
|
2788
|
+
if (!isValidWebhookRouteName(name)) {
|
|
2789
|
+
console.error('route ต้องเป็น a-z/A-Z/0-9/_/- ความยาวไม่เกิน 64 และต้องขึ้นต้นด้วยตัวอักษรหรือตัวเลข');
|
|
2790
|
+
process.exit(2);
|
|
2791
|
+
}
|
|
2792
|
+
const prompt = argValue(rest, '--prompt');
|
|
2793
|
+
const description = argValue(rest, '--description');
|
|
2794
|
+
const events = parseStringCsv(argValue(rest, '--events')).map((event) => event.trim()).filter(Boolean);
|
|
2795
|
+
const deliver = webhookDeliverTarget(rest);
|
|
2796
|
+
const deliverOnly = rest.includes('--deliver-only');
|
|
2797
|
+
const insecureNoAuth = rest.includes('--insecure-no-auth');
|
|
2798
|
+
const routeSecret = insecureNoAuth ? 'INSECURE_NO_AUTH' : (argValue(rest, '--secret')?.trim() || generateWebhookSecret());
|
|
2799
|
+
const publicUrl = argValue(rest, '--public-url');
|
|
2800
|
+
const rateLimitRaw = argValue(rest, '--rate-limit', '--rate-limit-per-minute');
|
|
2801
|
+
const rateLimitPerMinute = rateLimitRaw ? parsePort(rateLimitRaw, 30, 'webhook route rate limit') : undefined;
|
|
2802
|
+
if (deliverOnly && deliver === 'log') {
|
|
2803
|
+
console.error('--deliver-only ต้องมี --to หรือ --deliver เป็น messaging target จริง');
|
|
2804
|
+
process.exit(2);
|
|
2805
|
+
}
|
|
2806
|
+
if (deliver !== 'log') {
|
|
2807
|
+
const { parseSendTarget } = await import('./gateway/targets.js');
|
|
2808
|
+
try {
|
|
2809
|
+
parseSendTarget(deliver);
|
|
2810
|
+
}
|
|
2811
|
+
catch (e) {
|
|
2812
|
+
console.error(e.message);
|
|
2813
|
+
process.exit(2);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
const { patchGatewayConfig, readGatewayConfig } = await import('./gateway/config.js');
|
|
2817
|
+
const current = await readGatewayConfig();
|
|
2818
|
+
await patchGatewayConfig({
|
|
2819
|
+
webhooks: {
|
|
2820
|
+
enabled: true,
|
|
2821
|
+
publicUrl: publicUrl?.trim() || current.webhooks?.publicUrl,
|
|
2822
|
+
routes: {
|
|
2823
|
+
[name]: {
|
|
2824
|
+
events,
|
|
2825
|
+
secret: routeSecret,
|
|
2826
|
+
prompt: prompt?.trim() || undefined,
|
|
2827
|
+
deliver,
|
|
2828
|
+
deliverOnly,
|
|
2829
|
+
description: description?.trim() || undefined,
|
|
2830
|
+
rateLimitPerMinute,
|
|
2831
|
+
},
|
|
2832
|
+
},
|
|
2833
|
+
},
|
|
2834
|
+
});
|
|
2835
|
+
const base = (publicUrl?.trim() || current.webhooks?.publicUrl || 'http://127.0.0.1:8787').replace(/\/+$/, '');
|
|
2836
|
+
console.log(`เพิ่ม webhook route "${name}" แล้ว`);
|
|
2837
|
+
console.log(`URL: ${base}/webhooks/${name}`);
|
|
2838
|
+
console.log(`secret: ${routeSecret}`);
|
|
2839
|
+
console.log(`test: ${BRAND.cliName} webhook test ${name} --payload '{"event_type":"ping"}'`);
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
if (action === 'list' || action === undefined) {
|
|
2843
|
+
const { readGatewayConfig, resolveWebhookConfig } = await import('./gateway/config.js');
|
|
2844
|
+
const cfg = await readGatewayConfig();
|
|
2845
|
+
const webhooks = resolveWebhookConfig(cfg);
|
|
2846
|
+
const routes = Object.values(webhooks.routes);
|
|
2847
|
+
if (!routes.length) {
|
|
2848
|
+
console.log(`ยังไม่มี webhook route — เพิ่มด้วย: ${BRAND.cliName} webhook subscribe <route> --prompt "Event: {__raw__}"`);
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
const base = (webhooks.publicUrl || 'http://127.0.0.1:8787').replace(/\/+$/, '');
|
|
2852
|
+
for (const route of routes) {
|
|
2853
|
+
const events = route.events.length ? route.events.join(',') : '*';
|
|
2854
|
+
const mode = route.deliverOnly ? 'direct' : 'agent';
|
|
2855
|
+
console.log(`${route.name.padEnd(20)} ${mode.padEnd(6)} events:${events.padEnd(12)} deliver:${route.deliver} ${base}/webhooks/${route.name}`);
|
|
2856
|
+
}
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
if (action === 'remove' || action === 'rm') {
|
|
2860
|
+
const name = positionalArgs(rest, valueFlags)[0];
|
|
2861
|
+
if (!name) {
|
|
2862
|
+
console.error(`ใช้: ${BRAND.cliName} webhook remove <route>`);
|
|
2863
|
+
process.exit(2);
|
|
2864
|
+
}
|
|
2865
|
+
const { readGatewayConfig, writeGatewayConfig } = await import('./gateway/config.js');
|
|
2866
|
+
const cfg = await readGatewayConfig();
|
|
2867
|
+
if (!cfg.webhooks?.routes?.[name]) {
|
|
2868
|
+
console.log(`ไม่พบ webhook route "${name}"`);
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
const routes = { ...cfg.webhooks.routes };
|
|
2872
|
+
delete routes[name];
|
|
2873
|
+
await writeGatewayConfig({ ...cfg, webhooks: { ...cfg.webhooks, routes } });
|
|
2874
|
+
console.log(`ลบ webhook route "${name}" แล้ว`);
|
|
2875
|
+
return;
|
|
2876
|
+
}
|
|
2877
|
+
if (action === 'test') {
|
|
2878
|
+
const name = positionalArgs(rest, valueFlags)[0];
|
|
2879
|
+
if (!name) {
|
|
2880
|
+
console.error(`ใช้: ${BRAND.cliName} webhook test <route> [--payload <json>]`);
|
|
2881
|
+
process.exit(2);
|
|
2882
|
+
}
|
|
2883
|
+
const payload = argValue(rest, '--payload') ?? '{"event_type":"ping"}';
|
|
2884
|
+
let rawBody;
|
|
2885
|
+
let parsedPayload;
|
|
2886
|
+
try {
|
|
2887
|
+
parsedPayload = JSON.parse(payload);
|
|
2888
|
+
rawBody = JSON.stringify(parsedPayload);
|
|
2889
|
+
}
|
|
2890
|
+
catch {
|
|
2891
|
+
console.error('--payload ต้องเป็น JSON object/string ที่ parse ได้');
|
|
2892
|
+
process.exit(2);
|
|
2893
|
+
}
|
|
2894
|
+
const { readGatewayConfig, resolveWebhookConfig } = await import('./gateway/config.js');
|
|
2895
|
+
const { handleWebhookRequest } = await import('./gateway/webhooks.js');
|
|
2896
|
+
const cfg = resolveWebhookConfig(await readGatewayConfig());
|
|
2897
|
+
const route = cfg.routes[name];
|
|
2898
|
+
if (!route) {
|
|
2899
|
+
console.error(`ไม่พบ webhook route "${name}"`);
|
|
2900
|
+
process.exit(2);
|
|
2901
|
+
}
|
|
2902
|
+
const secret = route.secret || cfg.secret;
|
|
2903
|
+
const eventType = parsedPayload && typeof parsedPayload === 'object' && typeof parsedPayload.event_type === 'string'
|
|
2904
|
+
? parsedPayload.event_type
|
|
2905
|
+
: 'ping';
|
|
2906
|
+
const headers = { 'x-event-type': eventType };
|
|
2907
|
+
if (secret && secret !== 'INSECURE_NO_AUTH') {
|
|
2908
|
+
const { createHmac } = await import('node:crypto');
|
|
2909
|
+
headers['x-webhook-signature'] = createHmac('sha256', secret).update(rawBody).digest('hex');
|
|
2910
|
+
headers['x-request-id'] = `sanook-test-${Date.now()}`;
|
|
2911
|
+
}
|
|
2912
|
+
const appCfg = await loadConfig({});
|
|
2913
|
+
const result = await handleWebhookRequest({
|
|
2914
|
+
routeName: name,
|
|
2915
|
+
rawBody,
|
|
2916
|
+
headers,
|
|
2917
|
+
config: cfg,
|
|
2918
|
+
model: appCfg.model,
|
|
2919
|
+
budgetUsd: appCfg.budgetUsd,
|
|
2920
|
+
permissionMode: appCfg.permissionMode,
|
|
2921
|
+
onLog: (m) => process.stderr.write(`${DIM}${m}${RESET}\n`),
|
|
2922
|
+
});
|
|
2923
|
+
console.log(JSON.stringify(result.body, null, 2));
|
|
2924
|
+
if (result.status >= 400)
|
|
2925
|
+
process.exit(1);
|
|
2926
|
+
return;
|
|
2927
|
+
}
|
|
2928
|
+
console.error(`ไม่รู้จัก: webhook ${action} — ใช้ subscribe / list / remove / test`);
|
|
2929
|
+
process.exit(2);
|
|
2930
|
+
}
|
|
2931
|
+
function webhookDeliverTarget(args) {
|
|
2932
|
+
const direct = argValue(args, '--to', '-t')?.trim();
|
|
2933
|
+
if (direct)
|
|
2934
|
+
return direct;
|
|
2935
|
+
const deliver = argValue(args, '--deliver')?.trim();
|
|
2936
|
+
if (!deliver || deliver === 'log')
|
|
2937
|
+
return 'log';
|
|
2938
|
+
const chat = argValue(args, '--deliver-chat-id', '--chat-id')?.trim();
|
|
2939
|
+
return chat ? `${deliver}:${chat}` : deliver;
|
|
198
2940
|
}
|
|
199
2941
|
/** sanook cron add "<when>" "<task>" | cron list | cron rm <id> */
|
|
200
2942
|
async function runCron(args) {
|
|
201
2943
|
const [action, ...rest] = args;
|
|
202
2944
|
const { listTasks, enqueueTask, removeTask } = await import('./gateway/ledger.js');
|
|
2945
|
+
const valueFlags = ['--to', '-t', '--model', '-m'];
|
|
203
2946
|
if (action === 'add') {
|
|
204
|
-
const
|
|
205
|
-
const
|
|
2947
|
+
const deliverRaw = argValue(rest, '--to', '-t')?.trim();
|
|
2948
|
+
const model = argValue(rest, '--model', '-m');
|
|
2949
|
+
const positionals = positionalArgs(rest, valueFlags);
|
|
2950
|
+
const schedule = positionals[0];
|
|
2951
|
+
const spec = positionals.slice(1).join(' ').trim();
|
|
206
2952
|
if (!schedule || !spec) {
|
|
207
|
-
console.error(
|
|
2953
|
+
console.error(`ใช้: ${BRAND.cliName} cron add "<when>" "<task>" [--to <target>] [--model <provider:model>]`);
|
|
208
2954
|
console.error('หมายเหตุ: when ที่มีช่องว่างต้องครอบ quote เช่น "every 30m"');
|
|
209
2955
|
process.exit(1);
|
|
210
2956
|
}
|
|
@@ -217,14 +2963,30 @@ async function runCron(args) {
|
|
|
217
2963
|
}
|
|
218
2964
|
process.exit(1);
|
|
219
2965
|
}
|
|
2966
|
+
let deliver;
|
|
2967
|
+
if (deliverRaw) {
|
|
2968
|
+
const { parseSendTarget, formatTarget } = await import('./gateway/targets.js');
|
|
2969
|
+
try {
|
|
2970
|
+
deliver = formatTarget(parseSendTarget(deliverRaw));
|
|
2971
|
+
}
|
|
2972
|
+
catch (e) {
|
|
2973
|
+
console.error(e.message);
|
|
2974
|
+
process.exit(2);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
220
2977
|
const task = await enqueueTask({
|
|
221
2978
|
kind: sched.recurring ? 'cron' : 'once',
|
|
222
2979
|
spec,
|
|
223
2980
|
schedule: sched.recurring ? sched.normalized : undefined,
|
|
2981
|
+
model,
|
|
2982
|
+
deliver,
|
|
224
2983
|
runAt: sched.runAt,
|
|
225
2984
|
});
|
|
226
2985
|
const when = new Date(task.runAt).toLocaleString();
|
|
227
|
-
|
|
2986
|
+
const extras = [task.deliver ? `ส่งไป ${task.deliver}` : undefined, task.model ? `model ${task.model}` : undefined]
|
|
2987
|
+
.filter(Boolean)
|
|
2988
|
+
.join(' · ');
|
|
2989
|
+
console.log(`เพิ่ม task ${task.id} — รัน ${when}${sched.recurring ? ` แล้วทุก ${sched.normalized}` : ''}${extras ? ` · ${extras}` : ''}`);
|
|
228
2990
|
return;
|
|
229
2991
|
}
|
|
230
2992
|
if (action === 'rm' || action === 'remove') {
|
|
@@ -244,26 +3006,39 @@ async function runCron(args) {
|
|
|
244
3006
|
}
|
|
245
3007
|
for (const t of tasks) {
|
|
246
3008
|
const next = new Date(t.runAt).toLocaleString();
|
|
247
|
-
|
|
3009
|
+
const extras = [t.deliver ? `to:${t.deliver}` : undefined, t.model ? `model:${t.model}` : undefined].filter(Boolean).join(' ');
|
|
3010
|
+
console.log(`${t.id} [${t.status}] ${t.schedule ?? 'once'} next:${next}${extras ? ` ${extras}` : ''} → ${t.spec.slice(0, 50)}`);
|
|
248
3011
|
}
|
|
249
3012
|
return;
|
|
250
3013
|
}
|
|
251
3014
|
console.error(`ไม่รู้จัก: cron ${action} — ใช้ add / list / rm`);
|
|
252
3015
|
process.exit(1);
|
|
253
3016
|
}
|
|
254
|
-
/** sanook
|
|
3017
|
+
/** sanook init [--trust] — scaffold project .sanook/commands + onboarding hints */
|
|
3018
|
+
async function runInit(args) {
|
|
3019
|
+
const trust = args.includes('--trust');
|
|
3020
|
+
const unknown = args.filter((a) => a !== '--trust');
|
|
3021
|
+
if (unknown.length) {
|
|
3022
|
+
console.error(`ใช้: ${BRAND.cliName} init [--trust]`);
|
|
3023
|
+
process.exit(1);
|
|
3024
|
+
}
|
|
3025
|
+
const { initProject, formatInitResult } = await import('./project-init.js');
|
|
3026
|
+
const result = await initProject({ trust });
|
|
3027
|
+
console.log(formatInitResult(result));
|
|
3028
|
+
}
|
|
3029
|
+
/** sanook skill list | install <name|path> | add <source> | remove <name> */
|
|
255
3030
|
async function runSkill(args) {
|
|
256
3031
|
const [action, ...rest] = args;
|
|
257
|
-
if (action === 'add') {
|
|
3032
|
+
if (action === 'add' || action === 'install') {
|
|
258
3033
|
const source = rest[0];
|
|
259
3034
|
if (!source) {
|
|
260
|
-
console.error(
|
|
3035
|
+
console.error(`ใช้: ${BRAND.cliName} skill install <bundled-name|path> หรือ ${BRAND.cliName} skill add <github "user/repo" | URL ของ SKILL.md | local path>`);
|
|
261
3036
|
process.exit(1);
|
|
262
3037
|
}
|
|
263
3038
|
console.error(`${DIM}⚠ skill = instruction ที่ AI จะทำตาม — ติดตั้งจาก source ที่เชื่อถือเท่านั้น${RESET}`);
|
|
264
|
-
const {
|
|
3039
|
+
const { installNamedSkill } = await import('./skill-install.js');
|
|
265
3040
|
try {
|
|
266
|
-
const installed = await
|
|
3041
|
+
const installed = await installNamedSkill(source, (m) => process.stderr.write(`${DIM}${m}${RESET}\n`));
|
|
267
3042
|
console.log(`ติดตั้ง ${installed.length} skill: ${installed.map((s) => s.name).join(', ')}`);
|
|
268
3043
|
}
|
|
269
3044
|
catch (e) {
|
|
@@ -333,10 +3108,271 @@ async function runModels(args) {
|
|
|
333
3108
|
else
|
|
334
3109
|
console.log('\n✓ ทุก curated id มีใน provider');
|
|
335
3110
|
}
|
|
3111
|
+
function brainDoctorStatusLabel(status) {
|
|
3112
|
+
return status.toUpperCase().padEnd(4);
|
|
3113
|
+
}
|
|
3114
|
+
/** sanook brain doctor — check configured second-brain health without modifying it */
|
|
3115
|
+
async function runBrainDoctor() {
|
|
3116
|
+
const cfg = await loadConfig({});
|
|
3117
|
+
const { checkBrain } = await import('./brain-doctor.js');
|
|
3118
|
+
const report = await checkBrain({ brainPath: cfg.brainPath });
|
|
3119
|
+
console.log(`${BRAND.productName} brain doctor`);
|
|
3120
|
+
for (const check of report.checks) {
|
|
3121
|
+
console.log(`[${brainDoctorStatusLabel(check.status)}] ${check.id} — ${check.message}`);
|
|
3122
|
+
if (check.path)
|
|
3123
|
+
console.log(` ${check.path}`);
|
|
3124
|
+
for (const detail of check.details ?? [])
|
|
3125
|
+
console.log(` - ${detail}`);
|
|
3126
|
+
}
|
|
3127
|
+
if (!report.ok)
|
|
3128
|
+
process.exit(1);
|
|
3129
|
+
}
|
|
3130
|
+
/** sanook brain context [--task "..."] — show the prompt context Sanook loads from the vault */
|
|
3131
|
+
async function runBrainContext(args) {
|
|
3132
|
+
const { parseBrainContextArgs, inspectBrainContext, formatBrainContextReport } = await import('./brain-context.js');
|
|
3133
|
+
const parsed = parseBrainContextArgs(args);
|
|
3134
|
+
if (!parsed.ok) {
|
|
3135
|
+
console.error(parsed.message);
|
|
3136
|
+
console.error(`ใช้: ${BRAND.cliName} brain context [--task "..."] [--project <slug>] [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,session,skill] [--no-content]`);
|
|
3137
|
+
process.exit(1);
|
|
3138
|
+
}
|
|
3139
|
+
const cfg = await loadConfig({});
|
|
3140
|
+
const report = await inspectBrainContext({
|
|
3141
|
+
brainPath: cfg.brainPath,
|
|
3142
|
+
task: parsed.value.task,
|
|
3143
|
+
cwd: process.cwd(),
|
|
3144
|
+
projectSlug: parsed.value.project,
|
|
3145
|
+
mode: parsed.value.mode,
|
|
3146
|
+
limit: parsed.value.limit,
|
|
3147
|
+
sources: parsed.value.sources,
|
|
3148
|
+
});
|
|
3149
|
+
console.log(formatBrainContextReport(report, parsed.value.showContent));
|
|
3150
|
+
if (!report.ok)
|
|
3151
|
+
process.exit(1);
|
|
3152
|
+
}
|
|
3153
|
+
/** sanook brain eval — run the lightweight second-brain benchmark sanity checks */
|
|
3154
|
+
async function runBrainEval(args) {
|
|
3155
|
+
const allowed = new Set(['--no-retrieval']);
|
|
3156
|
+
const unknown = args.filter((arg) => !allowed.has(arg));
|
|
3157
|
+
if (unknown.length) {
|
|
3158
|
+
console.error(`ไม่รู้จัก option: ${unknown.join(' ')}`);
|
|
3159
|
+
console.error(`ใช้: ${BRAND.cliName} brain eval [--no-retrieval]`);
|
|
3160
|
+
process.exit(1);
|
|
3161
|
+
}
|
|
3162
|
+
const cfg = await loadConfig({});
|
|
3163
|
+
const { runBrainEval: evaluate, formatBrainEvalReport } = await import('./brain-eval.js');
|
|
3164
|
+
const report = await evaluate({ brainPath: cfg.brainPath, runRetrieval: !args.includes('--no-retrieval') });
|
|
3165
|
+
console.log(formatBrainEvalReport(report));
|
|
3166
|
+
if (!report.ok)
|
|
3167
|
+
process.exit(1);
|
|
3168
|
+
}
|
|
3169
|
+
/** sanook brain review — curator health review over the configured second-brain vault */
|
|
3170
|
+
async function runBrainReview(args) {
|
|
3171
|
+
const { parseBrainReviewArgs, reviewBrain, formatBrainReviewReport } = await import('./brain-review.js');
|
|
3172
|
+
const parsed = parseBrainReviewArgs(args);
|
|
3173
|
+
if (!parsed.ok) {
|
|
3174
|
+
console.error(parsed.message);
|
|
3175
|
+
console.error(`ใช้: ${BRAND.cliName} brain review [--no-hygiene]`);
|
|
3176
|
+
process.exit(1);
|
|
3177
|
+
}
|
|
3178
|
+
const cfg = await loadConfig({});
|
|
3179
|
+
const report = await reviewBrain({
|
|
3180
|
+
brainPath: cfg.brainPath,
|
|
3181
|
+
scanMarkdownHygiene: parsed.value.scanMarkdownHygiene,
|
|
3182
|
+
});
|
|
3183
|
+
console.log(formatBrainReviewReport(report));
|
|
3184
|
+
if (!report.ok)
|
|
3185
|
+
process.exit(1);
|
|
3186
|
+
}
|
|
3187
|
+
/** sanook brain consolidate [--apply] [--archive] [--memory] — sleep-time consolidation runner */
|
|
3188
|
+
async function runBrainConsolidate(args) {
|
|
3189
|
+
const { parseBrainConsolidateArgs, runBrainConsolidate: consolidate, formatBrainConsolidateReport } = await import('./brain-consolidate.js');
|
|
3190
|
+
const parsed = parseBrainConsolidateArgs(args);
|
|
3191
|
+
if (!parsed.ok) {
|
|
3192
|
+
console.error(parsed.message);
|
|
3193
|
+
console.error(`ใช้: ${BRAND.cliName} brain consolidate [--apply] [--apply --archive] [--memory] [--no-retrieval]`);
|
|
3194
|
+
process.exit(1);
|
|
3195
|
+
}
|
|
3196
|
+
const cfg = await loadConfig({});
|
|
3197
|
+
const report = await consolidate({
|
|
3198
|
+
brainPath: cfg.brainPath,
|
|
3199
|
+
apply: parsed.value.apply,
|
|
3200
|
+
archive: parsed.value.archive,
|
|
3201
|
+
memory: parsed.value.memory,
|
|
3202
|
+
runRetrieval: parsed.value.runRetrieval,
|
|
3203
|
+
});
|
|
3204
|
+
console.log(formatBrainConsolidateReport(report));
|
|
3205
|
+
if (!report.ok)
|
|
3206
|
+
process.exit(1);
|
|
3207
|
+
}
|
|
3208
|
+
/** sanook brain metrics [--no-retrieval] — vault counts, stale notes, index + retrieval coverage */
|
|
3209
|
+
async function runBrainMetrics(args) {
|
|
3210
|
+
const allowed = new Set(['--no-retrieval']);
|
|
3211
|
+
const unknown = args.filter((arg) => !allowed.has(arg));
|
|
3212
|
+
if (unknown.length) {
|
|
3213
|
+
console.error(`ไม่รู้จัก option: ${unknown.join(' ')}`);
|
|
3214
|
+
console.error(`ใช้: ${BRAND.cliName} brain metrics [--no-retrieval]`);
|
|
3215
|
+
process.exit(1);
|
|
3216
|
+
}
|
|
3217
|
+
const cfg = await loadConfig({});
|
|
3218
|
+
const { collectBrainMetrics, formatBrainMetricsReport } = await import('./brain-metrics.js');
|
|
3219
|
+
const report = await collectBrainMetrics({ brainPath: cfg.brainPath, runRetrievalEval: !args.includes('--no-retrieval') });
|
|
3220
|
+
console.log(formatBrainMetricsReport(report));
|
|
3221
|
+
if (!report.ok)
|
|
3222
|
+
process.exit(1);
|
|
3223
|
+
}
|
|
3224
|
+
/** sanook brain projects list — show vault project workspaces and repo_path mappings */
|
|
3225
|
+
async function runBrainProjects(args) {
|
|
3226
|
+
if (args[0] && args[0] !== 'list') {
|
|
3227
|
+
console.error(`ใช้: ${BRAND.cliName} brain projects list`);
|
|
3228
|
+
process.exit(1);
|
|
3229
|
+
}
|
|
3230
|
+
const cfg = await loadConfig({});
|
|
3231
|
+
if (!cfg.brainPath) {
|
|
3232
|
+
console.error('ยังไม่ได้ตั้ง brainPath — รัน `sanook brain init [path]` ก่อน');
|
|
3233
|
+
process.exit(1);
|
|
3234
|
+
}
|
|
3235
|
+
const { listVaultProjects, formatVaultProjectLine, resolveVaultProject } = await import('./project-registry.js');
|
|
3236
|
+
const projects = await listVaultProjects(cfg.brainPath);
|
|
3237
|
+
const active = await resolveVaultProject({ brainPath: cfg.brainPath, cwd: process.cwd() });
|
|
3238
|
+
console.log(`${BRAND.productName} brain projects`);
|
|
3239
|
+
console.log(`vault: ${cfg.brainPath}`);
|
|
3240
|
+
console.log(`cwd: ${process.cwd()}${active ? ` → active: ${active.slug}` : ''}`);
|
|
3241
|
+
if (!projects.length) {
|
|
3242
|
+
console.log('(no project workspaces — run `sanook brain new project --title "..." --repo /path`)');
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
console.log('\nslug repo_path');
|
|
3246
|
+
for (const project of projects) {
|
|
3247
|
+
const marker = active?.slug === project.slug ? ' *' : '';
|
|
3248
|
+
console.log(`${formatVaultProjectLine(project)}${marker}`);
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
/** sanook brain pack list|show <name> — inspect Shared/Context-Packs/ bundles */
|
|
3252
|
+
async function runBrainPack(args) {
|
|
3253
|
+
const { parseBrainPackArgs, listContextPacks, showContextPack, formatBrainPackListReport, formatBrainPackShowReport } = await import('./brain-pack.js');
|
|
3254
|
+
const parsed = parseBrainPackArgs(args);
|
|
3255
|
+
if (!parsed.ok) {
|
|
3256
|
+
console.error(parsed.message);
|
|
3257
|
+
console.error(`ใช้: ${BRAND.cliName} brain pack list | ${BRAND.cliName} brain pack show <name>`);
|
|
3258
|
+
process.exit(1);
|
|
3259
|
+
}
|
|
3260
|
+
const cfg = await loadConfig({});
|
|
3261
|
+
if (!cfg.brainPath) {
|
|
3262
|
+
console.error('ยังไม่ได้ตั้ง brainPath — รัน `sanook brain init [path]` ก่อน');
|
|
3263
|
+
process.exit(1);
|
|
3264
|
+
}
|
|
3265
|
+
if (parsed.action === 'list') {
|
|
3266
|
+
const report = await listContextPacks(cfg.brainPath);
|
|
3267
|
+
console.log(formatBrainPackListReport(report));
|
|
3268
|
+
if (!report.ok)
|
|
3269
|
+
process.exit(1);
|
|
3270
|
+
return;
|
|
3271
|
+
}
|
|
3272
|
+
const report = await showContextPack(cfg.brainPath, parsed.name);
|
|
3273
|
+
console.log(formatBrainPackShowReport(report));
|
|
3274
|
+
if (!report.ok)
|
|
3275
|
+
process.exit(1);
|
|
3276
|
+
}
|
|
3277
|
+
/** sanook brain new <type> [--title "..."] — template-backed note creation in the correct vault folder */
|
|
3278
|
+
async function runBrainNew(args) {
|
|
3279
|
+
const { parseBrainNewArgs, createBrainNote, formatBrainNewReport } = await import('./brain-new.js');
|
|
3280
|
+
const parsed = parseBrainNewArgs(args);
|
|
3281
|
+
if (!parsed.ok) {
|
|
3282
|
+
console.error(parsed.message);
|
|
3283
|
+
console.error(`ใช้: ${BRAND.cliName} brain new <session|bug|handoff|project|golden-case|checklist> [--title "..."] [--repo /path] [--verify "..."] [--output path] [--force]`);
|
|
3284
|
+
process.exit(1);
|
|
3285
|
+
}
|
|
3286
|
+
const cfg = await loadConfig({});
|
|
3287
|
+
const report = await createBrainNote({
|
|
3288
|
+
brainPath: cfg.brainPath,
|
|
3289
|
+
today: new Date().toISOString().slice(0, 10),
|
|
3290
|
+
...parsed.value,
|
|
3291
|
+
});
|
|
3292
|
+
console.log(formatBrainNewReport(report));
|
|
3293
|
+
if (!report.ok)
|
|
3294
|
+
process.exit(1);
|
|
3295
|
+
}
|
|
3296
|
+
/** sanook brain repair [--dry-run] — apply safe one-line fixes after doctor/review findings */
|
|
3297
|
+
async function runBrainRepair(args) {
|
|
3298
|
+
const { parseBrainRepairArgs, repairBrain, formatBrainRepairReport } = await import('./brain-repair.js');
|
|
3299
|
+
const parsed = parseBrainRepairArgs(args);
|
|
3300
|
+
if (!parsed.ok) {
|
|
3301
|
+
console.error(parsed.message);
|
|
3302
|
+
console.error(`ใช้: ${BRAND.cliName} brain repair [--dry-run]`);
|
|
3303
|
+
process.exit(1);
|
|
3304
|
+
}
|
|
3305
|
+
const cfg = await loadConfig({});
|
|
3306
|
+
const report = await repairBrain({ brainPath: cfg.brainPath, dryRun: parsed.dryRun });
|
|
3307
|
+
console.log(formatBrainRepairReport(report));
|
|
3308
|
+
if (!report.ok)
|
|
3309
|
+
process.exit(1);
|
|
3310
|
+
}
|
|
3311
|
+
/** sanook brain final [--task "..."] [--from-diff] [--lite] — create an evidence-backed closeout note */
|
|
3312
|
+
async function runBrainFinal(args) {
|
|
3313
|
+
const { parseBrainFinalArgs, createBrainFinal, formatBrainFinalReport } = await import('./brain-final.js');
|
|
3314
|
+
const parsed = parseBrainFinalArgs(args);
|
|
3315
|
+
if (!parsed.ok) {
|
|
3316
|
+
console.error(parsed.message);
|
|
3317
|
+
console.error(`ใช้: ${BRAND.cliName} brain final [--task "..."] [--from-diff] [--lite] [--output Sessions/name.md] [--force]`);
|
|
3318
|
+
process.exit(1);
|
|
3319
|
+
}
|
|
3320
|
+
const cfg = await loadConfig({});
|
|
3321
|
+
const report = await createBrainFinal({
|
|
3322
|
+
brainPath: cfg.brainPath,
|
|
3323
|
+
today: new Date().toISOString().slice(0, 10),
|
|
3324
|
+
...parsed.value,
|
|
3325
|
+
});
|
|
3326
|
+
console.log(formatBrainFinalReport(report));
|
|
3327
|
+
if (!report.ok)
|
|
3328
|
+
process.exit(1);
|
|
3329
|
+
}
|
|
336
3330
|
/** sanook brain init [path] — scaffold second-brain workspace (interactive ถ้าไม่ใส่ path) */
|
|
337
3331
|
async function runBrain(args) {
|
|
3332
|
+
if (args[0] === 'doctor')
|
|
3333
|
+
return runBrainDoctor();
|
|
3334
|
+
if (args[0] === 'context')
|
|
3335
|
+
return runBrainContext(args.slice(1));
|
|
3336
|
+
if (args[0] === 'eval')
|
|
3337
|
+
return runBrainEval(args.slice(1));
|
|
3338
|
+
if (args[0] === 'review')
|
|
3339
|
+
return runBrainReview(args.slice(1));
|
|
3340
|
+
if (args[0] === 'projects')
|
|
3341
|
+
return runBrainProjects(args.slice(1));
|
|
3342
|
+
if (args[0] === 'pack')
|
|
3343
|
+
return runBrainPack(args.slice(1));
|
|
3344
|
+
if (args[0] === 'new')
|
|
3345
|
+
return runBrainNew(args.slice(1));
|
|
3346
|
+
if (args[0] === 'repair')
|
|
3347
|
+
return runBrainRepair(args.slice(1));
|
|
3348
|
+
if (args[0] === 'consolidate')
|
|
3349
|
+
return runBrainConsolidate(args.slice(1));
|
|
3350
|
+
if (args[0] === 'metrics')
|
|
3351
|
+
return runBrainMetrics(args.slice(1));
|
|
3352
|
+
if (args[0] === 'final')
|
|
3353
|
+
return runBrainFinal(args.slice(1));
|
|
338
3354
|
if (args[0] !== 'init') {
|
|
339
|
-
console.log(`ใช้:
|
|
3355
|
+
console.log(`ใช้:
|
|
3356
|
+
sanook brain init [path] สร้างโครงสร้าง second-brain (Obsidian vault)
|
|
3357
|
+
sanook brain doctor ตรวจ health ของ second-brain ที่ config.brainPath
|
|
3358
|
+
sanook brain context แสดง context ที่ Sanook จะ inject (project auto-detect จาก cwd)
|
|
3359
|
+
sanook brain context --task "..." ดู retrieval hits ต่อ task
|
|
3360
|
+
sanook brain context --project <slug> บังคับ project workspace
|
|
3361
|
+
sanook brain projects list แสดง Projects/<slug>/ + repo_path
|
|
3362
|
+
sanook brain eval รัน second-brain benchmark sanity checks
|
|
3363
|
+
sanook brain review curator review: inbox, packs, sessions, evals, note hygiene
|
|
3364
|
+
sanook brain pack list แสดง context packs ใน Shared/Context-Packs/
|
|
3365
|
+
sanook brain pack show <name> แสดง load order / done criteria ของ pack
|
|
3366
|
+
sanook brain new project --title "..." --repo /path [--verify "..."]
|
|
3367
|
+
sanook brain new <type> [--title "..."] สร้างโน้ตจาก template
|
|
3368
|
+
sanook brain repair [--dry-run] แก้ one-line fixes หลัง doctor/review
|
|
3369
|
+
sanook brain consolidate [--apply] [--apply --archive] [--memory]
|
|
3370
|
+
sleep-time consolidation (dry-run default)
|
|
3371
|
+
sanook brain metrics [--no-retrieval]
|
|
3372
|
+
vault counts, stale notes, index freshness, retrieval coverage
|
|
3373
|
+
sanook brain final --task "..." [--from-diff] [--lite]
|
|
3374
|
+
สร้าง final gate note ใน Sessions
|
|
3375
|
+
|
|
340
3376
|
ไม่ใส่ path → wizard ถาม path + ตัวตน
|
|
341
3377
|
-y, --yes ใช้ค่า default ทั้งหมด (ต้องระบุ path)`);
|
|
342
3378
|
return;
|
|
@@ -351,7 +3387,9 @@ async function runBrain(args) {
|
|
|
351
3387
|
return;
|
|
352
3388
|
}
|
|
353
3389
|
const { scaffoldBrain, BRAIN_DEFAULTS, expandHome, wireBrainMcp } = await import('./brain.js');
|
|
354
|
-
|
|
3390
|
+
// resolve to absolute before persisting — getBrainPath() is later read from an arbitrary cwd,
|
|
3391
|
+
// so a relative path (e.g. "./vault") would resolve differently per run
|
|
3392
|
+
const target = resolve(expandHome(pathArg ?? join(homedir(), 'Documents', BRAIN_DEFAULTS.vaultName)));
|
|
355
3393
|
const today = new Date().toISOString().slice(0, 10);
|
|
356
3394
|
try {
|
|
357
3395
|
const res = await scaffoldBrain(target, { ...BRAIN_DEFAULTS, today });
|
|
@@ -379,7 +3417,22 @@ async function readStdin() {
|
|
|
379
3417
|
async function runConfig(args) {
|
|
380
3418
|
const { readGlobalConfigRaw, patchGlobalConfig } = await import('./config.js');
|
|
381
3419
|
const [action, key, ...rest] = args;
|
|
382
|
-
const ALLOWED = [
|
|
3420
|
+
const ALLOWED = [
|
|
3421
|
+
'model',
|
|
3422
|
+
'fallbackModel',
|
|
3423
|
+
'budgetUsd',
|
|
3424
|
+
'maxSteps',
|
|
3425
|
+
'permissionMode',
|
|
3426
|
+
'brainPath',
|
|
3427
|
+
'pricing',
|
|
3428
|
+
'cacheTtl',
|
|
3429
|
+
'compaction',
|
|
3430
|
+
'contextCompression',
|
|
3431
|
+
'thinking',
|
|
3432
|
+
'summaryModel',
|
|
3433
|
+
'embeddingModel',
|
|
3434
|
+
'personality',
|
|
3435
|
+
];
|
|
383
3436
|
if (action === 'set') {
|
|
384
3437
|
if (!key || rest.length === 0) {
|
|
385
3438
|
console.error(`ใช้: ${BRAND.cliName} config set <key> <value> (key: ${ALLOWED.join(' | ')})`);
|
|
@@ -392,8 +3445,8 @@ async function runConfig(args) {
|
|
|
392
3445
|
const raw = rest.join(' ');
|
|
393
3446
|
let value = raw;
|
|
394
3447
|
if (key === 'budgetUsd') {
|
|
395
|
-
const n =
|
|
396
|
-
if (
|
|
3448
|
+
const n = parseBudgetUsd(raw);
|
|
3449
|
+
if (n === undefined) {
|
|
397
3450
|
console.error('budgetUsd ต้องเป็นตัวเลขบวก เช่น 0.25');
|
|
398
3451
|
process.exit(1);
|
|
399
3452
|
}
|
|
@@ -419,20 +3472,31 @@ async function runConfig(args) {
|
|
|
419
3472
|
console.error('compaction ต้องเป็น truncate หรือ summarize');
|
|
420
3473
|
process.exit(1);
|
|
421
3474
|
}
|
|
3475
|
+
else if (key === 'contextCompression' && raw !== 'off' && raw !== 'selective' && raw !== 'headroom') {
|
|
3476
|
+
console.error('contextCompression ต้องเป็น off, selective หรือ headroom');
|
|
3477
|
+
process.exit(1);
|
|
3478
|
+
}
|
|
3479
|
+
else if (key === 'brainPath') {
|
|
3480
|
+
// store absolute — getBrainPath() is read from arbitrary cwd, so a relative path drifts
|
|
3481
|
+
const { expandHome } = await import('./brain.js');
|
|
3482
|
+
value = resolve(expandHome(raw.trim()));
|
|
3483
|
+
}
|
|
422
3484
|
else if (key === 'thinking') {
|
|
423
3485
|
// เก็บเป็น number (budget) หรือ boolean ให้ตรง ConfigSchema (ไม่เก็บ string)
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
3486
|
+
value = parseThinkingConfigValue(raw);
|
|
3487
|
+
if (value === undefined) {
|
|
3488
|
+
console.error('thinking ต้องเป็น on/off, true/false, yes/no หรือ budget tokens (integer บวก เช่น 4000)');
|
|
3489
|
+
process.exit(1);
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
else if (key === 'personality') {
|
|
3493
|
+
const { normalizePersonalityName, personalityListText } = await import('./personality.js');
|
|
3494
|
+
const name = normalizePersonalityName(raw);
|
|
3495
|
+
if (!name) {
|
|
3496
|
+
console.error(`personality ไม่รู้จัก: ${raw}\n${personalityListText()}`);
|
|
3497
|
+
process.exit(1);
|
|
435
3498
|
}
|
|
3499
|
+
value = name === 'none' ? undefined : name;
|
|
436
3500
|
}
|
|
437
3501
|
else if (key === 'pricing') {
|
|
438
3502
|
try {
|
|
@@ -459,33 +3523,24 @@ async function runIndex(_args) {
|
|
|
459
3523
|
const { reindex } = await import('./search/indexer.js');
|
|
460
3524
|
console.log('indexing…');
|
|
461
3525
|
const r = await reindex();
|
|
3526
|
+
const { resetSearchCaches } = await import('./search/engine.js');
|
|
3527
|
+
resetSearchCaches();
|
|
462
3528
|
console.log(`done: +${r.added} ~${r.updated} -${r.removed} (skipped ${r.skipped}) · ` +
|
|
463
|
-
`memory=${r.memory} sessions=${r.sessions} skills=${r.skills}
|
|
3529
|
+
`memory=${r.memory} sessions=${r.sessions} skills=${r.skills} vectors=${r.vectors}\n` +
|
|
3530
|
+
`vault: ${r.vaultPath ?? '(not set — `' + BRAND.cliName + ' brain init` or set config.brainPath)'}`);
|
|
464
3531
|
}
|
|
465
3532
|
/** sanook search "<query>" [--mode ..] [--limit N] [--source a,b] — one-shot ranked search */
|
|
466
3533
|
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) {
|
|
3534
|
+
const { parseSearchArgs } = await import('./search/cli.js');
|
|
3535
|
+
const parsed = parseSearchArgs(args);
|
|
3536
|
+
if (!parsed.ok) {
|
|
3537
|
+
console.error(parsed.message);
|
|
484
3538
|
console.error(`ใช้: ${BRAND.cliName} search "<query>" [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,memory]`);
|
|
485
3539
|
process.exit(1);
|
|
486
3540
|
}
|
|
3541
|
+
const { query, mode, limit, sources } = parsed.value;
|
|
487
3542
|
const { search } = await import('./search/engine.js');
|
|
488
|
-
const res = await search(query, { mode
|
|
3543
|
+
const res = await search(query, { mode, limit, sources });
|
|
489
3544
|
if (res.degraded)
|
|
490
3545
|
console.log(`${DIM}(mode=${res.mode}, degraded: ${res.degraded})${RESET}`);
|
|
491
3546
|
else
|
|
@@ -506,24 +3561,213 @@ async function runMcpServe() {
|
|
|
506
3561
|
const { runMcpServer } = await import('./mcp-server.js');
|
|
507
3562
|
await runMcpServer();
|
|
508
3563
|
}
|
|
509
|
-
/** sanook mcp [list
|
|
3564
|
+
/** sanook mcp [search|info|install|test|doctor|enable|disable|list|add|remove] — จัดการ ~/.sanook/mcp.json */
|
|
510
3565
|
async function runMcp(args) {
|
|
3566
|
+
const readConfig = async (path) => {
|
|
3567
|
+
try {
|
|
3568
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
3569
|
+
return { mcpServers: parsed.mcpServers ?? {} };
|
|
3570
|
+
}
|
|
3571
|
+
catch {
|
|
3572
|
+
return { mcpServers: {} };
|
|
3573
|
+
}
|
|
3574
|
+
};
|
|
3575
|
+
const writeConfig = async (path, cfg) => {
|
|
3576
|
+
await mkdir(dirname(path), { recursive: true });
|
|
3577
|
+
await writeFile(path, `${JSON.stringify(cfg, null, 2)}\n`, { mode: 0o600 });
|
|
3578
|
+
await chmod(path, 0o600).catch(() => { });
|
|
3579
|
+
};
|
|
511
3580
|
const mcpPath = appHomePath('mcp.json');
|
|
512
|
-
let cfg =
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
3581
|
+
let cfg = await readConfig(mcpPath);
|
|
3582
|
+
const [action = 'list', ...rest] = args;
|
|
3583
|
+
const positionals = (items, valueFlags = new Set()) => {
|
|
3584
|
+
const out = [];
|
|
3585
|
+
for (let i = 0; i < items.length; i++) {
|
|
3586
|
+
const item = items[i];
|
|
3587
|
+
if (!item.startsWith('--')) {
|
|
3588
|
+
out.push(item);
|
|
3589
|
+
continue;
|
|
3590
|
+
}
|
|
3591
|
+
const flag = item.includes('=') ? item.slice(0, item.indexOf('=')) : item;
|
|
3592
|
+
if (!item.includes('=') && valueFlags.has(flag) && items[i + 1])
|
|
3593
|
+
i++;
|
|
3594
|
+
}
|
|
3595
|
+
return out;
|
|
3596
|
+
};
|
|
3597
|
+
if (action === 'search') {
|
|
3598
|
+
const { searchMcpRegistry, formatRegistrySearch, parseMcpRegistrySearchArgs } = await import('./mcp-registry.js');
|
|
3599
|
+
const parsed = parseMcpRegistrySearchArgs(rest);
|
|
3600
|
+
if (!parsed.ok) {
|
|
3601
|
+
console.error(parsed.message);
|
|
3602
|
+
process.exit(1);
|
|
3603
|
+
}
|
|
3604
|
+
const result = await searchMcpRegistry(parsed.value.query, { limit: parsed.value.limit, cursor: parsed.value.cursor });
|
|
3605
|
+
console.log(formatRegistrySearch(result));
|
|
3606
|
+
return;
|
|
516
3607
|
}
|
|
517
|
-
|
|
518
|
-
|
|
3608
|
+
if (action === 'info') {
|
|
3609
|
+
const { getMcpRegistryServer, formatRegistryInfo } = await import('./mcp-registry.js');
|
|
3610
|
+
const name = positionals(rest, new Set(['--version']))[0];
|
|
3611
|
+
const versionArg = rest.find((item) => item.startsWith('--version='));
|
|
3612
|
+
const version = versionArg?.slice('--version='.length) ?? (rest.includes('--version') ? rest[rest.indexOf('--version') + 1] : undefined);
|
|
3613
|
+
if (!name) {
|
|
3614
|
+
console.error(`ใช้: ${BRAND.cliName} mcp info <registry-server-name> [--version=x.y.z]`);
|
|
3615
|
+
process.exit(1);
|
|
3616
|
+
}
|
|
3617
|
+
const server = await getMcpRegistryServer(name, { version });
|
|
3618
|
+
if (!server) {
|
|
3619
|
+
console.error(`ไม่เจอ MCP registry server: ${name}`);
|
|
3620
|
+
process.exit(1);
|
|
3621
|
+
}
|
|
3622
|
+
console.log(formatRegistryInfo(server));
|
|
3623
|
+
return;
|
|
3624
|
+
}
|
|
3625
|
+
if (action === 'preset') {
|
|
3626
|
+
const { formatPreset } = await import('./mcp-registry.js');
|
|
3627
|
+
console.log(formatPreset(rest[0]));
|
|
3628
|
+
return;
|
|
3629
|
+
}
|
|
3630
|
+
if (action === 'install') {
|
|
3631
|
+
const { buildMcpInstallPlan, getMcpRegistryServer, parseKeyValueList, formatRegistryInfo, parseMcpRegistryInstallArgs, } = await import('./mcp-registry.js');
|
|
3632
|
+
const parsedInstall = parseMcpRegistryInstallArgs(rest);
|
|
3633
|
+
if (!parsedInstall.ok) {
|
|
3634
|
+
console.error(parsedInstall.message);
|
|
3635
|
+
process.exit(1);
|
|
3636
|
+
}
|
|
3637
|
+
const { name, alias, transport, version, env, headers, project } = parsedInstall.value;
|
|
3638
|
+
if (alias && !isValidMcpServerName(alias)) {
|
|
3639
|
+
console.error('ชื่อ MCP server ต้องเป็น a-z/A-Z/0-9/_/- ความยาวไม่เกิน 64 และห้ามใช้ชื่อพิเศษ');
|
|
3640
|
+
process.exit(1);
|
|
3641
|
+
}
|
|
3642
|
+
if (transport && !['auto', 'remote', 'stdio'].includes(transport)) {
|
|
3643
|
+
console.error('--transport ต้องเป็น auto, remote, หรือ stdio');
|
|
3644
|
+
process.exit(1);
|
|
3645
|
+
}
|
|
3646
|
+
const server = await getMcpRegistryServer(name, { version });
|
|
3647
|
+
if (!server) {
|
|
3648
|
+
console.error(`ไม่เจอ MCP registry server: ${name}`);
|
|
3649
|
+
process.exit(1);
|
|
3650
|
+
}
|
|
3651
|
+
const plan = buildMcpInstallPlan(server, {
|
|
3652
|
+
alias,
|
|
3653
|
+
transport,
|
|
3654
|
+
env: parseKeyValueList(env),
|
|
3655
|
+
headers: parseKeyValueList(headers),
|
|
3656
|
+
});
|
|
3657
|
+
if (!plan.ok) {
|
|
3658
|
+
console.log(formatRegistryInfo(server));
|
|
3659
|
+
console.error(`\nยัง install ไม่ได้: ต้องระบุ ${plan.missing.join(', ') || 'transport/package ที่รองรับ'}`);
|
|
3660
|
+
if (plan.missing.some((item) => item.startsWith('env:')))
|
|
3661
|
+
console.error(`ตัวอย่าง: ${BRAND.cliName} mcp install ${name} --env KEY=value`);
|
|
3662
|
+
if (plan.missing.some((item) => item.startsWith('header:')))
|
|
3663
|
+
console.error(`ตัวอย่าง: ${BRAND.cliName} mcp install ${name} --header Authorization='Bearer ...'`);
|
|
3664
|
+
for (const warning of plan.warnings)
|
|
3665
|
+
console.error(`warning: ${warning}`);
|
|
3666
|
+
process.exit(1);
|
|
3667
|
+
}
|
|
3668
|
+
let targetPath = mcpPath;
|
|
3669
|
+
if (project) {
|
|
3670
|
+
// Use trust status (not projectConfigPathIfTrusted, which requires a PRE-EXISTING file) so the
|
|
3671
|
+
// FIRST project-scoped install into a trusted project can create .sanook/mcp.json.
|
|
3672
|
+
const { projectTrustStatus, projectRoot } = await import('./trust.js');
|
|
3673
|
+
const { appProjectPath } = await import('./brand.js');
|
|
3674
|
+
const root = await projectRoot(process.cwd());
|
|
3675
|
+
const trust = await projectTrustStatus(root);
|
|
3676
|
+
if (!trust.trusted) {
|
|
3677
|
+
console.error(`project MCP ต้อง trust ก่อน: ${BRAND.cliName} trust add`);
|
|
3678
|
+
process.exit(1);
|
|
3679
|
+
}
|
|
3680
|
+
targetPath = appProjectPath(root, 'mcp.json'); // may not exist yet — writeConfig creates it
|
|
3681
|
+
}
|
|
3682
|
+
cfg = await readConfig(targetPath);
|
|
3683
|
+
cfg.mcpServers[plan.alias] = plan.config;
|
|
3684
|
+
await writeConfig(targetPath, cfg);
|
|
3685
|
+
console.log(`ติดตั้ง MCP "${plan.alias}" จาก ${server.name} (${plan.source}) → ${targetPath}`);
|
|
3686
|
+
const { inferRegistryServerRisk, formatMcpRiskLabel } = await import('./mcp-risk.js');
|
|
3687
|
+
console.log(`risk: ${formatMcpRiskLabel(inferRegistryServerRisk(server))}`);
|
|
3688
|
+
if (plan.requirements.length)
|
|
3689
|
+
console.log(`requirements: ${plan.requirements.join(', ')}`);
|
|
3690
|
+
for (const warning of plan.warnings)
|
|
3691
|
+
console.log(`warning: ${warning}`);
|
|
3692
|
+
console.log(`ทดสอบ: ${BRAND.cliName} mcp test ${plan.alias}`);
|
|
3693
|
+
return;
|
|
3694
|
+
}
|
|
3695
|
+
if (action === 'test' || action === 'doctor') {
|
|
3696
|
+
const { loadMcpConfig, probeMcpServer, isMcpServerEnabled } = await import('./mcp.js');
|
|
3697
|
+
const { inferConfiguredServerRisk, formatMcpRiskLabel } = await import('./mcp-risk.js');
|
|
3698
|
+
const logs = [];
|
|
3699
|
+
const merged = await loadMcpConfig((m) => logs.push(m));
|
|
3700
|
+
const explicitName = action === 'test' && rest[0] ? rest[0] : undefined;
|
|
3701
|
+
const names = explicitName
|
|
3702
|
+
? [explicitName]
|
|
3703
|
+
: action === 'test'
|
|
3704
|
+
? Object.keys(merged).filter((n) => isMcpServerEnabled(merged[n]))
|
|
3705
|
+
: Object.keys(merged);
|
|
3706
|
+
if (!names.length) {
|
|
3707
|
+
console.log(`ยังไม่มี MCP server — เพิ่ม: ${BRAND.cliName} mcp search github`);
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
let failed = false;
|
|
3711
|
+
if (logs.length)
|
|
3712
|
+
for (const log of logs)
|
|
3713
|
+
console.log(`note: ${log}`);
|
|
3714
|
+
for (const n of names) {
|
|
3715
|
+
const server = merged[n];
|
|
3716
|
+
if (!server) {
|
|
3717
|
+
failed = true;
|
|
3718
|
+
console.log(`[FAIL] ${n} — ไม่เจอใน config`);
|
|
3719
|
+
continue;
|
|
3720
|
+
}
|
|
3721
|
+
if (!explicitName && action === 'doctor' && !isMcpServerEnabled(server)) {
|
|
3722
|
+
console.log(`[SKIP] ${n} (disabled)`);
|
|
3723
|
+
continue;
|
|
3724
|
+
}
|
|
3725
|
+
const probe = await probeMcpServer(server);
|
|
3726
|
+
const risk = formatMcpRiskLabel(inferConfiguredServerRisk(n, server, probe.tools));
|
|
3727
|
+
if (probe.ok) {
|
|
3728
|
+
console.log(`[PASS] ${n} (${probe.transport}) — ${probe.tools.length} tool(s) · risk: ${risk}`);
|
|
3729
|
+
for (const tool of probe.tools.slice(0, action === 'doctor' ? 8 : 30))
|
|
3730
|
+
console.log(` - ${tool.name}${tool.description ? ` — ${tool.description}` : ''}`);
|
|
3731
|
+
if (action === 'doctor' && probe.tools.length > 8)
|
|
3732
|
+
console.log(` ... ${probe.tools.length - 8} more`);
|
|
3733
|
+
}
|
|
3734
|
+
else {
|
|
3735
|
+
failed = true;
|
|
3736
|
+
console.log(`[FAIL] ${n} (${probe.transport}) — ${probe.error} · risk: ${risk}`);
|
|
3737
|
+
if (probe.authHints?.length)
|
|
3738
|
+
for (const hint of probe.authHints)
|
|
3739
|
+
console.log(` hint: ${hint}`);
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
if (failed)
|
|
3743
|
+
process.exit(1);
|
|
3744
|
+
return;
|
|
3745
|
+
}
|
|
3746
|
+
if (action === 'enable' || action === 'disable') {
|
|
3747
|
+
const { findMcpServerConfigPath } = await import('./mcp.js');
|
|
3748
|
+
const [name] = rest;
|
|
3749
|
+
if (!name) {
|
|
3750
|
+
console.error(`ใช้: ${BRAND.cliName} mcp ${action} <name>`);
|
|
3751
|
+
process.exit(1);
|
|
3752
|
+
}
|
|
3753
|
+
const targetPath = await findMcpServerConfigPath(name);
|
|
3754
|
+
if (!targetPath) {
|
|
3755
|
+
console.error(`ไม่เจอ MCP server "${name}" ใน global หรือ trusted project config`);
|
|
3756
|
+
process.exit(1);
|
|
3757
|
+
}
|
|
3758
|
+
const targetCfg = await readConfig(targetPath);
|
|
3759
|
+
if (!targetCfg.mcpServers[name]) {
|
|
3760
|
+
console.error(`ไม่เจอ MCP server "${name}" ใน ${targetPath}`);
|
|
3761
|
+
process.exit(1);
|
|
3762
|
+
}
|
|
3763
|
+
targetCfg.mcpServers[name] = { ...targetCfg.mcpServers[name], enabled: action === 'enable' };
|
|
3764
|
+
await writeConfig(targetPath, targetCfg);
|
|
3765
|
+
console.log(`${action === 'enable' ? 'เปิด' : 'ปิด'} MCP server "${name}" แล้ว → ${targetPath}`);
|
|
3766
|
+
console.log(`ทดสอบ: ${BRAND.cliName} mcp test ${name}`);
|
|
3767
|
+
return;
|
|
519
3768
|
}
|
|
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
3769
|
if (action === 'add') {
|
|
3770
|
+
const [name, command, ...cmdArgs] = rest;
|
|
527
3771
|
if (!name || !command) {
|
|
528
3772
|
console.error(`ใช้: ${BRAND.cliName} mcp add <name> <command> [args...] (เช่น: mcp add fs npx -y @modelcontextprotocol/server-filesystem /path)`);
|
|
529
3773
|
console.error(` remote: ${BRAND.cliName} mcp add <name> https://host/mcp (Streamable-HTTP)`);
|
|
@@ -535,29 +3779,60 @@ async function runMcp(args) {
|
|
|
535
3779
|
}
|
|
536
3780
|
// command เป็น http(s):// → remote MCP (Streamable-HTTP), ไม่งั้น stdio
|
|
537
3781
|
cfg.mcpServers[name] = /^https?:\/\//.test(command) ? { url: command } : { command, args: cmdArgs };
|
|
538
|
-
await
|
|
3782
|
+
await writeConfig(mcpPath, cfg);
|
|
539
3783
|
console.log(`เพิ่ม MCP server "${name}"${/^https?:\/\//.test(command) ? ' (remote http)' : ''}`);
|
|
540
3784
|
return;
|
|
541
3785
|
}
|
|
542
3786
|
if (action === 'remove' || action === 'rm') {
|
|
3787
|
+
const [name] = rest;
|
|
543
3788
|
if (name && cfg.mcpServers[name]) {
|
|
544
3789
|
delete cfg.mcpServers[name];
|
|
545
|
-
await
|
|
546
|
-
console.log(`ลบ MCP server "${name}"
|
|
3790
|
+
await writeConfig(mcpPath, cfg);
|
|
3791
|
+
console.log(`ลบ MCP server "${name}" แล้ว (${mcpPath})`);
|
|
547
3792
|
}
|
|
548
3793
|
else
|
|
549
3794
|
console.log(`ไม่เจอ MCP server "${name ?? ''}"`);
|
|
550
3795
|
return;
|
|
551
3796
|
}
|
|
552
|
-
|
|
3797
|
+
if (action !== 'list') {
|
|
3798
|
+
console.log(`ใช้: ${BRAND.cliName} mcp [search|info|install|test|doctor|enable|disable|preset|list|add|remove|serve]`);
|
|
3799
|
+
return;
|
|
3800
|
+
}
|
|
3801
|
+
const { loadMcpConfig, isMcpServerEnabled } = await import('./mcp.js');
|
|
3802
|
+
const { inferConfiguredServerRisk, formatMcpRiskLabel } = await import('./mcp-risk.js');
|
|
3803
|
+
const logs = [];
|
|
3804
|
+
const merged = await loadMcpConfig((m) => logs.push(m));
|
|
3805
|
+
const names = Object.keys(merged);
|
|
553
3806
|
if (!names.length) {
|
|
554
|
-
console.log(`ยังไม่มี MCP server — เพิ่ม: ${BRAND.cliName} mcp
|
|
3807
|
+
console.log(`ยังไม่มี MCP server — เพิ่ม: ${BRAND.cliName} mcp search github`);
|
|
555
3808
|
return;
|
|
556
3809
|
}
|
|
3810
|
+
if (logs.length)
|
|
3811
|
+
for (const log of logs)
|
|
3812
|
+
console.log(`note: ${log}`);
|
|
557
3813
|
console.log(`${names.length} MCP servers:`);
|
|
558
3814
|
for (const n of names) {
|
|
559
|
-
const s =
|
|
560
|
-
|
|
3815
|
+
const s = merged[n];
|
|
3816
|
+
const enabled = isMcpServerEnabled(s);
|
|
3817
|
+
const risk = formatMcpRiskLabel(inferConfiguredServerRisk(n, s));
|
|
3818
|
+
console.log(` ${n}${enabled ? '' : ' (disabled)'} — risk: ${risk} — ${s.url ? `${s.url} (http)` : `${s.command} ${(s.args ?? []).join(' ')}`}`);
|
|
3819
|
+
}
|
|
3820
|
+
if (rest.includes('--tools')) {
|
|
3821
|
+
const { probeMcpServer } = await import('./mcp.js');
|
|
3822
|
+
for (const n of names) {
|
|
3823
|
+
if (!isMcpServerEnabled(merged[n])) {
|
|
3824
|
+
console.log(`\n[SKIP] ${n} tools (disabled)`);
|
|
3825
|
+
continue;
|
|
3826
|
+
}
|
|
3827
|
+
const probe = await probeMcpServer(merged[n]);
|
|
3828
|
+
const risk = formatMcpRiskLabel(inferConfiguredServerRisk(n, merged[n], probe.tools));
|
|
3829
|
+
console.log(`\n${probe.ok ? '[PASS]' : '[FAIL]'} ${n} tools${probe.ok ? ` (${probe.tools.length})` : ` — ${probe.error}`} · risk: ${risk}`);
|
|
3830
|
+
if (probe.authHints?.length)
|
|
3831
|
+
for (const hint of probe.authHints)
|
|
3832
|
+
console.log(` hint: ${hint}`);
|
|
3833
|
+
for (const tool of probe.tools.slice(0, 30))
|
|
3834
|
+
console.log(` - ${tool.name}`);
|
|
3835
|
+
}
|
|
561
3836
|
}
|
|
562
3837
|
}
|
|
563
3838
|
/** sanook trust [status|add|remove] — trust project .sanook content that can steer/execute code */
|
|
@@ -642,6 +3917,66 @@ async function askYesNo(question) {
|
|
|
642
3917
|
rl.close();
|
|
643
3918
|
}
|
|
644
3919
|
}
|
|
3920
|
+
async function askText(question) {
|
|
3921
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3922
|
+
try {
|
|
3923
|
+
return (await rl.question(question)).trim();
|
|
3924
|
+
}
|
|
3925
|
+
finally {
|
|
3926
|
+
rl.close();
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
function extractValue(args, ...names) {
|
|
3930
|
+
const rest = [];
|
|
3931
|
+
let value;
|
|
3932
|
+
for (let i = 0; i < args.length; i++) {
|
|
3933
|
+
const a = args[i];
|
|
3934
|
+
const eq = names.find((name) => a.startsWith(`${name}=`));
|
|
3935
|
+
if (eq) {
|
|
3936
|
+
value ??= a.slice(eq.length + 1);
|
|
3937
|
+
continue;
|
|
3938
|
+
}
|
|
3939
|
+
if (names.includes(a)) {
|
|
3940
|
+
if (args[i + 1] && !args[i + 1].startsWith('-')) {
|
|
3941
|
+
value ??= args[i + 1];
|
|
3942
|
+
i++;
|
|
3943
|
+
}
|
|
3944
|
+
continue;
|
|
3945
|
+
}
|
|
3946
|
+
rest.push(a);
|
|
3947
|
+
}
|
|
3948
|
+
return { value, rest };
|
|
3949
|
+
}
|
|
3950
|
+
function stripBooleanFlags(args, ...names) {
|
|
3951
|
+
return args.filter((a) => !names.includes(a));
|
|
3952
|
+
}
|
|
3953
|
+
function positionalArgs(args, valueFlags = []) {
|
|
3954
|
+
const out = [];
|
|
3955
|
+
for (let i = 0; i < args.length; i++) {
|
|
3956
|
+
const a = args[i];
|
|
3957
|
+
if (valueFlags.some((name) => a.startsWith(`${name}=`)))
|
|
3958
|
+
continue;
|
|
3959
|
+
if (valueFlags.includes(a)) {
|
|
3960
|
+
i++;
|
|
3961
|
+
continue;
|
|
3962
|
+
}
|
|
3963
|
+
if (a.startsWith('-'))
|
|
3964
|
+
continue;
|
|
3965
|
+
out.push(a);
|
|
3966
|
+
}
|
|
3967
|
+
return out;
|
|
3968
|
+
}
|
|
3969
|
+
function argValue(args, ...names) {
|
|
3970
|
+
for (const name of names) {
|
|
3971
|
+
const eq = args.find((a) => a.startsWith(`${name}=`));
|
|
3972
|
+
if (eq)
|
|
3973
|
+
return eq.slice(name.length + 1);
|
|
3974
|
+
const idx = args.indexOf(name);
|
|
3975
|
+
if (idx !== -1 && args[idx + 1] && !args[idx + 1].startsWith('--'))
|
|
3976
|
+
return args[idx + 1];
|
|
3977
|
+
}
|
|
3978
|
+
return undefined;
|
|
3979
|
+
}
|
|
645
3980
|
async function maybePromptForInteractiveUpdate() {
|
|
646
3981
|
if (envFlag(BRAND_ENV.disableUpdateCheck) || process.env.CI)
|
|
647
3982
|
return;
|
|
@@ -686,8 +4021,12 @@ function headlessKeyHint(modelSpec) {
|
|
|
686
4021
|
`⚠ ยังไม่มี API key สำหรับ ${cfg.label} (${cfg.envVar})`,
|
|
687
4022
|
`เริ่มใช้งาน:`,
|
|
688
4023
|
` • รัน "${BRAND.cliName}" (ไม่ใส่ task) → setup wizard ทีละขั้น (แนะนำ)`,
|
|
4024
|
+
` • หรือ: ${BRAND.cliName} auth add ${provider} --api-key "..." --use${url ? ` · เอา key ที่: ${url}` : ''}`,
|
|
689
4025
|
` • หรือ: export ${cfg.envVar}="..."${url ? ` · เอา key ที่: ${url}` : ''}`,
|
|
690
4026
|
];
|
|
4027
|
+
if (provider === 'openai') {
|
|
4028
|
+
lines.push(` • ถ้าต้องการใช้ ChatGPT plan ไม่ใช้ API key: ใช้ ${BRAND.cliName} -m codex แล้วรัน codex login`);
|
|
4029
|
+
}
|
|
691
4030
|
const other = detectEnvProvider();
|
|
692
4031
|
if (other && other.provider !== provider) {
|
|
693
4032
|
lines.push(` • เจอ key ของ ${other.label} อยู่แล้ว → ใช้เลย: ${BRAND.cliName} -m ${other.provider} "<task>"`);
|
|
@@ -703,11 +4042,15 @@ async function main() {
|
|
|
703
4042
|
process.exit(1);
|
|
704
4043
|
}
|
|
705
4044
|
const argv = process.argv.slice(2);
|
|
706
|
-
|
|
4045
|
+
// --version/--help win in ANY position before `--` (so `sanook --help foo`, `-m x -h` work),
|
|
4046
|
+
// not only as the sole argument. (Quoted prompts like `sanook "explain --help"` are one token → unaffected.)
|
|
4047
|
+
const optArgsEnd = argv.indexOf('--');
|
|
4048
|
+
const topOpts = optArgsEnd === -1 ? argv : argv.slice(0, optArgsEnd);
|
|
4049
|
+
if (topOpts.includes('-v') || topOpts.includes('--version')) {
|
|
707
4050
|
console.log(VERSION);
|
|
708
4051
|
return;
|
|
709
4052
|
}
|
|
710
|
-
if (
|
|
4053
|
+
if (topOpts.includes('-h') || topOpts.includes('--help')) {
|
|
711
4054
|
console.log(HELP);
|
|
712
4055
|
return;
|
|
713
4056
|
}
|
|
@@ -722,18 +4065,61 @@ async function main() {
|
|
|
722
4065
|
// โหลด API key จาก ~/.sanook/auth.json เข้า env (ไม่ override env ที่ตั้งไว้แล้ว)
|
|
723
4066
|
await loadKeysIntoEnv();
|
|
724
4067
|
process.on('exit', closeMcp); // ปิด MCP server (kill child) ตอนจบ
|
|
4068
|
+
// management surfaces (Sanook-branded) — setup/model/gateway/status/tools/send
|
|
4069
|
+
if (argv[0] === '-z')
|
|
4070
|
+
return runPureOneShot(argv.slice(1));
|
|
4071
|
+
if (argv[0] === 'chat')
|
|
4072
|
+
return runChat(argv.slice(1));
|
|
4073
|
+
if (argv[0] === 'plan')
|
|
4074
|
+
return runPlan(argv.slice(1));
|
|
4075
|
+
if (argv[0] === 'setup')
|
|
4076
|
+
return runSetup(argv.slice(1));
|
|
4077
|
+
if (argv[0] === 'model' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
4078
|
+
return startModelSetup();
|
|
4079
|
+
if (argv[0] === 'gateway')
|
|
4080
|
+
return runGateway(argv.slice(1));
|
|
4081
|
+
if (argv[0] === 'status' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
4082
|
+
return runStatus();
|
|
4083
|
+
if (argv[0] === 'auth')
|
|
4084
|
+
return runAuth(argv.slice(1));
|
|
4085
|
+
if (argv[0] === 'sessions' || argv[0] === 'session')
|
|
4086
|
+
return runSessions(argv.slice(1));
|
|
4087
|
+
if (argv[0] === 'insights')
|
|
4088
|
+
return runInsights(argv.slice(1));
|
|
4089
|
+
if (argv[0] === 'memory' && ['log', 'stats', undefined].includes(argv[1]))
|
|
4090
|
+
return runMemory(argv.slice(1));
|
|
4091
|
+
if (argv[0] === 'dump')
|
|
4092
|
+
return runDump(argv.slice(1));
|
|
4093
|
+
if (argv[0] === 'prompt-size' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
4094
|
+
return runPromptSize(argv.slice(1));
|
|
4095
|
+
if (argv[0] === 'runtimes' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
4096
|
+
return runRuntimes(argv.slice(1));
|
|
4097
|
+
if (argv[0] === 'dashboard' && (argv.length === 1 || !argv[1].startsWith('-') || argv[1] === '--port')) {
|
|
4098
|
+
return runDashboard(argv.slice(1));
|
|
4099
|
+
}
|
|
4100
|
+
if (argv[0] === 'web' && ['status', 'doctor', 'fetch', 'search', 'setup', undefined].includes(argv[1]))
|
|
4101
|
+
return runWeb(argv.slice(1));
|
|
4102
|
+
if (argv[0] === 'tools' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
4103
|
+
return runTools(argv.slice(1));
|
|
4104
|
+
if (argv[0] === 'send')
|
|
4105
|
+
return runSend(argv.slice(1));
|
|
4106
|
+
if (argv[0] === 'webhook' || argv[0] === 'webhooks')
|
|
4107
|
+
return runWebhook(argv.slice(1));
|
|
725
4108
|
// subcommands: serve · cron — match เฉพาะรูปแบบที่ถูกต้อง กัน prompt unquoted ("serve coffee") misfire
|
|
726
|
-
if (
|
|
4109
|
+
if (hasServeCommandRequest(argv))
|
|
727
4110
|
return runServe(argv.slice(1));
|
|
728
4111
|
if (argv[0] === 'cron' && ['add', 'list', 'rm', 'remove', undefined].includes(argv[1])) {
|
|
729
4112
|
return runCron(argv.slice(1));
|
|
730
4113
|
}
|
|
731
|
-
if (argv[0] === 'skill' && ['list', 'add', 'remove', 'rm', undefined].includes(argv[1])) {
|
|
4114
|
+
if (argv[0] === 'skill' && ['list', 'add', 'install', 'remove', 'rm', undefined].includes(argv[1])) {
|
|
732
4115
|
return runSkill(argv.slice(1));
|
|
733
4116
|
}
|
|
4117
|
+
if (argv[0] === 'init' && (argv.length === 1 || argv[1].startsWith('-')))
|
|
4118
|
+
return runInit(argv.slice(1));
|
|
734
4119
|
if (argv[0] === 'models')
|
|
735
4120
|
return runModels(argv.slice(1));
|
|
736
|
-
if (argv[0] === 'brain' &&
|
|
4121
|
+
if (argv[0] === 'brain' &&
|
|
4122
|
+
['init', 'doctor', 'context', 'eval', 'review', 'projects', 'pack', 'new', 'repair', 'consolidate', 'metrics', 'final', undefined].includes(argv[1]))
|
|
737
4123
|
return runBrain(argv.slice(1));
|
|
738
4124
|
if (argv[0] === 'config' && ['get', 'set', 'list', undefined].includes(argv[1]))
|
|
739
4125
|
return runConfig(argv.slice(1));
|
|
@@ -743,17 +4129,28 @@ async function main() {
|
|
|
743
4129
|
return runSearch(argv.slice(1));
|
|
744
4130
|
if (argv[0] === 'mcp' && argv[1] === 'serve')
|
|
745
4131
|
return runMcpServe();
|
|
746
|
-
if (argv[0] === 'mcp' && ['add', 'list', 'remove', 'rm', undefined].includes(argv[1]))
|
|
4132
|
+
if (argv[0] === 'mcp' && ['add', 'list', 'remove', 'rm', 'search', 'info', 'install', 'test', 'doctor', 'enable', 'disable', 'preset', undefined].includes(argv[1]))
|
|
747
4133
|
return runMcp(argv.slice(1));
|
|
748
4134
|
if (argv[0] === 'trust' && ['status', 'add', 'remove', 'rm', undefined].includes(argv[1]))
|
|
749
4135
|
return runTrust(argv.slice(1));
|
|
750
|
-
|
|
4136
|
+
// A management command word whose subcommand didn't match any route above → don't silently
|
|
4137
|
+
// fall through and run it as an LLM task (costly + confusing). These words intentionally
|
|
4138
|
+
// require a valid subcommand; an NL prompt starting with one can still be quoted as a single arg.
|
|
4139
|
+
const MANAGEMENT_WORDS = new Set(['config', 'mcp', 'brain', 'web', 'trust', 'cron', 'skill', 'init', 'dashboard']);
|
|
4140
|
+
if (argv[0] && MANAGEMENT_WORDS.has(argv[0]) && argv[1] && !argv[1].startsWith('-')) {
|
|
4141
|
+
console.error(`${BRAND.cliName}: ไม่รู้จัก subcommand "${argv[0]} ${argv[1]}" — ดูวิธีใช้: ${BRAND.cliName} --help`);
|
|
4142
|
+
process.exit(1);
|
|
4143
|
+
}
|
|
4144
|
+
const { model, budget, budgetInvalid, json, quiet, prompt: argPrompt, planMode, yes, resume } = parseArgs(argv);
|
|
4145
|
+
if (budgetInvalid)
|
|
4146
|
+
process.stderr.write(`${BRAND.cliName}: ⚠ --budget ไม่ถูกต้อง (ต้องเป็นจำนวนบวก) — รันต่อโดยไม่มี spend cap\n`);
|
|
4147
|
+
const resumeSession = await requestedResumeSession(argv, resume);
|
|
751
4148
|
const budgetUsd = Number.isFinite(budget) ? budget : undefined;
|
|
752
4149
|
// stdin piping: `git diff | sanook "review this"` → ผนวก stdin เข้า prompt (headless/CI)
|
|
753
4150
|
const piped = process.stdin.isTTY ? '' : (await readStdin()).trim();
|
|
754
4151
|
const prompt = piped ? `${argPrompt}\n\n<stdin>\n${piped}\n</stdin>`.trim() : argPrompt;
|
|
755
4152
|
if (prompt) {
|
|
756
|
-
const config = await loadConfig({ model, budgetUsd });
|
|
4153
|
+
const config = await loadConfig({ model: model ?? resumeSession?.model, budgetUsd });
|
|
757
4154
|
// headless + ยังไม่มี key → บอกวิธีเริ่มแบบ actionable แทนปล่อยให้ throw error ดิบ (กัน dead-end ของ flow ที่ README แนะนำ)
|
|
758
4155
|
const noKey = headlessKeyHint(config.model);
|
|
759
4156
|
if (noKey) {
|
|
@@ -761,11 +4158,8 @@ async function main() {
|
|
|
761
4158
|
process.exit(1);
|
|
762
4159
|
}
|
|
763
4160
|
// --continue / -c → โหลด session ล่าสุดมาต่อ (จำว่าทำถึงไหน)
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
? (await latestSession(argv.includes('--continue-any') ? null : process.cwd()))?.messages
|
|
767
|
-
: undefined;
|
|
768
|
-
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, json, history, planMode, yes ? 'auto' : config.permissionMode, quiet, config.fallbackModel);
|
|
4161
|
+
const history = resumeSession?.messages ?? (await requestedContinuationHistory(argv));
|
|
4162
|
+
await runHeadless(config.model, prompt, config.budgetUsd, config.maxSteps, json, history, planMode, yes ? 'auto' : config.permissionMode, quiet, config.fallbackModel, planMode ? prompt : undefined);
|
|
769
4163
|
return;
|
|
770
4164
|
}
|
|
771
4165
|
await maybePromptForInteractiveUpdate();
|
|
@@ -776,7 +4170,8 @@ async function main() {
|
|
|
776
4170
|
const flagProvider = model ? parseSpec(model).provider : undefined;
|
|
777
4171
|
const target = flagProvider ?? detectEnvProvider()?.provider;
|
|
778
4172
|
const tcfg = target ? PROVIDERS[target] : undefined;
|
|
779
|
-
|
|
4173
|
+
const { providerCanSkipSetup } = await import('./first-run.js');
|
|
4174
|
+
if (target && tcfg && (await providerCanSkipSetup(target))) {
|
|
780
4175
|
// มี key ใช้ได้จริง (ผ่าน policy ไม่ใช่ OAuth) → ข้าม wizard, ตั้ง default, บอกว่าพร้อมใช้
|
|
781
4176
|
const { saveGlobalConfig } = await import('./config.js');
|
|
782
4177
|
await saveGlobalConfig({ model: model ?? `${target}:${tcfg.models.default}`, provider: target });
|
|
@@ -786,11 +4181,13 @@ async function main() {
|
|
|
786
4181
|
needsSetup = true; // ไม่มี provider ที่ key ใช้ได้ (หรือ -m provider ไม่มี key) → wizard (รัน Ink เดียวกับ REPL)
|
|
787
4182
|
}
|
|
788
4183
|
}
|
|
789
|
-
const config = await loadConfig({ model, budgetUsd });
|
|
4184
|
+
const config = await loadConfig({ model: model ?? resumeSession?.model, budgetUsd });
|
|
4185
|
+
if (!needsSetup) {
|
|
4186
|
+
const { modelNeedsSetup } = await import('./first-run.js');
|
|
4187
|
+
needsSetup = await modelNeedsSetup(config.model);
|
|
4188
|
+
}
|
|
790
4189
|
// --continue / -c → โหลด conversation ล่าสุดเข้า REPL (เดิม resume ได้แค่ headless)
|
|
791
|
-
const initialHistory =
|
|
792
|
-
? (await latestSession(argv.includes('--continue-any') ? null : process.cwd()))?.messages
|
|
793
|
-
: undefined;
|
|
4190
|
+
const initialHistory = resumeSession?.messages ?? (await requestedContinuationHistory(argv));
|
|
794
4191
|
const { startApp } = await import('./ui/render.js');
|
|
795
4192
|
startApp({
|
|
796
4193
|
needsSetup,
|