sanook-cli 0.4.0
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 +23 -0
- package/CHANGELOG.md +38 -0
- package/LICENSE +201 -0
- package/README.md +239 -0
- package/dist/agentContext.js +2 -0
- package/dist/approval.js +78 -0
- package/dist/bin.js +461 -0
- package/dist/brain.js +186 -0
- package/dist/commands.js +66 -0
- package/dist/compaction.js +85 -0
- package/dist/config.js +101 -0
- package/dist/cost.js +59 -0
- package/dist/diff.js +36 -0
- package/dist/gateway/auth.js +32 -0
- package/dist/gateway/ledger.js +94 -0
- package/dist/gateway/lock.js +114 -0
- package/dist/gateway/schedule.js +74 -0
- package/dist/gateway/scheduler.js +87 -0
- package/dist/gateway/serve.js +57 -0
- package/dist/gateway/server.js +94 -0
- package/dist/gateway/telegram.js +115 -0
- package/dist/git.js +55 -0
- package/dist/hooks.js +104 -0
- package/dist/knowledge.js +68 -0
- package/dist/loop.js +169 -0
- package/dist/mcp.js +191 -0
- package/dist/memory.js +108 -0
- package/dist/providers/codex.js +86 -0
- package/dist/providers/keys.js +37 -0
- package/dist/providers/models.js +55 -0
- package/dist/providers/registry.js +241 -0
- package/dist/session.js +36 -0
- package/dist/skill-install.js +190 -0
- package/dist/skills.js +111 -0
- package/dist/tools/bash.js +26 -0
- package/dist/tools/edit.js +107 -0
- package/dist/tools/git.js +68 -0
- package/dist/tools/index.js +36 -0
- package/dist/tools/list.js +24 -0
- package/dist/tools/permission.js +30 -0
- package/dist/tools/read.js +18 -0
- package/dist/tools/recall.js +12 -0
- package/dist/tools/remember.js +14 -0
- package/dist/tools/schedule.js +61 -0
- package/dist/tools/search.js +54 -0
- package/dist/tools/skill.js +65 -0
- package/dist/tools/task.js +46 -0
- package/dist/tools/util.js +5 -0
- package/dist/tools/write.js +27 -0
- package/dist/ui/app.js +132 -0
- package/dist/ui/banner.js +20 -0
- package/dist/ui/brain-wizard.js +29 -0
- package/dist/ui/render.js +57 -0
- package/dist/ui/setup.js +46 -0
- package/package.json +77 -0
- package/second-brain/AGENTS.md +18 -0
- package/second-brain/CLAUDE.md +96 -0
- package/second-brain/Evals/retrieval-eval.md +30 -0
- package/second-brain/GEMINI.md +15 -0
- package/second-brain/Home.md +33 -0
- package/second-brain/README.md +29 -0
- package/second-brain/Runbooks/ingest-quarantine.md +27 -0
- package/second-brain/Runbooks/sleep-time-consolidation.md +26 -0
- package/second-brain/Shared/AI-Context-Index.md +52 -0
- package/second-brain/Shared/Core-Facts/protected-facts.md +21 -0
- package/second-brain/Shared/Decision-Memory/decision-log.md +24 -0
- package/second-brain/Shared/Memory-Inbox/memory-inbox.md +23 -0
- package/second-brain/Shared/Operating-State/current-state.md +30 -0
- package/second-brain/Shared/Provenance/ingest-log.md +27 -0
- package/second-brain/Shared/Rules/context-assembly-policy.md +28 -0
- package/second-brain/Shared/Rules/frontmatter-standard.md +33 -0
- package/second-brain/Shared/Rules/skills-admission.md +30 -0
- package/second-brain/Shared/User-Memory/user-preferences.md +25 -0
- package/second-brain/Templates/bug.md +22 -0
- package/second-brain/Templates/handoff.md +21 -0
- package/second-brain/Templates/project.md +24 -0
- package/second-brain/Templates/session.md +26 -0
- package/second-brain/USER.md +36 -0
- package/second-brain/Vault Structure Map.md +106 -0
- package/skills/agent-tool-mcp-builder/SKILL.md +88 -0
- package/skills/api-design-review/SKILL.md +70 -0
- package/skills/async-concurrency-correctness/SKILL.md +93 -0
- package/skills/audit-accessibility-wcag/SKILL.md +59 -0
- package/skills/audit-technical-seo/SKILL.md +62 -0
- package/skills/auth-jwt-session/SKILL.md +88 -0
- package/skills/brainstorm-design/SKILL.md +73 -0
- package/skills/build-etl-pipeline/SKILL.md +58 -0
- package/skills/build-form-validation/SKILL.md +103 -0
- package/skills/build-office-docs/SKILL.md +80 -0
- package/skills/build-react-component/SKILL.md +116 -0
- package/skills/build-spreadsheet/SKILL.md +106 -0
- package/skills/caching-strategy/SKILL.md +75 -0
- package/skills/cicd-pipeline-author/SKILL.md +65 -0
- package/skills/cloud-cost-optimize/SKILL.md +91 -0
- package/skills/code-comments/SKILL.md +52 -0
- package/skills/code-review/SKILL.md +61 -0
- package/skills/db-migration-safety/SKILL.md +67 -0
- package/skills/debug-frontend-browser/SKILL.md +58 -0
- package/skills/debug-root-cause/SKILL.md +54 -0
- package/skills/dependency-upgrade/SKILL.md +56 -0
- package/skills/deploy-release/SKILL.md +64 -0
- package/skills/diff-table-parity/SKILL.md +58 -0
- package/skills/dockerfile-optimize/SKILL.md +82 -0
- package/skills/error-message/SKILL.md +58 -0
- package/skills/estimate-work/SKILL.md +54 -0
- package/skills/explore-codebase/SKILL.md +73 -0
- package/skills/git-commit-pr/SKILL.md +65 -0
- package/skills/gitops-deploy-workflow/SKILL.md +97 -0
- package/skills/implement-from-design/SKILL.md +69 -0
- package/skills/incident-response-sre/SKILL.md +78 -0
- package/skills/k8s-debug-workload/SKILL.md +135 -0
- package/skills/k8s-manifest-review/SKILL.md +86 -0
- package/skills/llm-eval-harness/SKILL.md +63 -0
- package/skills/manage-client-server-state/SKILL.md +94 -0
- package/skills/mermaid-diagram/SKILL.md +61 -0
- package/skills/message-queue-jobs/SKILL.md +139 -0
- package/skills/naming-helper/SKILL.md +57 -0
- package/skills/observability-instrument/SKILL.md +113 -0
- package/skills/optimize-core-web-vitals/SKILL.md +75 -0
- package/skills/optimize-sql-query/SKILL.md +67 -0
- package/skills/performance-profiling/SKILL.md +65 -0
- package/skills/process-pdf/SKILL.md +107 -0
- package/skills/profile-dataset/SKILL.md +97 -0
- package/skills/prompt-engineering/SKILL.md +70 -0
- package/skills/rag-pipeline/SKILL.md +53 -0
- package/skills/rate-limiting/SKILL.md +96 -0
- package/skills/refactor-cleanup/SKILL.md +54 -0
- package/skills/regex-build/SKILL.md +72 -0
- package/skills/release-notes/SKILL.md +79 -0
- package/skills/rest-graphql-contract/SKILL.md +71 -0
- package/skills/scrape-structured-web-data/SKILL.md +61 -0
- package/skills/secrets-management/SKILL.md +96 -0
- package/skills/security-review/SKILL.md +62 -0
- package/skills/shell-script-robust/SKILL.md +71 -0
- package/skills/style-responsive-tailwind/SKILL.md +70 -0
- package/skills/terraform-plan-review/SKILL.md +95 -0
- package/skills/type-safety-strict/SKILL.md +82 -0
- package/skills/validate-data-quality/SKILL.md +62 -0
- package/skills/wrangle-tabular-data/SKILL.md +75 -0
- package/skills/write-adr/SKILL.md +75 -0
- package/skills/write-analytical-sql/SKILL.md +71 -0
- package/skills/write-data-viz/SKILL.md +58 -0
- package/skills/write-docs/SKILL.md +54 -0
- package/skills/write-plan/SKILL.md +59 -0
- package/skills/write-playwright-e2e/SKILL.md +86 -0
- package/skills/write-prd/SKILL.md +65 -0
- package/skills/write-rfc/SKILL.md +75 -0
- package/skills/write-tests/SKILL.md +50 -0
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
2
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
3
|
+
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
4
|
+
import { createDeepSeek } from '@ai-sdk/deepseek';
|
|
5
|
+
import { createXai } from '@ai-sdk/xai';
|
|
6
|
+
import { createMistral } from '@ai-sdk/mistral';
|
|
7
|
+
import { createGroq } from '@ai-sdk/groq';
|
|
8
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
9
|
+
import { resolveKeyFromEnv, assertDirectApiKey } from './keys.js';
|
|
10
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
// PROVIDER TABLE — เพิ่มค่าย = เพิ่ม 1 entry (loop/cost/keys ไม่ต้องแตะ)
|
|
12
|
+
// auth/format/OAuth-reject verify มิ.ย. 2026 (ดู Research/provider-connect-matrix)
|
|
13
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
14
|
+
export const PROVIDERS = {
|
|
15
|
+
// ── ToS-sensitive: Anthropic/Google แบน OAuth/subscription reuse ────────────
|
|
16
|
+
anthropic: {
|
|
17
|
+
id: 'anthropic',
|
|
18
|
+
label: 'Anthropic (Claude)',
|
|
19
|
+
envVar: 'ANTHROPIC_API_KEY',
|
|
20
|
+
baseURL: 'https://api.anthropic.com/v1',
|
|
21
|
+
requiresKey: true,
|
|
22
|
+
keyFormat: /^sk-ant-api\d{2}-/,
|
|
23
|
+
oauthRejectPrefixes: ['sk-ant-oat'], // Claude.ai subscription OAuth → banned
|
|
24
|
+
models: {
|
|
25
|
+
default: 'claude-opus-4-8',
|
|
26
|
+
opus: 'claude-opus-4-8',
|
|
27
|
+
sonnet: 'claude-sonnet-4-6',
|
|
28
|
+
haiku: 'claude-haiku-4-5',
|
|
29
|
+
fast: 'claude-haiku-4-5',
|
|
30
|
+
fable: 'claude-fable-5',
|
|
31
|
+
},
|
|
32
|
+
create: (key, baseURL) => createAnthropic({ apiKey: key, baseURL }),
|
|
33
|
+
note: 'API key only (x-api-key, sk-ant-api03-). ห้าม reuse Claude.ai OAuth — banned',
|
|
34
|
+
},
|
|
35
|
+
google: {
|
|
36
|
+
id: 'google',
|
|
37
|
+
label: 'Google Gemini',
|
|
38
|
+
envVar: 'GOOGLE_GENERATIVE_AI_API_KEY',
|
|
39
|
+
envFallbacks: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'],
|
|
40
|
+
requiresKey: true,
|
|
41
|
+
keyFormat: /^AIza[0-9A-Za-z_-]{35}$/,
|
|
42
|
+
oauthRejectPrefixes: ['ya29.', 'AQ.'], // Google OAuth / restricted token → banned
|
|
43
|
+
models: {
|
|
44
|
+
default: 'gemini-2.5-pro',
|
|
45
|
+
pro: 'gemini-2.5-pro',
|
|
46
|
+
flash: 'gemini-2.5-flash',
|
|
47
|
+
gemini: 'gemini-2.5-pro',
|
|
48
|
+
},
|
|
49
|
+
create: (key) => createGoogleGenerativeAI({ apiKey: key }),
|
|
50
|
+
note: 'AI Studio key (AIza). restrict เป็น Gemini-only. ห้าม reuse Gemini CLI OAuth',
|
|
51
|
+
},
|
|
52
|
+
// ── Cloud BYOK: commercial OK, ไม่มี OAuth landmine ─────────────────────────
|
|
53
|
+
openai: {
|
|
54
|
+
id: 'openai',
|
|
55
|
+
label: 'OpenAI',
|
|
56
|
+
envVar: 'OPENAI_API_KEY',
|
|
57
|
+
baseURL: 'https://api.openai.com/v1',
|
|
58
|
+
requiresKey: true,
|
|
59
|
+
keyFormat: /^sk-/,
|
|
60
|
+
models: {
|
|
61
|
+
default: 'gpt-5.5',
|
|
62
|
+
smart: 'gpt-5.5',
|
|
63
|
+
fast: 'gpt-5.4-mini',
|
|
64
|
+
gpt: 'gpt-5.5',
|
|
65
|
+
codex: 'gpt-5.3-codex', // coding-tuned (gpt-5-codex deprecated มิ.ย. 2026 — doc audit); เรียกผ่าน OpenAI API ปกติ
|
|
66
|
+
},
|
|
67
|
+
create: (key, baseURL) => createOpenAI({ apiKey: key, baseURL }),
|
|
68
|
+
note: 'Bearer key. org/project ผ่าน env. ห้าม reuse ChatGPT/Codex OAuth',
|
|
69
|
+
},
|
|
70
|
+
deepseek: {
|
|
71
|
+
id: 'deepseek',
|
|
72
|
+
label: 'DeepSeek',
|
|
73
|
+
envVar: 'DEEPSEEK_API_KEY',
|
|
74
|
+
requiresKey: true,
|
|
75
|
+
keyFormat: null, // opaque sk- → ข้าม format check
|
|
76
|
+
// V4 ids (doc audit มิ.ย. 2026): deepseek-chat/deepseek-reasoner เลิกใช้ 2026-07-24 → redirect มา V4 (dual thinking-mode)
|
|
77
|
+
models: { default: 'deepseek-v4-flash', smart: 'deepseek-v4-pro', fast: 'deepseek-v4-flash' },
|
|
78
|
+
create: (key) => createDeepSeek({ apiKey: key }),
|
|
79
|
+
},
|
|
80
|
+
xai: {
|
|
81
|
+
id: 'xai',
|
|
82
|
+
label: 'xAI Grok',
|
|
83
|
+
envVar: 'XAI_API_KEY',
|
|
84
|
+
requiresKey: true,
|
|
85
|
+
keyFormat: /^xai-[A-Za-z0-9]{16,}$/,
|
|
86
|
+
// grok-4 (snapshot grok-4-0709) retired 2026-05-15 → redirect grok-4.3 (doc audit มิ.ย. 2026)
|
|
87
|
+
models: { default: 'grok-4.3', smart: 'grok-4.3', grok: 'grok-4.3' },
|
|
88
|
+
create: (key) => createXai({ apiKey: key }),
|
|
89
|
+
},
|
|
90
|
+
mistral: {
|
|
91
|
+
id: 'mistral',
|
|
92
|
+
label: 'Mistral',
|
|
93
|
+
envVar: 'MISTRAL_API_KEY',
|
|
94
|
+
requiresKey: true,
|
|
95
|
+
keyFormat: null,
|
|
96
|
+
models: { default: 'mistral-large-latest', smart: 'mistral-large-latest', fast: 'mistral-small-latest' },
|
|
97
|
+
create: (key) => createMistral({ apiKey: key }),
|
|
98
|
+
},
|
|
99
|
+
groq: {
|
|
100
|
+
id: 'groq',
|
|
101
|
+
label: 'Groq',
|
|
102
|
+
envVar: 'GROQ_API_KEY',
|
|
103
|
+
requiresKey: true,
|
|
104
|
+
keyFormat: /^gsk_[A-Za-z0-9]{20,}$/,
|
|
105
|
+
models: { default: 'llama-3.3-70b-versatile', fast: 'llama-3.3-70b-versatile' },
|
|
106
|
+
create: (key) => createGroq({ apiKey: key }),
|
|
107
|
+
},
|
|
108
|
+
// ── Local: OpenAI-compatible, ไม่ต้อง key (placeholder ถ้า client บังคับ) ────
|
|
109
|
+
ollama: {
|
|
110
|
+
id: 'ollama',
|
|
111
|
+
label: 'Ollama (local)',
|
|
112
|
+
envVar: 'OLLAMA_BASE_URL',
|
|
113
|
+
baseURL: 'http://localhost:11434/v1', // /v1 (generic OpenAI-compat) ไม่ใช่ /api
|
|
114
|
+
requiresKey: false,
|
|
115
|
+
localPlaceholderKey: 'ollama',
|
|
116
|
+
keyFormat: null,
|
|
117
|
+
models: { default: 'qwen3', llama: 'llama3.3' },
|
|
118
|
+
create: (key, baseURL) => createOpenAICompatible({ name: 'ollama', apiKey: key, baseURL: baseURL ?? 'http://localhost:11434/v1' }),
|
|
119
|
+
note: 'OpenAI-compat /v1 endpoint. ไม่ต้อง key',
|
|
120
|
+
},
|
|
121
|
+
lmstudio: {
|
|
122
|
+
id: 'lmstudio',
|
|
123
|
+
label: 'LM Studio (local)',
|
|
124
|
+
envVar: 'LMSTUDIO_BASE_URL',
|
|
125
|
+
baseURL: 'http://localhost:1234/v1',
|
|
126
|
+
requiresKey: false,
|
|
127
|
+
localPlaceholderKey: 'lm-studio',
|
|
128
|
+
keyFormat: null,
|
|
129
|
+
models: { default: 'local-model' },
|
|
130
|
+
create: (key, baseURL) => createOpenAICompatible({ name: 'lmstudio', apiKey: key, baseURL: baseURL ?? 'http://localhost:1234/v1' }),
|
|
131
|
+
note: 'ต้อง Start Server ในแอปก่อน; โหลด model เดียว ใส่ id อะไรก็ serve ตัวนั้น',
|
|
132
|
+
},
|
|
133
|
+
// ── Cloud BYOK (OpenAI-compatible, จีน — data residency, ไม่มี OAuth landmine) ──
|
|
134
|
+
minimax: {
|
|
135
|
+
id: 'minimax',
|
|
136
|
+
label: 'MiniMax',
|
|
137
|
+
envVar: 'MINIMAX_API_KEY',
|
|
138
|
+
baseURL: 'https://api.minimax.io/v1',
|
|
139
|
+
requiresKey: true,
|
|
140
|
+
keyFormat: null, // opaque
|
|
141
|
+
models: { default: 'MiniMax-M2.7', smart: 'MiniMax-M3', fast: 'MiniMax-M2.7' },
|
|
142
|
+
create: (key, baseURL) => createOpenAICompatible({ name: 'minimax', apiKey: key, baseURL: baseURL ?? 'https://api.minimax.io/v1' }),
|
|
143
|
+
note: 'OpenAI-compat /v1. data จีน. MINIMAX_BASE_URL override (intl ↔ api.minimaxi.com/v1)',
|
|
144
|
+
},
|
|
145
|
+
glm: {
|
|
146
|
+
id: 'glm',
|
|
147
|
+
label: 'GLM (z.ai / Zhipu Coding Plan)',
|
|
148
|
+
envVar: 'ZHIPU_API_KEY',
|
|
149
|
+
envFallbacks: ['ZAI_API_KEY', 'GLM_API_KEY'],
|
|
150
|
+
// Coding Plan (subscription) ใช้ Anthropic Messages API — เหมือนที่ต่อกับ Claude Code.
|
|
151
|
+
// pay-as-you-go /paas/v4 (OpenAI-compat) มีแค่ glm-4.5-flash ฟรี ที่เหลือ 429 ถ้าไม่มี balance
|
|
152
|
+
baseURL: 'https://api.z.ai/api/anthropic/v1',
|
|
153
|
+
requiresKey: true,
|
|
154
|
+
keyFormat: null, // opaque ({id}.{secret})
|
|
155
|
+
models: { default: 'glm-4.6', smart: 'glm-5.1', air: 'glm-4.5-air', glm: 'glm-4.6' },
|
|
156
|
+
create: (key, baseURL) => createAnthropic({ apiKey: key, baseURL: baseURL ?? 'https://api.z.ai/api/anthropic/v1' }),
|
|
157
|
+
note: 'z.ai Coding Plan ผ่าน Anthropic Messages API. GLM_BASE_URL override → open.bigmodel.cn/api/anthropic/v1 (จีน)',
|
|
158
|
+
},
|
|
159
|
+
// ── Delegate: OpenAI Codex ผ่าน ChatGPT plan quota (wrap official codex CLI, ToS-safe) ──
|
|
160
|
+
codex: {
|
|
161
|
+
id: 'codex',
|
|
162
|
+
label: 'OpenAI Codex (ChatGPT plan)',
|
|
163
|
+
kind: 'delegate',
|
|
164
|
+
envVar: 'CODEX_HOME', // ไม่ใช้ API key — codex login จัดการ auth เอง
|
|
165
|
+
requiresKey: false,
|
|
166
|
+
localPlaceholderKey: 'codex',
|
|
167
|
+
keyFormat: null,
|
|
168
|
+
models: { default: 'gpt-5-codex', codex: 'gpt-5-codex' },
|
|
169
|
+
create: () => {
|
|
170
|
+
throw new Error('codex เป็น delegate provider — ใช้ผ่าน codex subprocess ไม่ใช่ Vercel AI SDK');
|
|
171
|
+
},
|
|
172
|
+
note: 'ใช้ ChatGPT plan quota ผ่าน official codex CLI (ToS-safe, ไม่เก็บ credential). ต้อง codex login ก่อน',
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
export const SUPPORTED_PROVIDERS = Object.keys(PROVIDERS);
|
|
176
|
+
// alias สั้นข้าม provider — bare word (ไม่มี ':') map ไป provider+alias
|
|
177
|
+
const GLOBAL_ALIAS = {
|
|
178
|
+
opus: { provider: 'anthropic', alias: 'opus' },
|
|
179
|
+
sonnet: { provider: 'anthropic', alias: 'sonnet' },
|
|
180
|
+
haiku: { provider: 'anthropic', alias: 'haiku' },
|
|
181
|
+
fast: { provider: 'anthropic', alias: 'fast' },
|
|
182
|
+
fable: { provider: 'anthropic', alias: 'fable' },
|
|
183
|
+
gpt: { provider: 'openai', alias: 'gpt' },
|
|
184
|
+
codex: { provider: 'codex', alias: 'default' }, // bare "codex" = ChatGPT quota (delegate); "openai:codex" = API key
|
|
185
|
+
gemini: { provider: 'google', alias: 'gemini' },
|
|
186
|
+
flash: { provider: 'google', alias: 'flash' },
|
|
187
|
+
grok: { provider: 'xai', alias: 'grok' },
|
|
188
|
+
deepseek: { provider: 'deepseek', alias: 'default' },
|
|
189
|
+
mistral: { provider: 'mistral', alias: 'default' },
|
|
190
|
+
groq: { provider: 'groq', alias: 'default' },
|
|
191
|
+
ollama: { provider: 'ollama', alias: 'default' },
|
|
192
|
+
lmstudio: { provider: 'lmstudio', alias: 'default' },
|
|
193
|
+
glm: { provider: 'glm', alias: 'default' },
|
|
194
|
+
minimax: { provider: 'minimax', alias: 'default' },
|
|
195
|
+
};
|
|
196
|
+
/** parse "provider:model" | "provider:alias" | alias | "model" (default anthropic) */
|
|
197
|
+
export function parseSpec(spec) {
|
|
198
|
+
const idx = spec.indexOf(':');
|
|
199
|
+
if (idx !== -1) {
|
|
200
|
+
const provider = spec.slice(0, idx);
|
|
201
|
+
const rest = spec.slice(idx + 1);
|
|
202
|
+
const cfg = PROVIDERS[provider];
|
|
203
|
+
// ถ้าเป็น alias ของ provider นั้น → map เป็น model id จริง, ไม่งั้นใช้ rest เป็น raw model id
|
|
204
|
+
const model = cfg?.models[rest] ?? rest;
|
|
205
|
+
return { provider, model };
|
|
206
|
+
}
|
|
207
|
+
const g = GLOBAL_ALIAS[spec];
|
|
208
|
+
if (g)
|
|
209
|
+
return { provider: g.provider, model: PROVIDERS[g.provider].models[g.alias] };
|
|
210
|
+
// bare model id → default provider anthropic
|
|
211
|
+
return { provider: 'anthropic', model: spec };
|
|
212
|
+
}
|
|
213
|
+
/** normalized key สำหรับ lookup pricing เช่น "anthropic:claude-sonnet-4-6" */
|
|
214
|
+
export function specKey(spec) {
|
|
215
|
+
const { provider, model } = parseSpec(spec);
|
|
216
|
+
return `${provider}:${model}`;
|
|
217
|
+
}
|
|
218
|
+
/** resolve spec → LanguageModel (throw ถ้าไม่มี key / provider ผิด / key เป็น OAuth) */
|
|
219
|
+
export function resolveModel(spec) {
|
|
220
|
+
const { provider, model } = parseSpec(spec);
|
|
221
|
+
const cfg = PROVIDERS[provider];
|
|
222
|
+
if (!cfg) {
|
|
223
|
+
throw new Error(`provider ไม่รองรับ: "${provider}" — รองรับ: ${SUPPORTED_PROVIDERS.join('/')} (เช่น "openai:gpt-5", "sonnet", "groq:fast")`);
|
|
224
|
+
}
|
|
225
|
+
let key;
|
|
226
|
+
if (cfg.requiresKey) {
|
|
227
|
+
const found = resolveKeyFromEnv(cfg.envVar, cfg.envFallbacks);
|
|
228
|
+
if (!found) {
|
|
229
|
+
throw new Error(`ต้องตั้ง ${cfg.envVar} ก่อนใช้ provider "${provider}" (BYOK — API key ตรงจาก console)`);
|
|
230
|
+
}
|
|
231
|
+
assertDirectApiKey(cfg, found); // reject OAuth/subscription token + format ผิด
|
|
232
|
+
key = found;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
key = resolveKeyFromEnv(cfg.envVar) ?? cfg.localPlaceholderKey ?? 'local';
|
|
236
|
+
}
|
|
237
|
+
// <PROVIDER>_BASE_URL env → override (สลับ region intl/จีน); ไม่งั้น local อ่าน env, cloud ใช้ default
|
|
238
|
+
const baseURL = process.env[`${cfg.id.toUpperCase()}_BASE_URL`] ??
|
|
239
|
+
(cfg.requiresKey ? cfg.baseURL : process.env[cfg.envVar] ?? cfg.baseURL);
|
|
240
|
+
return cfg.create(key, baseURL)(model);
|
|
241
|
+
}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
// session store — จำ conversation + ความคืบหน้า เพื่อ "ทำงานต่อได้" ไม่ลืมว่าทำถึงไหน
|
|
5
|
+
const SESSION_DIR = join(homedir(), '.sanook', 'sessions');
|
|
6
|
+
export function newSessionId() {
|
|
7
|
+
// CLI runtime — ใช้ Date/random ได้ (ไม่ใช่ workflow context)
|
|
8
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
9
|
+
return `${ts}-${Math.random().toString(36).slice(2, 8)}`;
|
|
10
|
+
}
|
|
11
|
+
export async function saveSession(s) {
|
|
12
|
+
await mkdir(SESSION_DIR, { recursive: true });
|
|
13
|
+
await writeFile(join(SESSION_DIR, `${s.id}.json`), `${JSON.stringify(s, null, 2)}\n`);
|
|
14
|
+
}
|
|
15
|
+
export async function loadSession(id) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(await readFile(join(SESSION_DIR, `${id}.json`), 'utf8'));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** session ล่าสุด (สำหรับ --continue) */
|
|
24
|
+
export async function latestSession() {
|
|
25
|
+
try {
|
|
26
|
+
const ids = (await readdir(SESSION_DIR)).filter((f) => f.endsWith('.json')).map((f) => f.slice(0, -5));
|
|
27
|
+
if (!ids.length)
|
|
28
|
+
return null;
|
|
29
|
+
const sessions = (await Promise.all(ids.map(loadSession))).filter((s) => s !== null);
|
|
30
|
+
sessions.sort((a, b) => b.updated.localeCompare(a.updated));
|
|
31
|
+
return sessions[0] ?? null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, readdir, rm, stat, lstat, copyFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir, tmpdir } from 'node:os';
|
|
3
|
+
import { join, basename, resolve, sep, dirname } from 'node:path';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { lookup } from 'node:dns/promises';
|
|
8
|
+
import { isIP } from 'node:net';
|
|
9
|
+
import { parseFrontmatter, isValidSkillName } from './skills.js';
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const USER_SKILLS = join(homedir(), '.sanook', 'skills');
|
|
12
|
+
const MAX_FILES = 300;
|
|
13
|
+
const MAX_BYTES = 20 * 1024 * 1024; // 20MB ต่อ skill
|
|
14
|
+
const MAX_MD = 2 * 1024 * 1024; // 2MB ต่อ SKILL.md จาก URL
|
|
15
|
+
const exists = async (p) => {
|
|
16
|
+
try {
|
|
17
|
+
await stat(p);
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
async function copyTreeSafe(srcDir, destDir, budget, depth = 2) {
|
|
25
|
+
if (depth < 0)
|
|
26
|
+
return;
|
|
27
|
+
let entries;
|
|
28
|
+
try {
|
|
29
|
+
entries = await readdir(srcDir, { withFileTypes: true });
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
for (const e of entries) {
|
|
35
|
+
if (e.name.startsWith('.'))
|
|
36
|
+
continue; // skip .git/dotfiles
|
|
37
|
+
const s = join(srcDir, e.name);
|
|
38
|
+
const st = await lstat(s);
|
|
39
|
+
if (st.isSymbolicLink())
|
|
40
|
+
continue; // ห้าม copy symlink (กัน planted symlink หลุด ~/.sanook)
|
|
41
|
+
if (st.isDirectory()) {
|
|
42
|
+
await copyTreeSafe(s, join(destDir, e.name), budget, depth - 1);
|
|
43
|
+
}
|
|
44
|
+
else if (st.isFile()) {
|
|
45
|
+
if (--budget.files < 0)
|
|
46
|
+
throw new Error('skill มีไฟล์เยอะเกินไป');
|
|
47
|
+
budget.bytes -= st.size;
|
|
48
|
+
if (budget.bytes < 0)
|
|
49
|
+
throw new Error('skill ใหญ่เกินไป (เกิน 20MB)');
|
|
50
|
+
await mkdir(dirname(join(destDir, e.name)), { recursive: true });
|
|
51
|
+
await copyFile(s, join(destDir, e.name));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** copy skill dir (SKILL.md + references/scripts ที่เป็น regular file) → ~/.sanook/skills/<name> */
|
|
56
|
+
async function installFromDir(srcDir) {
|
|
57
|
+
const md = join(srcDir, 'SKILL.md');
|
|
58
|
+
const stMd = await lstat(md);
|
|
59
|
+
if (stMd.isSymbolicLink() || !stMd.isFile())
|
|
60
|
+
throw new Error('SKILL.md ไม่ใช่ไฟล์ปกติ');
|
|
61
|
+
const { meta } = parseFrontmatter(await readFile(md, 'utf8'));
|
|
62
|
+
const name = meta.name || basename(srcDir);
|
|
63
|
+
if (!isValidSkillName(name))
|
|
64
|
+
throw new Error(`ชื่อ skill ไม่ถูกต้อง: "${name}"`);
|
|
65
|
+
const dest = join(USER_SKILLS, name);
|
|
66
|
+
await rm(dest, { recursive: true, force: true });
|
|
67
|
+
await mkdir(dest, { recursive: true });
|
|
68
|
+
await copyTreeSafe(srcDir, dest, { files: MAX_FILES, bytes: MAX_BYTES });
|
|
69
|
+
return { name, path: dest };
|
|
70
|
+
}
|
|
71
|
+
/** เขียน SKILL.md เดียว (จาก URL) → ~/.sanook/skills/<name> */
|
|
72
|
+
async function installFromContent(content, fallbackName) {
|
|
73
|
+
const { meta } = parseFrontmatter(content);
|
|
74
|
+
const name = meta.name || fallbackName;
|
|
75
|
+
if (!isValidSkillName(name))
|
|
76
|
+
throw new Error(`ชื่อ skill ไม่ถูกต้อง: "${name}"`);
|
|
77
|
+
const dest = join(USER_SKILLS, name);
|
|
78
|
+
await mkdir(dest, { recursive: true });
|
|
79
|
+
await writeFile(join(dest, 'SKILL.md'), content);
|
|
80
|
+
return { name, path: dest };
|
|
81
|
+
}
|
|
82
|
+
/** หา SKILL.md ใน dir (ตรงๆ, ทุก subdir, หรือ skills/ subdir) → ติดตั้งทั้งหมด */
|
|
83
|
+
async function installFromLocal(path, onLog) {
|
|
84
|
+
if (await exists(join(path, 'SKILL.md')))
|
|
85
|
+
return [await installFromDir(path)];
|
|
86
|
+
const out = [];
|
|
87
|
+
for (const root of [path, join(path, 'skills')]) {
|
|
88
|
+
let entries;
|
|
89
|
+
try {
|
|
90
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (const e of entries) {
|
|
96
|
+
if (e.isDirectory() && (await exists(join(root, e.name, 'SKILL.md')))) {
|
|
97
|
+
try {
|
|
98
|
+
out.push(await installFromDir(join(root, e.name)));
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
onLog?.(`ข้าม ${e.name}: ${err.message}`); // skip ตัวที่ fail ติดตั้งที่เหลือต่อ
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!out.length)
|
|
107
|
+
throw new Error(`ไม่เจอ SKILL.md ใน ${path} (รองรับ root, */SKILL.md, skills/*/SKILL.md)`);
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
/** clone GitHub repo (shallow) → ติดตั้ง skill — subPath ต้องอยู่ใต้ clone dir (กัน traversal escape) */
|
|
111
|
+
async function installFromGitHub(repoUrl, subPath, onLog) {
|
|
112
|
+
const tmp = join(tmpdir(), `sanook-skill-${randomUUID().slice(0, 8)}`);
|
|
113
|
+
try {
|
|
114
|
+
onLog?.(`clone ${repoUrl} …`);
|
|
115
|
+
// execFile (no shell) + '--' กัน url ขึ้นต้น '-' ถูกตีเป็น git option + timeout
|
|
116
|
+
await execFileAsync('git', ['clone', '--depth', '1', '--quiet', '--', repoUrl, tmp], { timeout: 90_000 });
|
|
117
|
+
// subPath traversal guard — target ต้องอยู่ใต้ tmp
|
|
118
|
+
const target = subPath ? resolve(tmp, subPath) : tmp;
|
|
119
|
+
if (subPath && target !== tmp && !target.startsWith(tmp + sep)) {
|
|
120
|
+
throw new Error(`sub-path ไม่ปลอดภัย: ${subPath}`);
|
|
121
|
+
}
|
|
122
|
+
return await installFromLocal(target, onLog);
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
if (e.code === 'ENOENT') {
|
|
126
|
+
throw new Error('ต้องติดตั้ง git ก่อนใช้ skill add จาก GitHub');
|
|
127
|
+
}
|
|
128
|
+
throw e;
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await rm(tmp, { recursive: true, force: true }).catch(() => { });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const PRIVATE_IP = /^(127\.|10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.|::1$|fe80:|f[cd])/i;
|
|
135
|
+
/** fetch SKILL.md จาก URL — block internal/private (SSRF) + timeout + cap ขนาด */
|
|
136
|
+
async function fetchSkillMd(url) {
|
|
137
|
+
const u = new URL(url);
|
|
138
|
+
if (u.protocol !== 'https:')
|
|
139
|
+
throw new Error('รองรับเฉพาะ https สำหรับ URL ของ SKILL.md');
|
|
140
|
+
// resolve hostname → block private/loopback IP (กัน SSRF ยิง internal/cloud-metadata)
|
|
141
|
+
let ip = u.hostname;
|
|
142
|
+
if (!isIP(u.hostname)) {
|
|
143
|
+
const res = await lookup(u.hostname).catch(() => null);
|
|
144
|
+
ip = res?.address ?? '';
|
|
145
|
+
}
|
|
146
|
+
if (!ip || PRIVATE_IP.test(ip))
|
|
147
|
+
throw new Error(`URL ชี้ไป internal/private address — ปฏิเสธ (${u.hostname})`);
|
|
148
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(30_000) });
|
|
149
|
+
if (!r.ok)
|
|
150
|
+
throw new Error(`fetch ไม่สำเร็จ: HTTP ${r.status}`);
|
|
151
|
+
if (Number(r.headers.get('content-length') ?? 0) > MAX_MD)
|
|
152
|
+
throw new Error('SKILL.md ใหญ่เกิน 2MB');
|
|
153
|
+
const text = await r.text();
|
|
154
|
+
if (text.length > MAX_MD)
|
|
155
|
+
throw new Error('SKILL.md ใหญ่เกิน 2MB');
|
|
156
|
+
return text;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* ติดตั้ง skill จาก source — local path · URL ของ SKILL.md (https) · GitHub ("user/repo" หรือ "user/repo/sub/path")
|
|
160
|
+
* ⚠️ skill = instruction ที่ agent จะ trust (ไม่ใช่ data) — ติดตั้งจาก source ที่เชื่อถือเท่านั้น
|
|
161
|
+
*/
|
|
162
|
+
export async function installSkill(source, onLog) {
|
|
163
|
+
if (await exists(source))
|
|
164
|
+
return installFromLocal(source, onLog);
|
|
165
|
+
if (/^https?:\/\//.test(source)) {
|
|
166
|
+
if (source.endsWith('.md')) {
|
|
167
|
+
const parts = source.split('/');
|
|
168
|
+
return [await installFromContent(await fetchSkillMd(source), parts[parts.length - 2] ?? 'skill')];
|
|
169
|
+
}
|
|
170
|
+
const m = source.match(/^(https:\/\/github\.com\/[\w.-]+\/[\w.-]+?)(?:\.git)?(?:\/tree\/[^/]+\/(.+))?$/);
|
|
171
|
+
if (m)
|
|
172
|
+
return installFromGitHub(`${m[1]}.git`, m[2] ?? '', onLog);
|
|
173
|
+
throw new Error(`URL ไม่รองรับ: ${source} (ใช้ลิงก์ SKILL.md ตรง หรือ GitHub repo)`);
|
|
174
|
+
}
|
|
175
|
+
const seg = source.split('/');
|
|
176
|
+
if (seg.length >= 2 && /^[\w.-]+$/.test(seg[0]) && /^[\w.-]+$/.test(seg[1])) {
|
|
177
|
+
return installFromGitHub(`https://github.com/${seg[0]}/${seg[1]}.git`, seg.slice(2).join('/'), onLog);
|
|
178
|
+
}
|
|
179
|
+
throw new Error(`ไม่รู้จัก source: "${source}" — ใช้ GitHub "user/repo", URL ของ SKILL.md, หรือ local path`);
|
|
180
|
+
}
|
|
181
|
+
/** ลบ skill ที่ติดตั้งไว้ (เฉพาะ ~/.sanook/skills — ไม่แตะ bundled) */
|
|
182
|
+
export async function removeInstalledSkill(name) {
|
|
183
|
+
if (!isValidSkillName(name))
|
|
184
|
+
return false;
|
|
185
|
+
const dir = join(USER_SKILLS, name);
|
|
186
|
+
if (!(await exists(dir)))
|
|
187
|
+
return false;
|
|
188
|
+
await rm(dir, { recursive: true, force: true });
|
|
189
|
+
return true;
|
|
190
|
+
}
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
// skills = วิธีทำงานเฉพาะทาง/runbook ที่โหลด on-demand (progressive disclosure)
|
|
6
|
+
// agent เห็นแค่ name+description ใน system prompt → โหลดเต็มด้วย `skill` tool เมื่อ task ตรง
|
|
7
|
+
// self-improvement: agent สร้าง skill เองด้วย `create_skill` เมื่อเจอ procedure ที่ reuse ได้
|
|
8
|
+
// 3 ชั้น: bundled (ship กับ CLI) → global (~/.sanook) → project (.sanook) — ชั้นหลัง override ชื่อซ้ำ
|
|
9
|
+
const BUNDLED_SKILLS = join(dirname(fileURLToPath(import.meta.url)), '..', 'skills');
|
|
10
|
+
const GLOBAL_SKILLS = join(homedir(), '.sanook', 'skills');
|
|
11
|
+
const projectSkills = () => join(process.cwd(), '.sanook', 'skills');
|
|
12
|
+
/** minimal frontmatter parser (key: value ใน --- block) — ไม่พึ่ง YAML dep */
|
|
13
|
+
export function parseFrontmatter(content) {
|
|
14
|
+
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
15
|
+
if (!m)
|
|
16
|
+
return { meta: {}, body: content };
|
|
17
|
+
const meta = {};
|
|
18
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
19
|
+
const i = line.indexOf(':');
|
|
20
|
+
if (i === -1)
|
|
21
|
+
continue;
|
|
22
|
+
const k = line.slice(0, i).trim();
|
|
23
|
+
const v = line
|
|
24
|
+
.slice(i + 1)
|
|
25
|
+
.trim()
|
|
26
|
+
.replace(/^["']|["']$/g, '');
|
|
27
|
+
if (k)
|
|
28
|
+
meta[k] = v;
|
|
29
|
+
}
|
|
30
|
+
return { meta, body: m[2] };
|
|
31
|
+
}
|
|
32
|
+
/** ป้องกัน path traversal — ชื่อ skill ต้องเป็น slug ปลอดภัย */
|
|
33
|
+
export function isValidSkillName(name) {
|
|
34
|
+
return /^[a-z0-9][a-z0-9-]{0,63}$/.test(name);
|
|
35
|
+
}
|
|
36
|
+
/** scan project + global skills → list (name+description เท่านั้น สำหรับ inject). project ทับ global ชื่อซ้ำ */
|
|
37
|
+
export async function loadSkills() {
|
|
38
|
+
const out = new Map();
|
|
39
|
+
// bundled ก่อน → global → project ทับ (specific กว่าอยู่ท้าย)
|
|
40
|
+
for (const dir of [BUNDLED_SKILLS, GLOBAL_SKILLS, projectSkills()]) {
|
|
41
|
+
let entries;
|
|
42
|
+
try {
|
|
43
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue; // ไม่มีโฟลเดอร์ = ข้าม
|
|
47
|
+
}
|
|
48
|
+
for (const e of entries) {
|
|
49
|
+
if (!e.isDirectory() || !isValidSkillName(e.name))
|
|
50
|
+
continue;
|
|
51
|
+
const p = join(dir, e.name, 'SKILL.md');
|
|
52
|
+
try {
|
|
53
|
+
const { meta } = parseFrontmatter(await readFile(p, 'utf8'));
|
|
54
|
+
out.set(meta.name || e.name, {
|
|
55
|
+
name: meta.name || e.name,
|
|
56
|
+
description: meta.description ?? '',
|
|
57
|
+
whenToUse: meta.when_to_use,
|
|
58
|
+
path: p,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// ไม่มี SKILL.md = ข้าม
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return [...out.values()];
|
|
67
|
+
}
|
|
68
|
+
/** อ่านเนื้อหา SKILL.md เต็ม (skill tool เรียกเมื่อ agent ตัดสินใจใช้) */
|
|
69
|
+
export async function getSkillBody(name) {
|
|
70
|
+
if (!isValidSkillName(name))
|
|
71
|
+
return null;
|
|
72
|
+
for (const dir of [projectSkills(), GLOBAL_SKILLS, BUNDLED_SKILLS]) {
|
|
73
|
+
try {
|
|
74
|
+
return await readFile(join(dir, name, 'SKILL.md'), 'utf8');
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* ลอง dir ถัดไป */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/** สร้าง/อัปเดต skill (create_skill tool เรียก) — เขียนลง global skills */
|
|
83
|
+
export async function saveSkill(name, description, body, whenToUse) {
|
|
84
|
+
if (!isValidSkillName(name)) {
|
|
85
|
+
throw new Error(`ชื่อ skill ไม่ถูกต้อง: "${name}" — ใช้ a-z 0-9 - เท่านั้น`);
|
|
86
|
+
}
|
|
87
|
+
const dir = join(GLOBAL_SKILLS, name);
|
|
88
|
+
await mkdir(dir, { recursive: true });
|
|
89
|
+
const fm = [
|
|
90
|
+
'---',
|
|
91
|
+
`name: ${name}`,
|
|
92
|
+
`description: ${description.replace(/\n/g, ' ').trim()}`,
|
|
93
|
+
...(whenToUse ? [`when_to_use: ${whenToUse.replace(/\n/g, ' ').trim()}`] : []),
|
|
94
|
+
'---',
|
|
95
|
+
'',
|
|
96
|
+
body.trim(),
|
|
97
|
+
'',
|
|
98
|
+
].join('\n');
|
|
99
|
+
const p = join(dir, 'SKILL.md');
|
|
100
|
+
await writeFile(p, fm);
|
|
101
|
+
return p;
|
|
102
|
+
}
|
|
103
|
+
/** render รายชื่อ skill (name+desc) สำหรับ inject เข้า system prompt */
|
|
104
|
+
export function renderAvailableSkills(skills) {
|
|
105
|
+
if (!skills.length)
|
|
106
|
+
return '';
|
|
107
|
+
// truncate description กัน system prompt บวมเมื่อ skill เยอะ (whenToUse/body เต็มอ่านผ่าน skill/find_skills tool)
|
|
108
|
+
const trunc = (s, n) => (s.length > n ? `${s.slice(0, n).trimEnd()}…` : s);
|
|
109
|
+
const lines = skills.map((s) => `- ${s.name}: ${trunc(s.description, 140)}`);
|
|
110
|
+
return `<available_skills note="โหลดเต็มด้วย skill tool · ค้นด้วย find_skills เมื่อไม่แน่ใจว่าตัวไหนตรง">\n${lines.join('\n')}\n</available_skills>`;
|
|
111
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { exec } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { clamp } from './util.js';
|
|
6
|
+
import { checkBash } from './permission.js';
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
export const bashTool = tool({
|
|
9
|
+
description: 'รันคำสั่ง shell (ls/grep/find/cat/test/npm ฯลฯ) เพื่อค้นหา ตรวจสอบ หรือรัน build/test',
|
|
10
|
+
inputSchema: z.object({
|
|
11
|
+
cmd: z.string().describe('คำสั่ง shell ที่จะรัน'),
|
|
12
|
+
}),
|
|
13
|
+
execute: async ({ cmd }) => {
|
|
14
|
+
const guard = checkBash(cmd);
|
|
15
|
+
if (!guard.ok)
|
|
16
|
+
return `BLOCKED: ${guard.reason}`;
|
|
17
|
+
try {
|
|
18
|
+
const { stdout, stderr } = await execAsync(cmd, { timeout: 120_000, maxBuffer: 10 * 1024 * 1024 });
|
|
19
|
+
const out = (stdout + (stderr ? `\n[stderr]\n${stderr}` : '')).trim();
|
|
20
|
+
return clamp(out) || '(no output)';
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
return `ERROR: คำสั่งล้มเหลว — ${err.message}`;
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
});
|