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.
Files changed (238) hide show
  1. package/.env.example +19 -0
  2. package/CHANGELOG.md +173 -0
  3. package/README.md +153 -20
  4. package/README.th.md +136 -0
  5. package/dist/agentContext.js +4 -0
  6. package/dist/approval.js +6 -0
  7. package/dist/bin.js +405 -57
  8. package/dist/brain.js +92 -59
  9. package/dist/brand.js +47 -0
  10. package/dist/checkpoint.js +37 -0
  11. package/dist/commands.js +86 -6
  12. package/dist/compaction.js +76 -5
  13. package/dist/config.js +100 -12
  14. package/dist/cost.js +60 -3
  15. package/dist/doctor.js +92 -0
  16. package/dist/gateway/auth.js +2 -2
  17. package/dist/gateway/ledger.js +2 -2
  18. package/dist/gateway/scheduler.js +1 -0
  19. package/dist/gateway/serve.js +6 -4
  20. package/dist/gateway/server.js +10 -2
  21. package/dist/git.js +11 -2
  22. package/dist/hooks.js +43 -17
  23. package/dist/knowledge.js +48 -49
  24. package/dist/loop.js +182 -66
  25. package/dist/lsp/client.js +173 -0
  26. package/dist/lsp/framing.js +56 -0
  27. package/dist/lsp/index.js +138 -0
  28. package/dist/lsp/servers.js +82 -0
  29. package/dist/mcp-server.js +244 -0
  30. package/dist/mcp.js +184 -29
  31. package/dist/memory-store.js +559 -0
  32. package/dist/memory.js +143 -29
  33. package/dist/orchestrate.js +150 -0
  34. package/dist/providers/codex.js +21 -7
  35. package/dist/providers/keys.js +3 -2
  36. package/dist/providers/models.js +22 -6
  37. package/dist/providers/registry.js +155 -1
  38. package/dist/repomap.js +93 -0
  39. package/dist/search/chunk.js +158 -0
  40. package/dist/search/embed-store.js +187 -0
  41. package/dist/search/engine.js +203 -0
  42. package/dist/search/fuse.js +35 -0
  43. package/dist/search/index-core.js +187 -0
  44. package/dist/search/indexer.js +241 -0
  45. package/dist/search/store.js +77 -0
  46. package/dist/session.js +42 -8
  47. package/dist/skill-install.js +10 -10
  48. package/dist/skills.js +12 -9
  49. package/dist/summarize.js +31 -0
  50. package/dist/tools/bash.js +21 -2
  51. package/dist/tools/diagnostics.js +41 -0
  52. package/dist/tools/edit.js +29 -7
  53. package/dist/tools/index.js +8 -1
  54. package/dist/tools/list.js +7 -2
  55. package/dist/tools/permission.js +90 -9
  56. package/dist/tools/read.js +23 -4
  57. package/dist/tools/remember.js +1 -1
  58. package/dist/tools/sandbox.js +61 -0
  59. package/dist/tools/search.js +105 -4
  60. package/dist/tools/task.js +195 -29
  61. package/dist/tools/timeout.js +35 -0
  62. package/dist/tools/util.js +10 -0
  63. package/dist/tools/write.js +6 -4
  64. package/dist/trust.js +89 -0
  65. package/dist/ui/app.js +228 -31
  66. package/dist/ui/banner.js +4 -9
  67. package/dist/ui/brain-wizard.js +2 -2
  68. package/dist/ui/history.js +30 -0
  69. package/dist/ui/mentions.js +44 -0
  70. package/dist/ui/render.js +55 -15
  71. package/dist/ui/setup.js +97 -12
  72. package/dist/ui/useEditor.js +83 -0
  73. package/dist/update.js +114 -0
  74. package/dist/worktree.js +173 -0
  75. package/package.json +11 -5
  76. package/scripts/postinstall.mjs +33 -0
  77. package/second-brain/.agents/_Index.md +30 -0
  78. package/second-brain/.agents/skills/_Index.md +30 -0
  79. package/second-brain/.agents/workflows/_Index.md +30 -0
  80. package/second-brain/AGENTS.md +4 -4
  81. package/second-brain/Acceptance/_Index.md +30 -0
  82. package/second-brain/Acceptance/golden-case-template.md +39 -0
  83. package/second-brain/Areas/_Index.md +30 -0
  84. package/second-brain/Bugs/System-OS/_Index.md +30 -0
  85. package/second-brain/Bugs/_Index.md +30 -0
  86. package/second-brain/CLAUDE.md +4 -1
  87. package/second-brain/Checklists/_Index.md +30 -0
  88. package/second-brain/Checklists/preflight-postflight-template.md +29 -0
  89. package/second-brain/Distillations/_Index.md +30 -0
  90. package/second-brain/Entities/_Index.md +30 -0
  91. package/second-brain/Entities/entity-template.md +33 -0
  92. package/second-brain/Evals/_Index.md +30 -0
  93. package/second-brain/Evals/correction-pairs.md +24 -0
  94. package/second-brain/Evals/failure-taxonomy.md +24 -0
  95. package/second-brain/Evals/golden-set.md +25 -0
  96. package/second-brain/Evals/quality-ledger.md +23 -0
  97. package/second-brain/Evals/self-eval-rubric.md +23 -0
  98. package/second-brain/GEMINI.md +4 -4
  99. package/second-brain/Goals/_Index.md +30 -0
  100. package/second-brain/Handoffs/_Index.md +30 -0
  101. package/second-brain/Home.md +7 -0
  102. package/second-brain/Intake/Raw Sources/_Index.md +30 -0
  103. package/second-brain/Intake/_Index.md +30 -0
  104. package/second-brain/Intake/_Quarantine/_Index.md +30 -0
  105. package/second-brain/Learning/_Index.md +30 -0
  106. package/second-brain/Playbooks/_Index.md +30 -0
  107. package/second-brain/Playbooks/playbook-template.md +23 -0
  108. package/second-brain/Projects/_Index.md +30 -0
  109. package/second-brain/Prompts/_Index.md +30 -0
  110. package/second-brain/README.md +2 -1
  111. package/second-brain/Research/_Index.md +30 -0
  112. package/second-brain/Retrospectives/_Index.md +30 -0
  113. package/second-brain/Reviews/_Index.md +30 -0
  114. package/second-brain/Runbooks/_Index.md +30 -0
  115. package/second-brain/Runbooks/eval-loop.md +24 -0
  116. package/second-brain/Sessions/_Index.md +30 -0
  117. package/second-brain/Shared/AI-Context-Index.md +20 -0
  118. package/second-brain/Shared/AI-Threads/_Index.md +30 -0
  119. package/second-brain/Shared/Archive/_Index.md +30 -0
  120. package/second-brain/Shared/Assets/_Index.md +30 -0
  121. package/second-brain/Shared/Context-Packs/_Index.md +30 -0
  122. package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
  123. package/second-brain/Shared/Coordination/NOW.md +28 -0
  124. package/second-brain/Shared/Coordination/_Index.md +30 -0
  125. package/second-brain/Shared/Coordination/agent-registry.md +24 -0
  126. package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
  127. package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
  128. package/second-brain/Shared/Coordination/task-board.md +32 -0
  129. package/second-brain/Shared/Core-Facts/_Index.md +30 -0
  130. package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
  131. package/second-brain/Shared/Glossary/_Index.md +30 -0
  132. package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
  133. package/second-brain/Shared/Operating-State/_Index.md +30 -0
  134. package/second-brain/Shared/Prompting/_Index.md +30 -0
  135. package/second-brain/Shared/Provenance/_Index.md +30 -0
  136. package/second-brain/Shared/Rules/_Index.md +30 -0
  137. package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
  138. package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
  139. package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
  140. package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
  141. package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
  142. package/second-brain/Shared/Rules/rules-formatting.md +34 -0
  143. package/second-brain/Shared/Scripts/_Index.md +30 -0
  144. package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
  145. package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
  146. package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
  147. package/second-brain/Shared/User-Memory/_Index.md +30 -0
  148. package/second-brain/Shared/User-Persona/_Index.md +30 -0
  149. package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
  150. package/second-brain/Shared/Working-Memory/_Index.md +30 -0
  151. package/second-brain/Shared/_Index.md +30 -0
  152. package/second-brain/Shared/mcp-servers/_Index.md +30 -0
  153. package/second-brain/Skills/_Index.md +30 -0
  154. package/second-brain/Templates/_Index.md +30 -0
  155. package/second-brain/Templates/bug.md +2 -0
  156. package/second-brain/Templates/handoff.md +2 -0
  157. package/second-brain/Templates/session.md +2 -0
  158. package/second-brain/Tools/_Index.md +30 -0
  159. package/second-brain/Traces/_Index.md +30 -0
  160. package/second-brain/Vault Structure Map.md +33 -1
  161. package/second-brain/copilot/_Index.md +30 -0
  162. package/skills/audit-license-compliance/SKILL.md +117 -0
  163. package/skills/author-codemod/SKILL.md +110 -0
  164. package/skills/build-audit-logging/SKILL.md +112 -0
  165. package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
  166. package/skills/build-cli-tool/SKILL.md +108 -0
  167. package/skills/build-data-table/SKILL.md +141 -0
  168. package/skills/build-native-mobile-ui/SKILL.md +154 -0
  169. package/skills/build-offline-first-sync/SKILL.md +118 -0
  170. package/skills/build-realtime-channel/SKILL.md +122 -0
  171. package/skills/build-vector-search/SKILL.md +131 -0
  172. package/skills/compose-local-dev-stack/SKILL.md +149 -0
  173. package/skills/configure-bundler-build/SKILL.md +166 -0
  174. package/skills/configure-dns-tls/SKILL.md +142 -0
  175. package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
  176. package/skills/configure-security-headers-csp/SKILL.md +122 -0
  177. package/skills/contract-testing/SKILL.md +140 -0
  178. package/skills/datetime-timezone-correctness/SKILL.md +125 -0
  179. package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
  180. package/skills/debug-flaky-tests/SKILL.md +128 -0
  181. package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
  182. package/skills/deliver-webhooks/SKILL.md +116 -0
  183. package/skills/design-api-pagination/SKILL.md +144 -0
  184. package/skills/design-authorization-model/SKILL.md +119 -0
  185. package/skills/design-backup-dr-recovery/SKILL.md +113 -0
  186. package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
  187. package/skills/design-multi-tenancy/SKILL.md +100 -0
  188. package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
  189. package/skills/design-relational-schema/SKILL.md +129 -0
  190. package/skills/design-search-index-infra/SKILL.md +151 -0
  191. package/skills/design-state-machine/SKILL.md +108 -0
  192. package/skills/design-token-system/SKILL.md +109 -0
  193. package/skills/distributed-locks-leases/SKILL.md +120 -0
  194. package/skills/encrypt-sensitive-data/SKILL.md +148 -0
  195. package/skills/feature-flags-rollout/SKILL.md +130 -0
  196. package/skills/file-upload-object-storage/SKILL.md +107 -0
  197. package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
  198. package/skills/harden-llm-app-reliability/SKILL.md +126 -0
  199. package/skills/i18n-localization-setup/SKILL.md +113 -0
  200. package/skills/idempotency-keys/SKILL.md +107 -0
  201. package/skills/implement-push-notifications/SKILL.md +142 -0
  202. package/skills/ingest-webhook-secure/SKILL.md +120 -0
  203. package/skills/integrate-oauth-oidc/SKILL.md +126 -0
  204. package/skills/load-stress-test/SKILL.md +129 -0
  205. package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
  206. package/skills/model-nosql-data/SKILL.md +118 -0
  207. package/skills/money-decimal-arithmetic/SKILL.md +123 -0
  208. package/skills/monitor-ml-drift/SKILL.md +109 -0
  209. package/skills/numeric-precision-units/SKILL.md +144 -0
  210. package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
  211. package/skills/optimize-react-rerenders/SKILL.md +124 -0
  212. package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
  213. package/skills/payments-billing-integration/SKILL.md +114 -0
  214. package/skills/pin-toolchain-versions/SKILL.md +116 -0
  215. package/skills/plan-strangler-migration/SKILL.md +95 -0
  216. package/skills/property-based-testing/SKILL.md +108 -0
  217. package/skills/publish-package-registry/SKILL.md +130 -0
  218. package/skills/recover-git-state/SKILL.md +119 -0
  219. package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
  220. package/skills/resilience-timeouts-retries/SKILL.md +104 -0
  221. package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
  222. package/skills/rewrite-git-history/SKILL.md +109 -0
  223. package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
  224. package/skills/schema-evolution-compatibility/SKILL.md +121 -0
  225. package/skills/send-transactional-email/SKILL.md +126 -0
  226. package/skills/serve-deploy-ml-model/SKILL.md +107 -0
  227. package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
  228. package/skills/setup-devcontainer-env/SKILL.md +131 -0
  229. package/skills/setup-lint-format-precommit/SKILL.md +140 -0
  230. package/skills/setup-monorepo-tooling/SKILL.md +125 -0
  231. package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
  232. package/skills/structured-output-llm/SKILL.md +86 -0
  233. package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
  234. package/skills/test-data-factories/SKILL.md +158 -0
  235. package/skills/threat-model-stride/SKILL.md +123 -0
  236. package/skills/train-evaluate-ml-model/SKILL.md +109 -0
  237. package/skills/unicode-text-correctness/SKILL.md +109 -0
  238. package/skills/visual-regression-testing/SKILL.md +120 -0
package/dist/mcp.js CHANGED
@@ -1,16 +1,19 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { readFile } from 'node:fs/promises';
3
- import { homedir } from 'node:os';
4
- import { join } from 'node:path';
5
3
  import { readFileSync } from 'node:fs';
6
4
  import { dynamicTool, jsonSchema } from 'ai';
5
+ import { appHomePath, appProjectPath, BRAND } from './brand.js';
6
+ import { hasUntrustedProjectConfig, projectConfigPathIfTrusted, projectRoot } from './trust.js';
7
7
  // version จาก package.json (single source of truth) — กัน drift เหมือน bin.ts/banner
8
8
  const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
9
- // MCP client (stdio JSON-RPC) เขียนเอง zero-dep — ต่อ MCP server (filesystem/github/postgres/ฯลฯ)
10
- // ทำให้ Sanook extensible เหมือน Claude Code/Codex. config: ~/.sanook/mcp.json + project .sanook/mcp.json
11
- // { "mcpServers": { "fs": { "command": "npx", "args": ["-y","@modelcontextprotocol/server-filesystem","/path"] } } }
12
- const PROTOCOL_VERSION = '2024-11-05';
9
+ // MCP client เขียนเอง zero-dep — ต่อ MCP server (filesystem/github/postgres/ฯลฯ)
10
+ // 2 transport: stdio (command) + Streamable-HTTP (url) ต่อทั้ง local และ remote/hosted MCP ได้
11
+ // config: ~/.sanook/mcp.json + project .sanook/mcp.json
12
+ // stdio: { "fs": { "command": "npx", "args": ["-y","@modelcontextprotocol/server-filesystem","/path"] } }
13
+ // remote: { "gh": { "url": "https://api.example.com/mcp", "headers": { "Authorization": "Bearer …" } } }
14
+ export const PROTOCOL_VERSION = '2024-11-05'; // shared by the MCP client (here) and server (mcp-server.ts)
13
15
  const MAX_BUF = 16 * 1024 * 1024; // กัน server ส่ง byte ยาวไม่มี newline → memory โต unbounded
16
+ const REQUEST_TIMEOUT = 20_000;
14
17
  // env ปลอดภัยที่ส่งให้ MCP child (ไม่มี secret) — server ที่ต้อง token ให้ตั้งใน cfg.env เอง
15
18
  const SAFE_ENV_KEYS = ['PATH', 'HOME', 'TMPDIR', 'TEMP', 'LANG', 'LC_ALL', 'USER', 'SHELL', 'TERM', 'NODE_PATH', 'NVM_DIR', 'APPDATA'];
16
19
  function safeEnv() {
@@ -22,8 +25,12 @@ function safeEnv() {
22
25
  }
23
26
  return out;
24
27
  }
25
- /** MCP stdio client — JSON-RPC 2.0, newline-delimited messages */
26
- class McpClient {
28
+ export function isValidMcpServerName(name) {
29
+ return (/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name) &&
30
+ !['__proto__', 'prototype', 'constructor'].includes(name));
31
+ }
32
+ /** stdio transport — JSON-RPC 2.0, newline-delimited ผ่าน child process stdin/stdout */
33
+ class StdioTransport {
27
34
  proc;
28
35
  buf = '';
29
36
  nextId = 1;
@@ -34,6 +41,9 @@ class McpClient {
34
41
  // minimal env เท่านั้น (PATH/HOME/locale) + cfg.env ที่ user ตั้งเอง — ไม่ส่ง secret
35
42
  // (ANTHROPIC_API_KEY/TELEGRAM_BOT_TOKEN/ฯลฯ) ให้ทุก MCP server (supply chain = npx -y <pkg>)
36
43
  env: { ...safeEnv(), ...cfg.env },
44
+ // Windows: `npx`/`npm`/JS bins เป็น .cmd shim → spawn ตรงๆ = ENOENT. shell=true ให้ผ่าน PATHEXT.
45
+ // (config นี้ user เป็นเจ้าของ/trust แล้ว — bare-name resolution เท่านั้น)
46
+ shell: process.platform === 'win32',
37
47
  stdio: ['pipe', 'pipe', 'pipe'],
38
48
  });
39
49
  this.proc.stdout?.on('data', (d) => this.onData(d.toString()));
@@ -76,7 +86,7 @@ class McpClient {
76
86
  }
77
87
  }
78
88
  }
79
- request(method, params, timeoutMs = 20_000) {
89
+ request(method, params, timeoutMs = REQUEST_TIMEOUT) {
80
90
  if (this.dead)
81
91
  return Promise.reject(new Error('mcp: server ตายแล้ว'));
82
92
  const id = this.nextId++;
@@ -101,20 +111,115 @@ class McpClient {
101
111
  notify(method, params) {
102
112
  this.proc.stdin?.write(`${JSON.stringify({ jsonrpc: '2.0', method, params })}\n`);
103
113
  }
114
+ close() {
115
+ try {
116
+ this.proc.kill();
117
+ }
118
+ catch {
119
+ /* ตายแล้ว */
120
+ }
121
+ }
122
+ }
123
+ /** Streamable-HTTP transport — POST JSON-RPC ต่อ request, รับ application/json หรือ text/event-stream */
124
+ class HttpTransport {
125
+ url;
126
+ userHeaders;
127
+ nextId = 1;
128
+ sessionId;
129
+ constructor(url, userHeaders = {}) {
130
+ this.url = url;
131
+ this.userHeaders = userHeaders;
132
+ }
133
+ headers() {
134
+ return {
135
+ 'content-type': 'application/json',
136
+ accept: 'application/json, text/event-stream',
137
+ ...(this.sessionId ? { 'mcp-session-id': this.sessionId } : {}),
138
+ ...this.userHeaders,
139
+ };
140
+ }
141
+ /** parse SSE body หา JSON-RPC response ที่ id ตรง (Streamable-HTTP คืน response ผ่าน event-stream ได้) */
142
+ parseSse(text, id) {
143
+ for (const block of text.split(/\n\n/)) {
144
+ const data = block
145
+ .split(/\n/)
146
+ .filter((l) => l.startsWith('data:'))
147
+ .map((l) => l.slice(5).trim())
148
+ .join('');
149
+ if (!data)
150
+ continue;
151
+ let msg;
152
+ try {
153
+ msg = JSON.parse(data);
154
+ }
155
+ catch {
156
+ continue; // block นี้ไม่ใช่ JSON สมบูรณ์ → ข้ามไป block ถัดไป (ไม่ abort ทั้ง stream)
157
+ }
158
+ // MCP protocol error / return อยู่นอก try → ไม่ถูกกลบโดย catch ของ JSON.parse
159
+ if (msg.id === id) {
160
+ if (msg.error)
161
+ throw new Error(msg.error.message ?? 'mcp error');
162
+ return msg.result;
163
+ }
164
+ }
165
+ throw new Error('mcp http: ไม่พบ response ใน event-stream');
166
+ }
167
+ async request(method, params, timeoutMs = REQUEST_TIMEOUT) {
168
+ const id = this.nextId++;
169
+ const res = await fetch(this.url, {
170
+ method: 'POST',
171
+ headers: this.headers(),
172
+ body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
173
+ signal: AbortSignal.timeout(timeoutMs),
174
+ });
175
+ const sid = res.headers.get('mcp-session-id');
176
+ if (sid)
177
+ this.sessionId = sid;
178
+ if (!res.ok)
179
+ throw new Error(`mcp http ${res.status} ${res.statusText}`);
180
+ const ctype = res.headers.get('content-type') ?? '';
181
+ if (ctype.includes('text/event-stream'))
182
+ return this.parseSse(await res.text(), id);
183
+ const json = (await res.json());
184
+ if (json.error)
185
+ throw new Error(json.error.message ?? 'mcp error');
186
+ return json.result;
187
+ }
188
+ notify(method, params) {
189
+ void fetch(this.url, {
190
+ method: 'POST',
191
+ headers: this.headers(),
192
+ body: JSON.stringify({ jsonrpc: '2.0', method, params }),
193
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT),
194
+ }).catch(() => { });
195
+ }
196
+ close() {
197
+ if (!this.sessionId)
198
+ return;
199
+ // best-effort terminate session (spec: DELETE) — ไม่รอผล
200
+ void fetch(this.url, { method: 'DELETE', headers: this.headers() }).catch(() => { });
201
+ }
202
+ }
203
+ /** MCP client — เลือก transport จาก config (url = http, ไม่งั้น stdio) แล้ว handshake + เรียก tool */
204
+ class McpClient {
205
+ transport;
206
+ constructor(cfg) {
207
+ this.transport = cfg.url ? new HttpTransport(cfg.url, cfg.headers) : new StdioTransport(cfg);
208
+ }
104
209
  async initialize() {
105
- await this.request('initialize', {
210
+ await this.transport.request('initialize', {
106
211
  protocolVersion: PROTOCOL_VERSION,
107
212
  capabilities: {},
108
- clientInfo: { name: 'sanook', version: VERSION },
213
+ clientInfo: { name: BRAND.mcpClientName, version: VERSION },
109
214
  });
110
- this.notify('notifications/initialized');
215
+ this.transport.notify('notifications/initialized');
111
216
  }
112
217
  async listTools() {
113
- const r = (await this.request('tools/list'));
218
+ const r = (await this.transport.request('tools/list'));
114
219
  return r?.tools ?? [];
115
220
  }
116
221
  async callTool(name, args) {
117
- const r = (await this.request('tools/call', { name, arguments: args ?? {} }));
222
+ const r = (await this.transport.request('tools/call', { name, arguments: args ?? {} }));
118
223
  const text = (r?.content ?? [])
119
224
  .filter((c) => c.type === 'text')
120
225
  .map((c) => c.text ?? '')
@@ -122,25 +227,71 @@ class McpClient {
122
227
  return r?.isError ? `MCP error: ${text}` : text || '(no output)';
123
228
  }
124
229
  close() {
125
- try {
126
- this.proc.kill();
127
- }
128
- catch {
129
- /* ตายแล้ว */
130
- }
230
+ this.transport.close();
131
231
  }
132
232
  }
133
- async function loadMcpConfig() {
233
+ function stringRecord(value) {
234
+ if (!value || typeof value !== 'object' || Array.isArray(value))
235
+ return undefined;
236
+ const out = {};
237
+ for (const [k, v] of Object.entries(value)) {
238
+ if (typeof k === 'string' && typeof v === 'string')
239
+ out[k] = v;
240
+ }
241
+ return Object.keys(out).length ? out : undefined;
242
+ }
243
+ function sanitizeMcpServerConfig(raw) {
244
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
245
+ return null;
246
+ const r = raw;
247
+ const cfg = {};
248
+ if (typeof r.command === 'string' && r.command)
249
+ cfg.command = r.command;
250
+ if (Array.isArray(r.args) && r.args.every((a) => typeof a === 'string'))
251
+ cfg.args = r.args;
252
+ if (typeof r.url === 'string' && r.url)
253
+ cfg.url = r.url;
254
+ const env = stringRecord(r.env);
255
+ if (env)
256
+ cfg.env = env;
257
+ const headers = stringRecord(r.headers);
258
+ if (headers)
259
+ cfg.headers = headers;
260
+ return cfg.command || cfg.url ? cfg : null;
261
+ }
262
+ async function readMcpFile(path, merged) {
263
+ const cfg = JSON.parse(await readFile(path, 'utf8'));
264
+ if (!cfg.mcpServers || typeof cfg.mcpServers !== 'object')
265
+ return;
266
+ for (const [name, raw] of Object.entries(cfg.mcpServers)) {
267
+ if (!isValidMcpServerName(name))
268
+ continue;
269
+ const server = sanitizeMcpServerConfig(raw);
270
+ if (server)
271
+ merged[name] = server;
272
+ }
273
+ }
274
+ export async function loadMcpConfig(onLog, cwd = process.cwd()) {
134
275
  const merged = {};
135
- for (const p of [join(homedir(), '.sanook', 'mcp.json'), join(process.cwd(), '.sanook', 'mcp.json')]) {
276
+ try {
277
+ await readMcpFile(appHomePath('mcp.json'), merged);
278
+ }
279
+ catch {
280
+ /* ไม่มี global config = ข้าม */
281
+ }
282
+ const root = await projectRoot(cwd);
283
+ const projectPath = await projectConfigPathIfTrusted('mcp.json', root);
284
+ if (projectPath) {
136
285
  try {
137
- const cfg = JSON.parse(await readFile(p, 'utf8'));
138
- Object.assign(merged, cfg.mcpServers ?? {});
286
+ await readMcpFile(projectPath, merged);
139
287
  }
140
- catch {
141
- /* ไม่มี config = ข้าม */
288
+ catch (e) {
289
+ onLog?.(`project MCP config อ่านไม่ได้: ${e.message}`);
142
290
  }
143
291
  }
292
+ else if (await hasUntrustedProjectConfig('mcp.json', root)) {
293
+ onLog?.(`project MCP config ถูกข้าม (ยังไม่ trust): ${appProjectPath(root, 'mcp.json')}`);
294
+ }
144
295
  return merged;
145
296
  }
146
297
  let cachePromise = null;
@@ -151,16 +302,20 @@ export function getMcpTools(onLog) {
151
302
  return cachePromise;
152
303
  }
153
304
  async function buildMcpTools(onLog) {
154
- const config = await loadMcpConfig();
305
+ const config = await loadMcpConfig(onLog);
155
306
  if (!Object.keys(config).length)
156
307
  return {};
157
308
  const tools = {};
158
309
  const clients = [];
159
310
  activeClients = clients; // ref เดียวกัน → closeMcp kill client ที่ spawn ระหว่าง build ได้ด้วย
160
311
  for (const [serverName, cfg] of Object.entries(config)) {
312
+ if (!cfg.url && !cfg.command) {
313
+ onLog?.(`MCP "${serverName}" ข้าม: ต้องมี "command" (stdio) หรือ "url" (remote)`);
314
+ continue;
315
+ }
161
316
  try {
162
317
  const client = new McpClient(cfg);
163
- clients.push(client); // push ทันที (constructor spawn แล้ว) ก่อน await → ไม่ leak ถ้า build ค้าง
318
+ clients.push(client); // push ทันที (อาจ spawn แล้ว) ก่อน await → ไม่ leak ถ้า build ค้าง
164
319
  await client.initialize();
165
320
  const defs = await client.listTools();
166
321
  for (const def of defs) {
@@ -175,7 +330,7 @@ async function buildMcpTools(onLog) {
175
330
  execute: async (args) => client.callTool(def.name, args),
176
331
  });
177
332
  }
178
- onLog?.(`MCP "${serverName}": ${defs.length} tools`);
333
+ onLog?.(`MCP "${serverName}" (${cfg.url ? 'http' : 'stdio'}): ${defs.length} tools`);
179
334
  }
180
335
  catch (e) {
181
336
  onLog?.(`MCP "${serverName}" ต่อไม่ได้: ${e.message}`);