sanook-cli 0.4.0 → 0.5.1
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 +19 -0
- package/CHANGELOG.md +173 -0
- package/README.md +153 -20
- package/README.th.md +136 -0
- package/dist/agentContext.js +4 -0
- package/dist/approval.js +6 -0
- package/dist/bin.js +405 -57
- package/dist/brain.js +92 -59
- package/dist/brand.js +47 -0
- package/dist/checkpoint.js +37 -0
- package/dist/commands.js +86 -6
- package/dist/compaction.js +76 -5
- package/dist/config.js +100 -12
- package/dist/cost.js +60 -3
- package/dist/doctor.js +92 -0
- package/dist/gateway/auth.js +2 -2
- package/dist/gateway/ledger.js +2 -2
- package/dist/gateway/scheduler.js +1 -0
- package/dist/gateway/serve.js +6 -4
- package/dist/gateway/server.js +10 -2
- package/dist/git.js +11 -2
- package/dist/hooks.js +43 -17
- package/dist/knowledge.js +48 -49
- package/dist/loop.js +182 -66
- package/dist/lsp/client.js +173 -0
- package/dist/lsp/framing.js +56 -0
- package/dist/lsp/index.js +138 -0
- package/dist/lsp/servers.js +82 -0
- package/dist/mcp-server.js +244 -0
- package/dist/mcp.js +184 -29
- package/dist/memory-store.js +559 -0
- package/dist/memory.js +143 -29
- package/dist/orchestrate.js +150 -0
- package/dist/providers/codex.js +21 -7
- package/dist/providers/keys.js +3 -2
- package/dist/providers/models.js +22 -6
- package/dist/providers/registry.js +155 -1
- package/dist/repomap.js +93 -0
- package/dist/search/chunk.js +158 -0
- package/dist/search/embed-store.js +187 -0
- package/dist/search/engine.js +203 -0
- package/dist/search/fuse.js +35 -0
- package/dist/search/index-core.js +187 -0
- package/dist/search/indexer.js +241 -0
- package/dist/search/store.js +77 -0
- package/dist/session.js +42 -8
- package/dist/skill-install.js +10 -10
- package/dist/skills.js +12 -9
- package/dist/summarize.js +31 -0
- package/dist/tools/bash.js +21 -2
- package/dist/tools/diagnostics.js +41 -0
- package/dist/tools/edit.js +29 -7
- package/dist/tools/index.js +8 -1
- package/dist/tools/list.js +7 -2
- package/dist/tools/permission.js +90 -9
- package/dist/tools/read.js +23 -4
- package/dist/tools/remember.js +1 -1
- package/dist/tools/sandbox.js +61 -0
- package/dist/tools/search.js +105 -4
- package/dist/tools/task.js +195 -29
- package/dist/tools/timeout.js +35 -0
- package/dist/tools/util.js +10 -0
- package/dist/tools/write.js +6 -4
- package/dist/trust.js +89 -0
- package/dist/ui/app.js +228 -31
- package/dist/ui/banner.js +4 -9
- package/dist/ui/brain-wizard.js +2 -2
- package/dist/ui/history.js +30 -0
- package/dist/ui/mentions.js +44 -0
- package/dist/ui/render.js +55 -15
- package/dist/ui/setup.js +97 -12
- package/dist/ui/useEditor.js +83 -0
- package/dist/update.js +114 -0
- package/dist/worktree.js +173 -0
- package/package.json +11 -5
- package/scripts/postinstall.mjs +33 -0
- package/second-brain/.agents/_Index.md +30 -0
- package/second-brain/.agents/skills/_Index.md +30 -0
- package/second-brain/.agents/workflows/_Index.md +30 -0
- package/second-brain/AGENTS.md +4 -4
- package/second-brain/Acceptance/_Index.md +30 -0
- package/second-brain/Acceptance/golden-case-template.md +39 -0
- package/second-brain/Areas/_Index.md +30 -0
- package/second-brain/Bugs/System-OS/_Index.md +30 -0
- package/second-brain/Bugs/_Index.md +30 -0
- package/second-brain/CLAUDE.md +4 -1
- package/second-brain/Checklists/_Index.md +30 -0
- package/second-brain/Checklists/preflight-postflight-template.md +29 -0
- package/second-brain/Distillations/_Index.md +30 -0
- package/second-brain/Entities/_Index.md +30 -0
- package/second-brain/Entities/entity-template.md +33 -0
- package/second-brain/Evals/_Index.md +30 -0
- package/second-brain/Evals/correction-pairs.md +24 -0
- package/second-brain/Evals/failure-taxonomy.md +24 -0
- package/second-brain/Evals/golden-set.md +25 -0
- package/second-brain/Evals/quality-ledger.md +23 -0
- package/second-brain/Evals/self-eval-rubric.md +23 -0
- package/second-brain/GEMINI.md +4 -4
- package/second-brain/Goals/_Index.md +30 -0
- package/second-brain/Handoffs/_Index.md +30 -0
- package/second-brain/Home.md +7 -0
- package/second-brain/Intake/Raw Sources/_Index.md +30 -0
- package/second-brain/Intake/_Index.md +30 -0
- package/second-brain/Intake/_Quarantine/_Index.md +30 -0
- package/second-brain/Learning/_Index.md +30 -0
- package/second-brain/Playbooks/_Index.md +30 -0
- package/second-brain/Playbooks/playbook-template.md +23 -0
- package/second-brain/Projects/_Index.md +30 -0
- package/second-brain/Prompts/_Index.md +30 -0
- package/second-brain/README.md +2 -1
- package/second-brain/Research/_Index.md +30 -0
- package/second-brain/Retrospectives/_Index.md +30 -0
- package/second-brain/Reviews/_Index.md +30 -0
- package/second-brain/Runbooks/_Index.md +30 -0
- package/second-brain/Runbooks/eval-loop.md +24 -0
- package/second-brain/Sessions/_Index.md +30 -0
- package/second-brain/Shared/AI-Context-Index.md +20 -0
- package/second-brain/Shared/AI-Threads/_Index.md +30 -0
- package/second-brain/Shared/Archive/_Index.md +30 -0
- package/second-brain/Shared/Assets/_Index.md +30 -0
- package/second-brain/Shared/Context-Packs/_Index.md +30 -0
- package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
- package/second-brain/Shared/Coordination/NOW.md +28 -0
- package/second-brain/Shared/Coordination/_Index.md +30 -0
- package/second-brain/Shared/Coordination/agent-registry.md +24 -0
- package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
- package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
- package/second-brain/Shared/Coordination/task-board.md +32 -0
- package/second-brain/Shared/Core-Facts/_Index.md +30 -0
- package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
- package/second-brain/Shared/Glossary/_Index.md +30 -0
- package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
- package/second-brain/Shared/Operating-State/_Index.md +30 -0
- package/second-brain/Shared/Prompting/_Index.md +30 -0
- package/second-brain/Shared/Provenance/_Index.md +30 -0
- package/second-brain/Shared/Rules/_Index.md +30 -0
- package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
- package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
- package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
- package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
- package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
- package/second-brain/Shared/Rules/rules-formatting.md +34 -0
- package/second-brain/Shared/Scripts/_Index.md +30 -0
- package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
- package/second-brain/Shared/User-Memory/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
- package/second-brain/Shared/Working-Memory/_Index.md +30 -0
- package/second-brain/Shared/_Index.md +30 -0
- package/second-brain/Shared/mcp-servers/_Index.md +30 -0
- package/second-brain/Skills/_Index.md +30 -0
- package/second-brain/Templates/_Index.md +30 -0
- package/second-brain/Templates/bug.md +2 -0
- package/second-brain/Templates/handoff.md +2 -0
- package/second-brain/Templates/session.md +2 -0
- package/second-brain/Tools/_Index.md +30 -0
- package/second-brain/Traces/_Index.md +30 -0
- package/second-brain/Vault Structure Map.md +33 -1
- package/second-brain/copilot/_Index.md +30 -0
- package/skills/audit-license-compliance/SKILL.md +117 -0
- package/skills/author-codemod/SKILL.md +110 -0
- package/skills/build-audit-logging/SKILL.md +112 -0
- package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
- package/skills/build-cli-tool/SKILL.md +108 -0
- package/skills/build-data-table/SKILL.md +141 -0
- package/skills/build-native-mobile-ui/SKILL.md +154 -0
- package/skills/build-offline-first-sync/SKILL.md +118 -0
- package/skills/build-realtime-channel/SKILL.md +122 -0
- package/skills/build-vector-search/SKILL.md +131 -0
- package/skills/compose-local-dev-stack/SKILL.md +149 -0
- package/skills/configure-bundler-build/SKILL.md +166 -0
- package/skills/configure-dns-tls/SKILL.md +142 -0
- package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
- package/skills/configure-security-headers-csp/SKILL.md +122 -0
- package/skills/contract-testing/SKILL.md +140 -0
- package/skills/datetime-timezone-correctness/SKILL.md +125 -0
- package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
- package/skills/debug-flaky-tests/SKILL.md +128 -0
- package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
- package/skills/deliver-webhooks/SKILL.md +116 -0
- package/skills/design-api-pagination/SKILL.md +144 -0
- package/skills/design-authorization-model/SKILL.md +119 -0
- package/skills/design-backup-dr-recovery/SKILL.md +113 -0
- package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
- package/skills/design-multi-tenancy/SKILL.md +100 -0
- package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
- package/skills/design-relational-schema/SKILL.md +129 -0
- package/skills/design-search-index-infra/SKILL.md +151 -0
- package/skills/design-state-machine/SKILL.md +108 -0
- package/skills/design-token-system/SKILL.md +109 -0
- package/skills/distributed-locks-leases/SKILL.md +120 -0
- package/skills/encrypt-sensitive-data/SKILL.md +148 -0
- package/skills/feature-flags-rollout/SKILL.md +130 -0
- package/skills/file-upload-object-storage/SKILL.md +107 -0
- package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
- package/skills/harden-llm-app-reliability/SKILL.md +126 -0
- package/skills/i18n-localization-setup/SKILL.md +113 -0
- package/skills/idempotency-keys/SKILL.md +107 -0
- package/skills/implement-push-notifications/SKILL.md +142 -0
- package/skills/ingest-webhook-secure/SKILL.md +120 -0
- package/skills/integrate-oauth-oidc/SKILL.md +126 -0
- package/skills/load-stress-test/SKILL.md +129 -0
- package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
- package/skills/model-nosql-data/SKILL.md +118 -0
- package/skills/money-decimal-arithmetic/SKILL.md +123 -0
- package/skills/monitor-ml-drift/SKILL.md +109 -0
- package/skills/numeric-precision-units/SKILL.md +144 -0
- package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
- package/skills/optimize-react-rerenders/SKILL.md +124 -0
- package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
- package/skills/payments-billing-integration/SKILL.md +114 -0
- package/skills/pin-toolchain-versions/SKILL.md +116 -0
- package/skills/plan-strangler-migration/SKILL.md +95 -0
- package/skills/property-based-testing/SKILL.md +108 -0
- package/skills/publish-package-registry/SKILL.md +130 -0
- package/skills/recover-git-state/SKILL.md +119 -0
- package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
- package/skills/resilience-timeouts-retries/SKILL.md +104 -0
- package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
- package/skills/rewrite-git-history/SKILL.md +109 -0
- package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
- package/skills/schema-evolution-compatibility/SKILL.md +121 -0
- package/skills/send-transactional-email/SKILL.md +126 -0
- package/skills/serve-deploy-ml-model/SKILL.md +107 -0
- package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
- package/skills/setup-devcontainer-env/SKILL.md +131 -0
- package/skills/setup-lint-format-precommit/SKILL.md +140 -0
- package/skills/setup-monorepo-tooling/SKILL.md +125 -0
- package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
- package/skills/structured-output-llm/SKILL.md +86 -0
- package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
- package/skills/test-data-factories/SKILL.md +158 -0
- package/skills/threat-model-stride/SKILL.md +123 -0
- package/skills/train-evaluate-ml-model/SKILL.md +109 -0
- package/skills/unicode-text-correctness/SKILL.md +109 -0
- package/skills/visual-regression-testing/SKILL.md +120 -0
package/dist/bin.js
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runAgent } from './loop.js';
|
|
3
3
|
import { redactKey } from './providers/keys.js';
|
|
4
|
-
import {
|
|
4
|
+
import { specKey, parseSpec, PROVIDERS, consoleUrl, detectEnvProvider, hasUsableEnvKey } from './providers/registry.js';
|
|
5
|
+
import { resolveKeyFromEnv } from './providers/keys.js';
|
|
6
|
+
import { hasPricingForKey } from './cost.js';
|
|
7
|
+
import { loadConfig, isFirstRun, loadKeysIntoEnv, parsePricingOverride } from './config.js';
|
|
5
8
|
import { saveSession, latestSession, newSessionId } from './session.js';
|
|
6
|
-
import { closeMcp } from './mcp.js';
|
|
9
|
+
import { closeMcp, isValidMcpServerName } from './mcp.js';
|
|
7
10
|
import { readFileSync } from 'node:fs';
|
|
8
11
|
import { homedir } from 'node:os';
|
|
9
12
|
import { join, dirname } from 'node:path';
|
|
10
|
-
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
import { chmod, readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
14
|
+
import { createInterface } from 'node:readline/promises';
|
|
15
|
+
import { appHomePath, BRAND, BRAND_ENV, envFlag } from './brand.js';
|
|
16
|
+
// สี: เคารพ NO_COLOR + auto-plain เมื่อ pipe/redirect (legacy Windows cmd ก็ไม่เห็น garbage ANSI); FORCE_COLOR บังคับได้
|
|
17
|
+
const useColor = !process.env.NO_COLOR && (Boolean(process.env.FORCE_COLOR) || process.stdout.isTTY === true);
|
|
18
|
+
const DIM = useColor ? '\x1b[2m' : '';
|
|
19
|
+
const RESET = useColor ? '\x1b[0m' : '';
|
|
13
20
|
function parseArgs(argv) {
|
|
14
21
|
let model;
|
|
15
22
|
let budget;
|
|
16
23
|
let json = false;
|
|
24
|
+
let quiet = false;
|
|
17
25
|
let planMode = false;
|
|
18
26
|
let yes = false;
|
|
19
27
|
const rest = [];
|
|
@@ -25,6 +33,16 @@ function parseArgs(argv) {
|
|
|
25
33
|
budget = Number.parseFloat(argv[++i] ?? '');
|
|
26
34
|
else if (a === '--json')
|
|
27
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
|
+
}
|
|
28
46
|
else if (a === '--plan')
|
|
29
47
|
planMode = true;
|
|
30
48
|
else if (a === '--yes' || a === '-y')
|
|
@@ -35,17 +53,27 @@ function parseArgs(argv) {
|
|
|
35
53
|
else
|
|
36
54
|
rest.push(a);
|
|
37
55
|
}
|
|
38
|
-
return { model, budget, json, prompt: rest.join(' ').trim(), planMode, yes };
|
|
56
|
+
return { model, budget, json, quiet, prompt: rest.join(' ').trim(), planMode, yes };
|
|
39
57
|
}
|
|
40
|
-
async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, planMode = false, permissionMode = '
|
|
58
|
+
async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, planMode = false, permissionMode = 'ask', quiet = false, fallbackModel) {
|
|
41
59
|
const controller = new AbortController();
|
|
42
60
|
process.on('SIGINT', () => {
|
|
43
61
|
controller.abort();
|
|
44
62
|
process.exit(130);
|
|
45
63
|
});
|
|
64
|
+
// budget cap ตั้งไว้แต่ไม่มี pricing สำหรับ model นี้ → cap จะไม่ทำงาน เตือนไม่ให้เงียบ (correctness)
|
|
65
|
+
if (budgetUsd != null && !hasPricingForKey(specKey(model)) && !json) {
|
|
66
|
+
process.stderr.write(`${DIM}⚠ budget $${budgetUsd} ตั้งไว้ แต่ไม่มี pricing สำหรับ ${model} → cap จะไม่ทำงาน ` +
|
|
67
|
+
`(ตั้งราคาเอง: ${BRAND.cliName} config set pricing '{"${specKey(model)}":{"input":1,"output":3}}')${RESET}\n`);
|
|
68
|
+
}
|
|
69
|
+
// เตือน fallback model ด้วย (budget cap re-key ไป fallback ตอน primary ล้ม) — ไม่ซ้ำถ้าทั้งคู่ไม่มี pricing
|
|
70
|
+
if (budgetUsd != null && fallbackModel && fallbackModel !== model && !hasPricingForKey(specKey(fallbackModel)) && !json) {
|
|
71
|
+
process.stderr.write(`${DIM}⚠ fallback model ${fallbackModel} ไม่มี pricing → budget cap จะไม่ทำงานถ้า fallback ถูกใช้${RESET}\n`);
|
|
72
|
+
}
|
|
46
73
|
try {
|
|
47
74
|
const { cost, messages } = await runAgent({
|
|
48
75
|
model,
|
|
76
|
+
fallbackModel,
|
|
49
77
|
prompt,
|
|
50
78
|
history,
|
|
51
79
|
budgetUsd,
|
|
@@ -60,15 +88,23 @@ async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, pl
|
|
|
60
88
|
}
|
|
61
89
|
if (e.type === 'text')
|
|
62
90
|
process.stdout.write(e.text ?? '');
|
|
63
|
-
else if (e.type === 'tool-call')
|
|
91
|
+
else if (e.type === 'tool-call' && !quiet)
|
|
64
92
|
process.stdout.write(`\n${DIM}→ ${e.tool}${RESET}\n`);
|
|
65
93
|
},
|
|
66
94
|
});
|
|
67
|
-
if (!json)
|
|
95
|
+
if (!json && !quiet)
|
|
68
96
|
process.stdout.write(`\n${DIM}${cost.summary()}${RESET}\n`);
|
|
97
|
+
else if (quiet)
|
|
98
|
+
process.stdout.write('\n');
|
|
69
99
|
// จำ session ไว้ทำงานต่อได้ (sanook --continue "...") — แก้ concern AI ลืมว่าทำถึงไหน
|
|
70
100
|
const now = new Date().toISOString();
|
|
71
101
|
await saveSession({ id: newSessionId(), created: now, updated: now, model, cwd: process.cwd(), messages });
|
|
102
|
+
// auto-worklog เข้า second-brain (ถ้าตั้ง brainPath) — "vault จำว่าวันนี้ทำอะไร"
|
|
103
|
+
const { getBrainPath, appendBrainWorklog } = await import('./memory.js');
|
|
104
|
+
const brain = await getBrainPath();
|
|
105
|
+
if (brain) {
|
|
106
|
+
await appendBrainWorklog(brain, { prompt, summary: cost.summary(), model, today: now.slice(0, 10) }).catch(() => { });
|
|
107
|
+
}
|
|
72
108
|
}
|
|
73
109
|
catch (err) {
|
|
74
110
|
const msg = redactKey(err.message);
|
|
@@ -80,38 +116,49 @@ async function runHeadless(model, prompt, budgetUsd, maxSteps, json, history, pl
|
|
|
80
116
|
}
|
|
81
117
|
}
|
|
82
118
|
// อ่านจาก package.json (single source of truth) — กัน version constant drift
|
|
83
|
-
const
|
|
84
|
-
const
|
|
119
|
+
const PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
120
|
+
const VERSION = PACKAGE.version;
|
|
121
|
+
const PACKAGE_NAME = PACKAGE.name;
|
|
122
|
+
const HELP = `${BRAND.productName} — a terminal AI coding agent (BYOK)
|
|
85
123
|
|
|
86
124
|
usage:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
125
|
+
${BRAND.cliName} "<task>" run one task (headless)
|
|
126
|
+
${BRAND.cliName} interactive REPL
|
|
127
|
+
${BRAND.cliName} --json "<task>" headless, JSONL output (for CI/scripts)
|
|
128
|
+
${BRAND.cliName} update update ${BRAND.cliName} to the latest npm release
|
|
129
|
+
${BRAND.cliName} doctor ตรวจการติดตั้ง + วิธีแก้ PATH (เมื่อพิมพ์ "${BRAND.cliName}" แล้วไม่เจอ)
|
|
90
130
|
|
|
91
131
|
gateway (อยู่ยาว 24/7 — HTTP loopback + cron):
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
132
|
+
${BRAND.cliName} serve [--port 8787] เปิด gateway (OpenAI-compat /v1/chat/completions + scheduler)
|
|
133
|
+
${BRAND.cliName} cron add "<when>" "<task>" ตั้งงานล่วงหน้า (when: "every 30m" | "09:00" | ISO | now)
|
|
134
|
+
${BRAND.cliName} cron list ดู task ทั้งหมด
|
|
135
|
+
${BRAND.cliName} cron rm <id> ลบ task
|
|
96
136
|
|
|
97
|
-
skills (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
137
|
+
skills (built-in + ติดตั้งเพิ่มได้):
|
|
138
|
+
${BRAND.cliName} skill list ดู skill ทั้งหมด
|
|
139
|
+
${BRAND.cliName} skill add <user/repo|url|path> ติดตั้ง skill จาก GitHub / URL / local
|
|
140
|
+
${BRAND.cliName} skill remove <name> ลบ skill ที่ติดตั้ง
|
|
141
|
+
${BRAND.cliName} models [provider] ดู/verify model id (เทียบ provider จริงถ้ามี key)
|
|
102
142
|
|
|
103
143
|
second brain (Obsidian workspace สำหรับจัดเก็บงาน + ความจำ AI):
|
|
104
|
-
|
|
144
|
+
${BRAND.cliName} brain init [path] สร้างโครงสร้าง second-brain ที่ path (ไม่ใส่ = ถาม)
|
|
145
|
+
|
|
146
|
+
search (BM25 + optional BYOK semantic เหนือ vault + memory + sessions + skills):
|
|
147
|
+
${BRAND.cliName} index (re)index vault+memory แบบ incremental (O(delta))
|
|
148
|
+
${BRAND.cliName} search "<query>" [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,memory]
|
|
149
|
+
${BRAND.cliName} mcp serve expose brain เป็น MCP server (stdio) ให้ Claude Desktop/Cursor
|
|
105
150
|
|
|
106
151
|
config & mcp:
|
|
107
|
-
|
|
108
|
-
|
|
152
|
+
${BRAND.cliName} config [get|set <k> <v>] ดู/แก้ ${appHomePath('config.json')} (model/budgetUsd/permissionMode/cacheTtl/compaction/thinking)
|
|
153
|
+
${BRAND.cliName} mcp [list|add <name> <cmd> …|remove <name>] จัดการ MCP servers
|
|
154
|
+
${BRAND.cliName} trust [status|add|remove] อนุญาต/ยกเลิก project .sanook mcp/hooks/skills/commands
|
|
109
155
|
|
|
110
156
|
flags:
|
|
111
157
|
-m, --model <spec> sonnet/opus/haiku/fable · gpt/codex · gemini · grok · deepseek · mistral · groq · ollama/lmstudio
|
|
112
158
|
or "provider:model-id" (e.g. openai:gpt-5-codex, groq:fast, google:gemini-2.5-flash)
|
|
113
159
|
-b, --budget <usd> stop when estimated cost exceeds this
|
|
114
|
-
-c, --continue resume the latest session
|
|
160
|
+
-c, --continue resume the latest session ของ project นี้
|
|
161
|
+
--continue-any resume latest session ข้าม project (explicit)
|
|
115
162
|
--plan plan mode — สำรวจ+วางแผนเท่านั้น ไม่แก้ไฟล์ (read-only)
|
|
116
163
|
-y, --yes อนุมัติ tool อัตโนมัติ (ข้าม ask-mode permission)
|
|
117
164
|
--json machine-readable JSONL output
|
|
@@ -119,7 +166,8 @@ flags:
|
|
|
119
166
|
-h, --help
|
|
120
167
|
|
|
121
168
|
env (BYOK — direct API key only):
|
|
122
|
-
ANTHROPIC_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / OPENAI_API_KEY
|
|
169
|
+
ANTHROPIC_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / OPENAI_API_KEY
|
|
170
|
+
${BRAND_ENV.disableUpdateCheck}=1 disable interactive update prompts`;
|
|
123
171
|
/** sanook serve [--port N] [--model spec] — เปิด gateway (HTTP loopback + cron scheduler) อยู่ยาว */
|
|
124
172
|
async function runServe(args) {
|
|
125
173
|
const portIdx = args.indexOf('--port');
|
|
@@ -131,11 +179,12 @@ async function runServe(args) {
|
|
|
131
179
|
const mIdx = args.findIndex((a) => a === '--model' || a === '-m');
|
|
132
180
|
const config = await loadConfig({ model: mIdx !== -1 ? args[mIdx + 1] : undefined });
|
|
133
181
|
const { startGateway } = await import('./gateway/serve.js');
|
|
134
|
-
process.stdout.write(`${DIM}
|
|
182
|
+
process.stdout.write(`${DIM}${BRAND.productName} gateway — model: ${config.model}${RESET}\n`);
|
|
135
183
|
const stop = await startGateway({
|
|
136
184
|
port,
|
|
137
185
|
model: config.model,
|
|
138
186
|
budgetUsd: config.budgetUsd,
|
|
187
|
+
permissionMode: envFlag(BRAND_ENV.gatewayAllowWrite) ? 'auto' : config.permissionMode,
|
|
139
188
|
onLog: (m) => process.stdout.write(`${DIM}[gateway] ${m}${RESET}\n`),
|
|
140
189
|
});
|
|
141
190
|
const shutdown = () => {
|
|
@@ -190,7 +239,7 @@ async function runCron(args) {
|
|
|
190
239
|
if (action === 'list' || action === undefined) {
|
|
191
240
|
const tasks = await listTasks();
|
|
192
241
|
if (!tasks.length) {
|
|
193
|
-
console.log(
|
|
242
|
+
console.log(`ยังไม่มี task — เพิ่มด้วย: ${BRAND.cliName} cron add "every 1h" "เช็คข่าว AI"`);
|
|
194
243
|
return;
|
|
195
244
|
}
|
|
196
245
|
for (const t of tasks) {
|
|
@@ -330,10 +379,10 @@ async function readStdin() {
|
|
|
330
379
|
async function runConfig(args) {
|
|
331
380
|
const { readGlobalConfigRaw, patchGlobalConfig } = await import('./config.js');
|
|
332
381
|
const [action, key, ...rest] = args;
|
|
333
|
-
const ALLOWED = ['model', 'budgetUsd', 'permissionMode', 'brainPath'];
|
|
382
|
+
const ALLOWED = ['model', 'fallbackModel', 'budgetUsd', 'maxSteps', 'permissionMode', 'brainPath', 'pricing', 'cacheTtl', 'compaction', 'thinking', 'summaryModel'];
|
|
334
383
|
if (action === 'set') {
|
|
335
384
|
if (!key || rest.length === 0) {
|
|
336
|
-
console.error(`ใช้:
|
|
385
|
+
console.error(`ใช้: ${BRAND.cliName} config set <key> <value> (key: ${ALLOWED.join(' | ')})`);
|
|
337
386
|
process.exit(1);
|
|
338
387
|
}
|
|
339
388
|
if (!ALLOWED.includes(key)) {
|
|
@@ -341,7 +390,60 @@ async function runConfig(args) {
|
|
|
341
390
|
process.exit(1);
|
|
342
391
|
}
|
|
343
392
|
const raw = rest.join(' ');
|
|
344
|
-
|
|
393
|
+
let value = raw;
|
|
394
|
+
if (key === 'budgetUsd') {
|
|
395
|
+
const n = Number(raw);
|
|
396
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
397
|
+
console.error('budgetUsd ต้องเป็นตัวเลขบวก เช่น 0.25');
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
value = n;
|
|
401
|
+
}
|
|
402
|
+
else if (key === 'maxSteps') {
|
|
403
|
+
const n = Number(raw);
|
|
404
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
405
|
+
console.error('maxSteps ต้องเป็น integer บวก เช่น 20');
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
value = n;
|
|
409
|
+
}
|
|
410
|
+
else if (key === 'permissionMode' && raw !== 'auto' && raw !== 'ask') {
|
|
411
|
+
console.error('permissionMode ต้องเป็น auto หรือ ask');
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
else if (key === 'cacheTtl' && raw !== '5m' && raw !== '1h') {
|
|
415
|
+
console.error('cacheTtl ต้องเป็น 5m หรือ 1h');
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
else if (key === 'compaction' && raw !== 'truncate' && raw !== 'summarize') {
|
|
419
|
+
console.error('compaction ต้องเป็น truncate หรือ summarize');
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
else if (key === 'thinking') {
|
|
423
|
+
// เก็บเป็น number (budget) หรือ boolean ให้ตรง ConfigSchema (ไม่เก็บ string)
|
|
424
|
+
if (raw === 'on' || raw === 'true')
|
|
425
|
+
value = true;
|
|
426
|
+
else if (raw === 'off' || raw === 'false')
|
|
427
|
+
value = false;
|
|
428
|
+
else {
|
|
429
|
+
const n = Number(raw);
|
|
430
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
431
|
+
console.error('thinking ต้องเป็น on/off หรือ budget tokens (integer บวก เช่น 4000)');
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
value = n;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else if (key === 'pricing') {
|
|
438
|
+
try {
|
|
439
|
+
value = parsePricingOverride(raw); // { "provider:model": { input, output, cacheRead?, cacheWrite? } }
|
|
440
|
+
}
|
|
441
|
+
catch (e) {
|
|
442
|
+
console.error(`pricing ต้องเป็น JSON เช่น '{"openai:gpt-5.5":{"input":1.25,"output":10}}' — ${e.message}`);
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
await patchGlobalConfig({ [key]: value });
|
|
345
447
|
console.log(`ตั้ง ${key} = ${raw}`);
|
|
346
448
|
return;
|
|
347
449
|
}
|
|
@@ -350,11 +452,63 @@ async function runConfig(args) {
|
|
|
350
452
|
console.log(cfg[key] ?? '(ไม่ได้ตั้ง)');
|
|
351
453
|
return;
|
|
352
454
|
}
|
|
353
|
-
console.log(
|
|
455
|
+
console.log(`${appHomePath('config.json')}:\n${JSON.stringify(await readGlobalConfigRaw(), null, 2)}`);
|
|
456
|
+
}
|
|
457
|
+
/** sanook index — incremental (re)index of vault + memory + sessions + skills */
|
|
458
|
+
async function runIndex(_args) {
|
|
459
|
+
const { reindex } = await import('./search/indexer.js');
|
|
460
|
+
console.log('indexing…');
|
|
461
|
+
const r = await reindex();
|
|
462
|
+
console.log(`done: +${r.added} ~${r.updated} -${r.removed} (skipped ${r.skipped}) · ` +
|
|
463
|
+
`memory=${r.memory} sessions=${r.sessions} skills=${r.skills}\nvault: ${r.vaultPath ?? '(not set — `' + BRAND.cliName + ' brain init` or set config.brainPath)'}`);
|
|
464
|
+
}
|
|
465
|
+
/** sanook search "<query>" [--mode ..] [--limit N] [--source a,b] — one-shot ranked search */
|
|
466
|
+
async function runSearch(args) {
|
|
467
|
+
const queryParts = [];
|
|
468
|
+
let mode = 'auto';
|
|
469
|
+
let limit = 8;
|
|
470
|
+
let sources;
|
|
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) {
|
|
484
|
+
console.error(`ใช้: ${BRAND.cliName} search "<query>" [--mode auto|fts|semantic|hybrid] [--limit N] [--source vault,memory]`);
|
|
485
|
+
process.exit(1);
|
|
486
|
+
}
|
|
487
|
+
const { search } = await import('./search/engine.js');
|
|
488
|
+
const res = await search(query, { mode: mode, limit, sources: sources });
|
|
489
|
+
if (res.degraded)
|
|
490
|
+
console.log(`${DIM}(mode=${res.mode}, degraded: ${res.degraded})${RESET}`);
|
|
491
|
+
else
|
|
492
|
+
console.log(`${DIM}(mode=${res.mode}, ${res.hits.length} hits)${RESET}`);
|
|
493
|
+
if (!res.hits.length) {
|
|
494
|
+
console.log(`ไม่เจอ "${query}" — ลองรัน ${BRAND.cliName} index ก่อน (ถ้ายังไม่เคย index vault)`);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
for (const h of res.hits) {
|
|
498
|
+
const title = h.title.trim();
|
|
499
|
+
const head = title ? `${title} — ${h.snippet}` : h.snippet;
|
|
500
|
+
const where = h.path ? ` ${DIM}(${h.path})${RESET}` : '';
|
|
501
|
+
console.log(`${DIM}[${h.source}]${RESET} ${head}${where}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/** sanook mcp serve — run the stdio MCP server exposing sanook's brain */
|
|
505
|
+
async function runMcpServe() {
|
|
506
|
+
const { runMcpServer } = await import('./mcp-server.js');
|
|
507
|
+
await runMcpServer();
|
|
354
508
|
}
|
|
355
509
|
/** sanook mcp [list | add <name> <command> [args...] | remove <name>] — จัดการ ~/.sanook/mcp.json */
|
|
356
510
|
async function runMcp(args) {
|
|
357
|
-
const mcpPath =
|
|
511
|
+
const mcpPath = appHomePath('mcp.json');
|
|
358
512
|
let cfg = { mcpServers: {} };
|
|
359
513
|
try {
|
|
360
514
|
const parsed = JSON.parse(await readFile(mcpPath, 'utf8'));
|
|
@@ -365,17 +519,24 @@ async function runMcp(args) {
|
|
|
365
519
|
}
|
|
366
520
|
const write = async () => {
|
|
367
521
|
await mkdir(dirname(mcpPath), { recursive: true });
|
|
368
|
-
await writeFile(mcpPath, `${JSON.stringify(cfg, null, 2)}\n
|
|
522
|
+
await writeFile(mcpPath, `${JSON.stringify(cfg, null, 2)}\n`, { mode: 0o600 });
|
|
523
|
+
await chmod(mcpPath, 0o600).catch(() => { });
|
|
369
524
|
};
|
|
370
525
|
const [action, name, command, ...cmdArgs] = args;
|
|
371
526
|
if (action === 'add') {
|
|
372
527
|
if (!name || !command) {
|
|
373
|
-
console.error(
|
|
528
|
+
console.error(`ใช้: ${BRAND.cliName} mcp add <name> <command> [args...] (เช่น: mcp add fs npx -y @modelcontextprotocol/server-filesystem /path)`);
|
|
529
|
+
console.error(` remote: ${BRAND.cliName} mcp add <name> https://host/mcp (Streamable-HTTP)`);
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
if (!isValidMcpServerName(name)) {
|
|
533
|
+
console.error('ชื่อ MCP server ต้องเป็น a-z/A-Z/0-9/_/- ความยาวไม่เกิน 64 และห้ามใช้ชื่อพิเศษ');
|
|
374
534
|
process.exit(1);
|
|
375
535
|
}
|
|
376
|
-
|
|
536
|
+
// command เป็น http(s):// → remote MCP (Streamable-HTTP), ไม่งั้น stdio
|
|
537
|
+
cfg.mcpServers[name] = /^https?:\/\//.test(command) ? { url: command } : { command, args: cmdArgs };
|
|
377
538
|
await write();
|
|
378
|
-
console.log(`เพิ่ม MCP server "${name}"`);
|
|
539
|
+
console.log(`เพิ่ม MCP server "${name}"${/^https?:\/\//.test(command) ? ' (remote http)' : ''}`);
|
|
379
540
|
return;
|
|
380
541
|
}
|
|
381
542
|
if (action === 'remove' || action === 'rm') {
|
|
@@ -390,23 +551,174 @@ async function runMcp(args) {
|
|
|
390
551
|
}
|
|
391
552
|
const names = Object.keys(cfg.mcpServers);
|
|
392
553
|
if (!names.length) {
|
|
393
|
-
console.log(
|
|
554
|
+
console.log(`ยังไม่มี MCP server — เพิ่ม: ${BRAND.cliName} mcp add <name> <command> [args...]`);
|
|
394
555
|
return;
|
|
395
556
|
}
|
|
396
557
|
console.log(`${names.length} MCP servers:`);
|
|
397
|
-
for (const n of names)
|
|
398
|
-
|
|
558
|
+
for (const n of names) {
|
|
559
|
+
const s = cfg.mcpServers[n];
|
|
560
|
+
console.log(` ${n} — ${s.url ? `${s.url} (http)` : `${s.command} ${(s.args ?? []).join(' ')}`}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
/** sanook trust [status|add|remove] — trust project .sanook content that can steer/execute code */
|
|
564
|
+
async function runTrust(args) {
|
|
565
|
+
const action = args[0] ?? 'status';
|
|
566
|
+
const { projectTrustStatus, trustProject, untrustProject } = await import('./trust.js');
|
|
567
|
+
if (action === 'status') {
|
|
568
|
+
const s = await projectTrustStatus();
|
|
569
|
+
console.log(`${s.trusted ? 'trusted' : 'untrusted'} — ${s.root}${s.reason === 'env' ? ' (env override)' : ''}`);
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (action === 'add') {
|
|
573
|
+
const root = await trustProject();
|
|
574
|
+
console.log(`trusted project: ${root}`);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (action === 'remove' || action === 'rm') {
|
|
578
|
+
const root = await untrustProject();
|
|
579
|
+
console.log(`removed trust: ${root}`);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
console.error(`ไม่รู้จัก: trust ${action} — ใช้ status / add / remove`);
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
585
|
+
/** sanook update — one-command update path for globally installed CLI */
|
|
586
|
+
async function runUpdate(args) {
|
|
587
|
+
const checkOnly = args.includes('--check');
|
|
588
|
+
const unknown = args.filter((a) => a !== '--check');
|
|
589
|
+
if (unknown.length) {
|
|
590
|
+
console.error(`ใช้: ${BRAND.cliName} update [--check]`);
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
const { checkForUpdate, installLatest } = await import('./update.js');
|
|
594
|
+
try {
|
|
595
|
+
console.log(`เช็กอัปเดต ${PACKAGE_NAME}...`);
|
|
596
|
+
const check = await checkForUpdate({ name: PACKAGE_NAME, version: VERSION });
|
|
597
|
+
if (!check.isOutdated) {
|
|
598
|
+
console.log(`คุณใช้เวอร์ชันล่าสุดแล้ว (${check.currentVersion})`);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
console.log(`มีเวอร์ชันใหม่: ${check.currentVersion} → ${check.latestVersion}`);
|
|
602
|
+
console.log(`คำสั่งอัปเดต: ${check.installCommand}`);
|
|
603
|
+
if (checkOnly) {
|
|
604
|
+
console.log(`รัน "${BRAND.cliName} update" เพื่ออัปเดต`);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const code = await installLatest({ name: PACKAGE_NAME, version: VERSION });
|
|
608
|
+
if (code !== 0) {
|
|
609
|
+
console.error(`อัปเดตไม่สำเร็จ (npm exit ${code}) — ลองรันเอง: ${check.installCommand}`);
|
|
610
|
+
process.exit(code);
|
|
611
|
+
}
|
|
612
|
+
console.log(`อัปเดตสำเร็จ — ตรวจสอบด้วย: ${BRAND.cliName} --version`);
|
|
613
|
+
}
|
|
614
|
+
catch (e) {
|
|
615
|
+
console.error(`เช็ก/อัปเดตไม่สำเร็จ: ${redactKey(e.message)}`);
|
|
616
|
+
console.error(`ลองรันเอง: npm install -g ${PACKAGE_NAME}@latest`);
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const UPDATE_CACHE_PATH = appHomePath('update-check.json');
|
|
621
|
+
async function readUpdateCache() {
|
|
622
|
+
try {
|
|
623
|
+
const parsed = JSON.parse(await readFile(UPDATE_CACHE_PATH, 'utf8'));
|
|
624
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
return {};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
async function writeUpdateCache(latestVersion) {
|
|
631
|
+
await mkdir(dirname(UPDATE_CACHE_PATH), { recursive: true });
|
|
632
|
+
await writeFile(UPDATE_CACHE_PATH, `${JSON.stringify({ checkedAt: new Date().toISOString(), latestVersion }, null, 2)}\n`, { mode: 0o600 });
|
|
633
|
+
await chmod(UPDATE_CACHE_PATH, 0o600).catch(() => { });
|
|
634
|
+
}
|
|
635
|
+
async function askYesNo(question) {
|
|
636
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
637
|
+
try {
|
|
638
|
+
const answer = (await rl.question(question)).trim().toLowerCase();
|
|
639
|
+
return answer === '' || answer === 'y' || answer === 'yes';
|
|
640
|
+
}
|
|
641
|
+
finally {
|
|
642
|
+
rl.close();
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
async function maybePromptForInteractiveUpdate() {
|
|
646
|
+
if (envFlag(BRAND_ENV.disableUpdateCheck) || process.env.CI)
|
|
647
|
+
return;
|
|
648
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
649
|
+
return;
|
|
650
|
+
const { checkForUpdate, installLatest, shouldCheckForUpdate } = await import('./update.js');
|
|
651
|
+
const cache = await readUpdateCache();
|
|
652
|
+
if (!shouldCheckForUpdate(cache))
|
|
653
|
+
return;
|
|
654
|
+
try {
|
|
655
|
+
const check = await checkForUpdate({ name: PACKAGE_NAME, version: VERSION }, { timeoutMs: 2500 });
|
|
656
|
+
await writeUpdateCache(check.latestVersion).catch(() => { });
|
|
657
|
+
if (!check.isOutdated)
|
|
658
|
+
return;
|
|
659
|
+
process.stdout.write(`\nมี ${BRAND.productName} CLI เวอร์ชันใหม่: ${check.currentVersion} → ${check.latestVersion}\n` +
|
|
660
|
+
`อัปเดตตอนนี้ด้วย "${BRAND.cliName} update" ไหม? [Y/n] `);
|
|
661
|
+
const ok = await askYesNo('');
|
|
662
|
+
if (!ok) {
|
|
663
|
+
process.stdout.write(`ข้ามอัปเดตตอนนี้ — อัปเดตภายหลังได้ด้วย: ${BRAND.cliName} update\n\n`);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const code = await installLatest({ name: PACKAGE_NAME, version: VERSION });
|
|
667
|
+
if (code !== 0) {
|
|
668
|
+
process.stdout.write(`อัปเดตไม่สำเร็จ (npm exit ${code}) — ลองรันเอง: ${check.installCommand}\n\n`);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
process.stdout.write(`อัปเดตสำเร็จ — เปิด ${BRAND.cliName} ใหม่เพื่อใช้เวอร์ชันล่าสุด\n\n`);
|
|
672
|
+
process.exit(0);
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
// update notifier ต้องไม่ block การเปิด TUI ถ้า offline/registry ล่ม/cache พัง
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/** headless: model ต้อง key แต่ env ยังไม่มี → คืนข้อความแนะวิธีเริ่ม (null = พร้อมใช้) */
|
|
679
|
+
function headlessKeyHint(modelSpec) {
|
|
680
|
+
const { provider } = parseSpec(modelSpec);
|
|
681
|
+
const cfg = PROVIDERS[provider];
|
|
682
|
+
if (!cfg?.requiresKey || resolveKeyFromEnv(cfg.envVar, cfg.envFallbacks))
|
|
683
|
+
return null;
|
|
684
|
+
const url = consoleUrl(provider);
|
|
685
|
+
const lines = [
|
|
686
|
+
`⚠ ยังไม่มี API key สำหรับ ${cfg.label} (${cfg.envVar})`,
|
|
687
|
+
`เริ่มใช้งาน:`,
|
|
688
|
+
` • รัน "${BRAND.cliName}" (ไม่ใส่ task) → setup wizard ทีละขั้น (แนะนำ)`,
|
|
689
|
+
` • หรือ: export ${cfg.envVar}="..."${url ? ` · เอา key ที่: ${url}` : ''}`,
|
|
690
|
+
];
|
|
691
|
+
const other = detectEnvProvider();
|
|
692
|
+
if (other && other.provider !== provider) {
|
|
693
|
+
lines.push(` • เจอ key ของ ${other.label} อยู่แล้ว → ใช้เลย: ${BRAND.cliName} -m ${other.provider} "<task>"`);
|
|
694
|
+
}
|
|
695
|
+
return lines.join('\n');
|
|
399
696
|
}
|
|
400
697
|
async function main() {
|
|
698
|
+
// Node ≥ 22 required (uses node:fs glob, AbortSignal.timeout, ฯลฯ) — บอกชัดแทนปล่อย crash งงๆ
|
|
699
|
+
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
700
|
+
if (Number.isFinite(nodeMajor) && nodeMajor < 22) {
|
|
701
|
+
console.error(`${BRAND.productName} ต้องใช้ Node.js เวอร์ชัน 22 ขึ้นไป — ตอนนี้ใช้ ${process.version}\n` +
|
|
702
|
+
`อัปเดต Node ที่ https://nodejs.org (หรือ nvm/fnm/volta) แล้วลองใหม่`);
|
|
703
|
+
process.exit(1);
|
|
704
|
+
}
|
|
401
705
|
const argv = process.argv.slice(2);
|
|
402
|
-
if (argv.
|
|
706
|
+
if (argv.length === 1 && (argv[0] === '-v' || argv[0] === '--version')) {
|
|
403
707
|
console.log(VERSION);
|
|
404
708
|
return;
|
|
405
709
|
}
|
|
406
|
-
if (argv.
|
|
710
|
+
if (argv.length === 1 && (argv[0] === '-h' || argv[0] === '--help')) {
|
|
407
711
|
console.log(HELP);
|
|
408
712
|
return;
|
|
409
713
|
}
|
|
714
|
+
if (argv[0] === 'update')
|
|
715
|
+
return runUpdate(argv.slice(1));
|
|
716
|
+
// doctor — ไม่ต้องโหลด key/mcp; ตรวจ Node/PATH/global-bin แล้วบอกวิธีแก้ "sanook ไม่เจอ"
|
|
717
|
+
if (argv[0] === 'doctor') {
|
|
718
|
+
const { runDoctor } = await import('./doctor.js');
|
|
719
|
+
console.log(await runDoctor(PACKAGE_NAME));
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
410
722
|
// โหลด API key จาก ~/.sanook/auth.json เข้า env (ไม่ override env ที่ตั้งไว้แล้ว)
|
|
411
723
|
await loadKeysIntoEnv();
|
|
412
724
|
process.on('exit', closeMcp); // ปิด MCP server (kill child) ตอนจบ
|
|
@@ -425,34 +737,70 @@ async function main() {
|
|
|
425
737
|
return runBrain(argv.slice(1));
|
|
426
738
|
if (argv[0] === 'config' && ['get', 'set', 'list', undefined].includes(argv[1]))
|
|
427
739
|
return runConfig(argv.slice(1));
|
|
740
|
+
if (argv[0] === 'index' && (argv.length === 1 || argv[1].startsWith('--')))
|
|
741
|
+
return runIndex(argv.slice(1));
|
|
742
|
+
if (argv[0] === 'search' && argv.length > 1)
|
|
743
|
+
return runSearch(argv.slice(1));
|
|
744
|
+
if (argv[0] === 'mcp' && argv[1] === 'serve')
|
|
745
|
+
return runMcpServe();
|
|
428
746
|
if (argv[0] === 'mcp' && ['add', 'list', 'remove', 'rm', undefined].includes(argv[1]))
|
|
429
747
|
return runMcp(argv.slice(1));
|
|
430
|
-
|
|
748
|
+
if (argv[0] === 'trust' && ['status', 'add', 'remove', 'rm', undefined].includes(argv[1]))
|
|
749
|
+
return runTrust(argv.slice(1));
|
|
750
|
+
const { model, budget, json, quiet, prompt: argPrompt, planMode, yes } = parseArgs(argv);
|
|
431
751
|
const budgetUsd = Number.isFinite(budget) ? budget : undefined;
|
|
432
752
|
// stdin piping: `git diff | sanook "review this"` → ผนวก stdin เข้า prompt (headless/CI)
|
|
433
753
|
const piped = process.stdin.isTTY ? '' : (await readStdin()).trim();
|
|
434
754
|
const prompt = piped ? `${argPrompt}\n\n<stdin>\n${piped}\n</stdin>`.trim() : argPrompt;
|
|
435
755
|
if (prompt) {
|
|
436
756
|
const config = await loadConfig({ model, budgetUsd });
|
|
757
|
+
// headless + ยังไม่มี key → บอกวิธีเริ่มแบบ actionable แทนปล่อยให้ throw error ดิบ (กัน dead-end ของ flow ที่ README แนะนำ)
|
|
758
|
+
const noKey = headlessKeyHint(config.model);
|
|
759
|
+
if (noKey) {
|
|
760
|
+
process.stderr.write(`${noKey}\n`);
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
437
763
|
// --continue / -c → โหลด session ล่าสุดมาต่อ (จำว่าทำถึงไหน)
|
|
438
|
-
const
|
|
439
|
-
|
|
764
|
+
const wantsContinue = argv.includes('--continue') || argv.includes('-c') || argv.includes('--continue-any');
|
|
765
|
+
const history = wantsContinue
|
|
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);
|
|
440
769
|
return;
|
|
441
770
|
}
|
|
442
|
-
|
|
771
|
+
await maybePromptForInteractiveUpdate();
|
|
772
|
+
// interactive — ครั้งแรก (ยังไม่มี config): ถ้าไม่มี key ใช้ได้ใน env → ต้องโชว์ wizard
|
|
773
|
+
let needsSetup = false;
|
|
443
774
|
if (await isFirstRun()) {
|
|
444
|
-
|
|
445
|
-
|
|
775
|
+
// provider เป้าหมาย: เคารพ -m ที่ user ใส่ก่อน (กันขึ้น "พร้อมใช้" ผิด provider), ไม่งั้น scan env ตามนิยม
|
|
776
|
+
const flagProvider = model ? parseSpec(model).provider : undefined;
|
|
777
|
+
const target = flagProvider ?? detectEnvProvider()?.provider;
|
|
778
|
+
const tcfg = target ? PROVIDERS[target] : undefined;
|
|
779
|
+
if (target && tcfg && hasUsableEnvKey(target)) {
|
|
780
|
+
// มี key ใช้ได้จริง (ผ่าน policy ไม่ใช่ OAuth) → ข้าม wizard, ตั้ง default, บอกว่าพร้อมใช้
|
|
781
|
+
const { saveGlobalConfig } = await import('./config.js');
|
|
782
|
+
await saveGlobalConfig({ model: model ?? `${target}:${tcfg.models.default}`, provider: target });
|
|
783
|
+
console.log(`✅ ${tcfg.label} พร้อมใช้เลย (ข้าม setup wizard)\n`);
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
needsSetup = true; // ไม่มี provider ที่ key ใช้ได้ (หรือ -m provider ไม่มี key) → wizard (รัน Ink เดียวกับ REPL)
|
|
787
|
+
}
|
|
446
788
|
}
|
|
447
789
|
const config = await loadConfig({ model, budgetUsd });
|
|
448
790
|
// --continue / -c → โหลด conversation ล่าสุดเข้า REPL (เดิม resume ได้แค่ headless)
|
|
449
|
-
const initialHistory = argv.includes('--continue') || argv.includes('-c')
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
791
|
+
const initialHistory = argv.includes('--continue') || argv.includes('-c') || argv.includes('--continue-any')
|
|
792
|
+
? (await latestSession(argv.includes('--continue-any') ? null : process.cwd()))?.messages
|
|
793
|
+
: undefined;
|
|
794
|
+
const { startApp } = await import('./ui/render.js');
|
|
795
|
+
startApp({
|
|
796
|
+
needsSetup,
|
|
797
|
+
appProps: {
|
|
798
|
+
initialModel: config.model,
|
|
799
|
+
fallbackModel: config.fallbackModel,
|
|
800
|
+
budgetUsd: config.budgetUsd,
|
|
801
|
+
permissionMode: yes ? 'auto' : config.permissionMode,
|
|
802
|
+
initialHistory,
|
|
803
|
+
},
|
|
456
804
|
});
|
|
457
805
|
}
|
|
458
806
|
main().catch((err) => {
|