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/trust.js ADDED
@@ -0,0 +1,89 @@
1
+ import { chmod, mkdir, readFile, realpath, stat, writeFile } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { appHomePath, appProjectPath, BRAND_ENV, envFlag } from './brand.js';
4
+ const TRUST_FILE = appHomePath('trusted-projects.json');
5
+ const BOUNDARY_MARKERS = ['.git', 'package.json'];
6
+ async function exists(p) {
7
+ try {
8
+ await stat(p);
9
+ return true;
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ async function canonical(p) {
16
+ try {
17
+ return await realpath(p);
18
+ }
19
+ catch {
20
+ return resolve(p);
21
+ }
22
+ }
23
+ export async function projectRoot(cwd = process.cwd()) {
24
+ let dir = resolve(cwd);
25
+ for (;;) {
26
+ const atBoundary = (await Promise.all(BOUNDARY_MARKERS.map((mk) => exists(join(dir, mk))))).some(Boolean);
27
+ if (atBoundary)
28
+ return canonical(dir);
29
+ const parent = dirname(dir);
30
+ if (parent === dir)
31
+ return canonical(resolve(cwd));
32
+ dir = parent;
33
+ }
34
+ }
35
+ async function readStore() {
36
+ try {
37
+ const parsed = JSON.parse(await readFile(TRUST_FILE, 'utf8'));
38
+ return parsed && typeof parsed === 'object' ? parsed : {};
39
+ }
40
+ catch {
41
+ return {};
42
+ }
43
+ }
44
+ async function writeStore(store) {
45
+ await mkdir(dirname(TRUST_FILE), { recursive: true });
46
+ await writeFile(TRUST_FILE, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
47
+ await chmod(TRUST_FILE, 0o600).catch(() => { });
48
+ }
49
+ export async function projectTrustStatus(cwd = process.cwd()) {
50
+ const root = await projectRoot(cwd);
51
+ if (envFlag(BRAND_ENV.trustProject))
52
+ return { root, trusted: true, reason: 'env' };
53
+ const store = await readStore();
54
+ const trusted = new Set(await Promise.all((store.trustedProjectRoots ?? []).map(canonical)));
55
+ return trusted.has(root)
56
+ ? { root, trusted: true, reason: 'store' }
57
+ : { root, trusted: false, reason: 'missing' };
58
+ }
59
+ export async function trustProject(cwd = process.cwd()) {
60
+ const root = await projectRoot(cwd);
61
+ const store = await readStore();
62
+ const existing = new Set(await Promise.all((store.trustedProjectRoots ?? []).map(canonical)));
63
+ existing.add(root);
64
+ await writeStore({ trustedProjectRoots: [...existing].sort() });
65
+ return root;
66
+ }
67
+ export async function untrustProject(cwd = process.cwd()) {
68
+ const root = await projectRoot(cwd);
69
+ const store = await readStore();
70
+ const roots = await Promise.all((store.trustedProjectRoots ?? []).map(canonical));
71
+ await writeStore({ trustedProjectRoots: roots.filter((r) => r !== root).sort() });
72
+ return root;
73
+ }
74
+ export async function projectConfigPathIfTrusted(file, cwd = process.cwd()) {
75
+ const root = await projectRoot(cwd);
76
+ const p = appProjectPath(root, file);
77
+ if (!(await exists(p)))
78
+ return null;
79
+ const trust = await projectTrustStatus(root);
80
+ return trust.trusted ? p : null;
81
+ }
82
+ export async function hasUntrustedProjectConfig(file, cwd = process.cwd()) {
83
+ const root = await projectRoot(cwd);
84
+ const p = appProjectPath(root, file);
85
+ if (!(await exists(p)))
86
+ return false;
87
+ const trust = await projectTrustStatus(root);
88
+ return !trust.trusted;
89
+ }
package/dist/ui/app.js CHANGED
@@ -1,16 +1,33 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useRef, useMemo } from 'react';
3
+ import { execFile } from 'node:child_process';
4
+ import { promisify } from 'node:util';
3
5
  import { Box, Text, Static, useApp, useInput } from 'ink';
4
- import { parseCommand } from '../commands.js';
6
+ import { BUILTIN_COMMANDS, expandCustomCommand, loadCustomCommands, parseCommand, parseSlashInvocation, } from '../commands.js';
5
7
  import { runAgent } from '../loop.js';
6
8
  import { saveSession, newSessionId } from '../session.js';
9
+ import { getBrainPath, appendBrainWorklog } from '../memory.js';
10
+ import { autoCompact, estimateTokens, summarizeCompact } from '../compaction.js';
11
+ import { makeSummarizer } from '../summarize.js';
12
+ import { agentTuning } from '../config.js';
13
+ import { snapshotWorkTree, restoreWorkTree } from '../checkpoint.js';
14
+ import { useEditor } from './useEditor.js';
15
+ import { loadHistory, appendHistory } from './history.js';
16
+ import { expandMentions } from './mentions.js';
17
+ import { BRAND } from '../brand.js';
7
18
  import { Banner } from './banner.js';
8
- export function App({ initialModel, budgetUsd, permissionMode = 'auto', initialHistory }) {
19
+ const execFileP = promisify(execFile);
20
+ const PRE_TURN_COMPACT_TOKENS = 100_000; // session ยาวมากเท่านั้นถึง summarize ก่อน turn (mode summarize)
21
+ export function App({ initialModel, fallbackModel, budgetUsd, permissionMode = 'ask', initialHistory, initialNote }) {
9
22
  const { exit } = useApp();
10
- const [history, setHistory] = useState(initialHistory?.length
11
- ? [{ id: -1, role: 'system', text: `↻ ต่อจาก session ก่อน (${initialHistory.length} ข้อความ)` }]
12
- : []);
13
- const [input, setInput] = useState('');
23
+ const [history, setHistory] = useState(() => {
24
+ const seed = [];
25
+ if (initialNote)
26
+ seed.push({ id: -2, role: 'system', text: initialNote });
27
+ if (initialHistory?.length)
28
+ seed.push({ id: -1, role: 'system', text: `↻ ต่อจาก session ก่อน (${initialHistory.length} ข้อความ)` });
29
+ return seed;
30
+ });
14
31
  const [streaming, setStreaming] = useState('');
15
32
  const [busy, setBusy] = useState(false);
16
33
  const [model, setModel] = useState(initialModel);
@@ -21,45 +38,148 @@ export function App({ initialModel, budgetUsd, permissionMode = 'auto', initialH
21
38
  const sessionId = useRef(newSessionId());
22
39
  const sessionCreated = useRef(new Date().toISOString());
23
40
  const approvalResolve = useRef(null);
41
+ const replHistory = useRef(loadHistory()); // prompt เก่า (persist) สำหรับ ↑/↓
42
+ const checkpoints = useRef([]);
43
+ const editor = useEditor(replHistory.current);
44
+ // real-time steering: หยุด turn ที่กำลังรัน (abort) + คิวข้อความที่พิมพ์ระหว่าง busy
45
+ const abortRef = useRef(null);
46
+ const queueRef = useRef([]);
47
+ const [queued, setQueued] = useState([]);
48
+ const enqueue = (msg) => {
49
+ queueRef.current.push(msg);
50
+ setQueued([...queueRef.current]);
51
+ };
52
+ const dequeue = () => {
53
+ const m = queueRef.current.shift();
54
+ setQueued([...queueRef.current]);
55
+ return m;
56
+ };
57
+ const clearQueue = () => {
58
+ queueRef.current = [];
59
+ setQueued([]);
60
+ };
24
61
  const addTurn = (role, text) => setHistory((h) => [...h, { id: idRef.current++, role, text }]);
62
+ // /diff /undo — git-backed (execFile ไม่ผ่าน shell)
63
+ async function runGit(args, label) {
64
+ try {
65
+ const { stdout, stderr } = await execFileP('git', args, { cwd: process.cwd(), maxBuffer: 1_000_000 });
66
+ addTurn('system', (stdout || stderr).trim() || `(${label}: ไม่มีการเปลี่ยนแปลง)`);
67
+ }
68
+ catch (e) {
69
+ addTurn('system', `git ${label}: ${e.message.split('\n')[0]}`);
70
+ }
71
+ }
25
72
  // ask-mode: tool ขออนุมัติ → คืน Promise ที่ resolve เมื่อ user กด y/n
26
73
  const requestApproval = (tool, summary) => new Promise((resolve) => {
27
74
  approvalResolve.current = resolve;
28
75
  setApprovalReq({ tool, summary });
29
76
  });
30
- useInput((char, key) => {
77
+ useInput((input, key) => {
31
78
  // มี approval ค้าง → จับ y/n ก่อน (แม้ agent กำลังรัน/busy)
32
79
  if (approvalReq) {
33
- if (char === 'y' || char === 'Y' || key.return) {
80
+ if (input === 'y' || input === 'Y' || key.return) {
34
81
  approvalResolve.current?.(true);
35
82
  setApprovalReq(null);
36
83
  }
37
- else if (char === 'n' || char === 'N' || key.escape) {
84
+ else if (input === 'n' || input === 'N' || key.escape) {
38
85
  approvalResolve.current?.(false);
39
86
  setApprovalReq(null);
40
87
  }
41
88
  return;
42
89
  }
43
- if (busy)
90
+ if (busy) {
91
+ // steering ระหว่าง turn: Esc / Ctrl+C = หยุด turn นี้ (ไม่ออกจากแอป) + ล้างคิว
92
+ if (key.escape || (key.ctrl && input === 'c')) {
93
+ abortRef.current?.abort();
94
+ clearQueue();
95
+ return;
96
+ }
97
+ // พิมพ์ระหว่าง busy ได้ — Enter = ต่อคิว (รันอัตโนมัติหลัง turn นี้จบ)
98
+ const a = editor.handleKey(input, key);
99
+ if (a === 'submit') {
100
+ const v = editor.value.trim();
101
+ editor.reset();
102
+ if (v)
103
+ enqueue(v);
104
+ }
105
+ return;
106
+ }
107
+ const action = editor.handleKey(input, key);
108
+ if (action === 'submit')
109
+ void submit(editor.value);
110
+ else if (action === 'interrupt') {
111
+ if (editor.value)
112
+ editor.reset(); // Ctrl+C ครั้งแรก = ล้างบรรทัด, ว่างแล้ว = ออก
113
+ else
114
+ exit();
115
+ }
116
+ });
117
+ /** ย้อน 1 turn — คืนไฟล์ (git, recoverable) + ตัดบทสนทนากลับ */
118
+ async function rewind() {
119
+ const cp = checkpoints.current.pop();
120
+ if (!cp) {
121
+ addTurn('system', 'ไม่มี checkpoint ให้ย้อน');
44
122
  return;
45
- if (key.return) {
46
- void submit();
47
123
  }
48
- else if (key.backspace || key.delete) {
49
- setInput((s) => s.slice(0, -1));
124
+ let note = '';
125
+ if (cp.ref) {
126
+ const r = await restoreWorkTree(cp.ref);
127
+ note = r.ok
128
+ ? r.recovery
129
+ ? ` · ไฟล์คืนแล้ว (กู้สถานะก่อนหน้า: ${r.recovery})`
130
+ : ' · ไฟล์คืนแล้ว'
131
+ : ` · ไฟล์: ${r.reason}`;
50
132
  }
51
- else if (key.ctrl && char === 'c') {
52
- exit();
133
+ msgsRef.current = msgsRef.current.slice(0, cp.msgLen);
134
+ setHistory((h) => h.filter((t) => t.id < cp.turnId));
135
+ addTurn('system', `↩ ย้อนกลับ 1 turn${note}`);
136
+ }
137
+ /** บีบ context: 'summarize' (ใช้ model ถูกย่อ) ถ้าตั้งไว้ ไม่งั้น 'truncate' (zero-LLM) */
138
+ async function compactHistory(targetTokens, label) {
139
+ const before = estimateTokens(msgsRef.current);
140
+ if (before <= targetTokens) {
141
+ addTurn('system', `context ~${before} tokens — ยังไม่ต้องบีบ`);
142
+ return;
53
143
  }
54
- else if (char && !key.ctrl && !key.meta) {
55
- setInput((s) => s + char);
144
+ const tuning = await agentTuning().catch(() => null);
145
+ if (tuning?.compaction === 'summarize') {
146
+ addTurn('system', '⏳ กำลังย่อ context ด้วย model ถูก…');
147
+ msgsRef.current = await summarizeCompact(msgsRef.current, targetTokens, makeSummarizer(model, tuning.summaryModel), 20).catch(() => autoCompact(msgsRef.current, targetTokens, 20));
148
+ addTurn('system', `ย่อ context แล้ว (summarize): ~${before} → ~${estimateTokens(msgsRef.current)} tokens`);
56
149
  }
57
- });
58
- async function submit() {
59
- const text = input.trim();
150
+ else {
151
+ msgsRef.current = autoCompact(msgsRef.current, targetTokens, 20);
152
+ addTurn('system', `บีบ context แล้ว: ~${before} → ~${estimateTokens(msgsRef.current)} tokens`);
153
+ }
154
+ }
155
+ async function submit(raw) {
156
+ const text = raw.trim();
157
+ editor.reset();
60
158
  if (!text)
61
159
  return;
62
- setInput('');
160
+ appendHistory(text, replHistory.current[replHistory.current.length - 1]);
161
+ replHistory.current.push(text);
162
+ const slash = parseSlashInvocation(text);
163
+ if (slash) {
164
+ if (slash.name === 'rewind') {
165
+ await rewind();
166
+ return;
167
+ }
168
+ if (!BUILTIN_COMMANDS.has(slash.name)) {
169
+ const custom = (await loadCustomCommands()).get(slash.name);
170
+ if (custom) {
171
+ const expanded = expandCustomCommand(custom, slash.args);
172
+ const mark = { turnId: idRef.current, msgLen: msgsRef.current.length };
173
+ addTurn('user', text);
174
+ if (!expanded.trim()) {
175
+ addTurn('system', `custom command /${slash.name} ว่าง`);
176
+ return;
177
+ }
178
+ await runAssistantTurn(expanded, [], mark);
179
+ return;
180
+ }
181
+ }
182
+ }
63
183
  const cmd = parseCommand(text, { model, costSummary: lastCost.current });
64
184
  if (cmd.handled) {
65
185
  addTurn('user', text);
@@ -67,26 +187,62 @@ export function App({ initialModel, budgetUsd, permissionMode = 'auto', initialH
67
187
  return exit();
68
188
  if (cmd.action === 'clear') {
69
189
  msgsRef.current = [];
190
+ checkpoints.current = [];
70
191
  return setHistory([]);
71
192
  }
193
+ if (cmd.action === 'compact') {
194
+ void compactHistory(40_000, 'บีบ context');
195
+ return;
196
+ }
197
+ if (cmd.action === 'diff')
198
+ return void runGit(['diff', '--stat'], 'diff');
199
+ if (cmd.action === 'undo') {
200
+ void runGit(['stash', 'push', '-u', '-m', BRAND.undoStashMessage], 'undo').then(() => addTurn('system', 'กู้คืน: git stash pop'));
201
+ return;
202
+ }
72
203
  if (cmd.modelChange)
73
204
  setModel(cmd.modelChange);
74
205
  if (cmd.message)
75
206
  addTurn('system', cmd.message);
76
207
  return;
77
208
  }
209
+ // prompt ปกติ → expand @mentions (inline ไฟล์ text + เก็บ path รูป)
210
+ const mark = { turnId: idRef.current, msgLen: msgsRef.current.length };
78
211
  addTurn('user', text);
212
+ const { text: expanded, images, errors } = await expandMentions(text);
213
+ if (errors.length)
214
+ addTurn('system', `@mention: ${errors.join(' · ')}`);
215
+ await runAssistantTurn(expanded, images, mark);
216
+ }
217
+ async function runAssistantTurn(promptText, images, mark) {
218
+ // proactive summarize-compaction สำหรับ session ยาวมาก (เฉพาะ mode summarize) — เริ่ม turn ให้ context lean
219
+ // (mode truncate: ปล่อยให้ loop.ts ตัดต่อ-step เอา; ไม่บีบที่นี่ กัน latency)
220
+ if (estimateTokens(msgsRef.current) > PRE_TURN_COMPACT_TOKENS) {
221
+ const t = await agentTuning().catch(() => null);
222
+ if (t?.compaction === 'summarize') {
223
+ addTurn('system', '⏳ context ยาว — ย่ออัตโนมัติก่อนรอบนี้…');
224
+ msgsRef.current = await summarizeCompact(msgsRef.current, PRE_TURN_COMPACT_TOKENS, makeSummarizer(model, t.summaryModel), 20).catch(() => msgsRef.current);
225
+ }
226
+ }
227
+ // checkpoint สถานะก่อนรัน (ไฟล์ git + ขอบเขตบทสนทนา) → /rewind ย้อนได้
228
+ const ref = await snapshotWorkTree();
229
+ checkpoints.current.push({ ref, turnId: mark.turnId, msgLen: mark.msgLen });
230
+ const ac = new AbortController(); // steering: ให้ Esc/Ctrl+C หยุด stream กลางทางได้
231
+ abortRef.current = ac;
79
232
  setBusy(true);
80
233
  let buf = '';
81
234
  let lastFlush = 0;
82
235
  try {
83
- const { cost, messages } = await runAgent({
236
+ const { cost, messages, text } = await runAgent({
84
237
  model,
85
- prompt: text,
238
+ fallbackModel,
239
+ prompt: promptText,
240
+ images: images.length ? images : undefined,
86
241
  history: msgsRef.current,
87
242
  budgetUsd,
88
243
  permissionMode,
89
244
  approve: requestApproval,
245
+ signal: ac.signal,
90
246
  onEvent: (e) => {
91
247
  if (e.type === 'text') {
92
248
  buf += e.text ?? '';
@@ -104,8 +260,8 @@ export function App({ initialModel, budgetUsd, permissionMode = 'auto', initialH
104
260
  });
105
261
  msgsRef.current = messages;
106
262
  lastCost.current = cost.summary();
107
- addTurn('assistant', buf.trim());
108
- // เซฟ session ทุกรอบ → resume ได้ด้วย sanook -c (เดิม REPL ไม่เคยเซฟ)
263
+ addTurn('assistant', buf.trim() || text.trim());
264
+ // เซฟ session ทุกรอบ → resume ได้ด้วย sanook -c
109
265
  void saveSession({
110
266
  id: sessionId.current,
111
267
  created: sessionCreated.current,
@@ -114,19 +270,60 @@ export function App({ initialModel, budgetUsd, permissionMode = 'auto', initialH
114
270
  cwd: process.cwd(),
115
271
  messages,
116
272
  });
273
+ // worklog เข้า second-brain — vault จำว่าทำอะไรใน session นี้
274
+ void (async () => {
275
+ const brain = await getBrainPath();
276
+ if (brain) {
277
+ await appendBrainWorklog(brain, {
278
+ prompt: promptText,
279
+ summary: cost.summary(),
280
+ model,
281
+ today: new Date().toISOString().slice(0, 10),
282
+ }).catch(() => { });
283
+ }
284
+ })();
117
285
  }
118
286
  catch (err) {
119
- addTurn('system', `ERROR: ${err.message}`);
287
+ if (ac.signal.aborted) {
288
+ // หยุดเอง — เก็บ partial output ไว้ดู, ทิ้ง turn นี้ออกจาก LLM history (msgsRef ไม่อัปเดต)
289
+ if (buf.trim())
290
+ addTurn('assistant', buf.trim());
291
+ addTurn('system', '⊘ หยุด turn แล้ว (ไฟล์ที่ tool แก้ไปแล้วคืนด้วย /rewind ได้)');
292
+ }
293
+ else {
294
+ addTurn('system', `ERROR: ${err.message}`);
295
+ }
120
296
  }
121
297
  finally {
122
298
  setStreaming('');
123
299
  setBusy(false);
300
+ abortRef.current = null;
124
301
  }
302
+ // steering: ข้อความที่พิมพ์ค้างคิวระหว่าง turn → รันต่อทันที (ถ้าไม่ได้ถูกหยุด)
303
+ const next = ac.signal.aborted ? undefined : dequeue();
304
+ if (next)
305
+ void submit(next);
125
306
  }
126
- const banner = useMemo(() => _jsx(Banner, { model: initialModel }), [initialModel]);
127
- return (_jsxs(Box, { flexDirection: "column", children: [banner, _jsx(Static, { items: history, children: (turn) => _jsx(TurnView, { turn: turn }, turn.id) }), streaming ? _jsx(Text, { children: streaming }) : null, approvalReq ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["\u26A0 \u0E02\u0E2D\u0E2D\u0E19\u0E38\u0E21\u0E31\u0E15\u0E34\u0E23\u0E31\u0E19 ", approvalReq.tool] }), _jsx(Text, { children: approvalReq.summary }), _jsx(Text, { color: "gray", children: "\u0E2D\u0E19\u0E38\u0E21\u0E31\u0E15\u0E34? (y = \u0E23\u0E31\u0E19 \u00B7 n = \u0E1B\u0E0F\u0E34\u0E40\u0E2A\u0E18)" })] })) : (_jsxs(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { color: busy ? 'gray' : 'cyan', children: busy ? '… ' : '› ' }), _jsx(Text, { children: input || (busy ? '' : 'พิมพ์คำสั่ง หรือ /help') })] })), _jsxs(Text, { color: "gray", dimColor: true, children: [' ', "? for shortcuts \u00B7 /help \u00B7 model: ", model, permissionMode === 'ask' ? ' · 🔒 ask-mode' : ''] })] }));
307
+ // banner ผูกกับ live `model` (ไม่ใช่ initialModel) → /model เปลี่ยนแล้ว banner อัปเดตตาม ไม่ค้าง model เก่า
308
+ const banner = useMemo(() => _jsx(Banner, { model: model }), [model]);
309
+ const costHint = lastCost.current.includes('cost ') ? lastCost.current.split('cost ')[1] : '';
310
+ return (_jsxs(Box, { flexDirection: "column", children: [banner, _jsx(Static, { items: history, children: (turn) => _jsx(TurnView, { turn: turn }, turn.id) }), streaming ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: streaming }) })) : null, queued.length ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: queued.map((q, i) => (_jsxs(Text, { dimColor: true, children: ["\u23F3 \u0E04\u0E34\u0E27 ", i + 1, ": ", q.length > 64 ? `${q.slice(0, 64)}…` : q] }, i))) })) : null, approvalReq ? (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["\u0E2D\u0E19\u0E38\u0E21\u0E31\u0E15\u0E34\u0E23\u0E31\u0E19 ", approvalReq.tool, "?"] }), _jsx(Text, { dimColor: true, children: approvalReq.summary }), _jsx(Text, { dimColor: true, children: "y = \u0E23\u0E31\u0E19 \u00B7 n = \u0E1B\u0E0F\u0E34\u0E40\u0E2A\u0E18" })] })) : (_jsxs(Box, { marginTop: 1, borderStyle: "round", borderColor: busy ? 'gray' : 'blue', paddingX: 1, children: [_jsx(Text, { color: busy ? 'gray' : 'cyan', children: busy ? '· ' : '› ' }), _jsx(InputView, { value: editor.value, cursor: editor.cursor, busy: busy })] })), _jsxs(Text, { dimColor: true, children: [' ', model, " \u00B7 ", permissionMode === 'ask' ? 'ask-mode' : 'auto', " \u00B7 /help \u00B7 @file \u00B7 \u2191 history", costHint ? ` · ${costHint}` : ''] })] }));
311
+ }
312
+ /** input ที่มี cursor (inverse) + placeholder — minimal; รับ input ได้แม้ busy (ต่อคิว) */
313
+ function InputView({ value, cursor, busy }) {
314
+ if (busy && !value)
315
+ return _jsx(Text, { dimColor: true, children: "\u0E01\u0E33\u0E25\u0E31\u0E07\u0E17\u0E33\u0E07\u0E32\u0E19\u2026 Esc/Ctrl+C \u0E2B\u0E22\u0E38\u0E14 \u00B7 \u0E1E\u0E34\u0E21\u0E1E\u0E4C\u0E40\u0E1E\u0E37\u0E48\u0E2D\u0E15\u0E48\u0E2D\u0E04\u0E34\u0E27 (\u23CE)" });
316
+ if (!busy && !value)
317
+ return _jsx(Text, { dimColor: true, children: "\u0E16\u0E32\u0E21\u0E2D\u0E30\u0E44\u0E23\u0E01\u0E47\u0E44\u0E14\u0E49 \u2014 /help \u0E14\u0E39\u0E04\u0E33\u0E2A\u0E31\u0E48\u0E07 \u00B7 /tools \u0E14\u0E39 tools \u00B7 @\u0E44\u0E1F\u0E25\u0E4C \u0E41\u0E19\u0E1A context/\u0E23\u0E39\u0E1B" });
318
+ const before = value.slice(0, cursor);
319
+ const at = value.slice(cursor, cursor + 1) || ' ';
320
+ const after = value.slice(cursor + 1);
321
+ return (_jsxs(Text, { children: [before, _jsx(Text, { inverse: true, children: at }), after, busy ? _jsxs(Text, { dimColor: true, children: [' ', "(\u23CE \u0E15\u0E48\u0E2D\u0E04\u0E34\u0E27)"] }) : null] }));
128
322
  }
129
323
  function TurnView({ turn }) {
130
- const color = turn.role === 'user' ? 'cyan' : turn.role === 'system' ? 'yellow' : undefined;
131
- return (_jsxs(Text, { color: color, children: [turn.role === 'user' ? '› ' : '', turn.text] }));
324
+ if (turn.role === 'system')
325
+ return _jsx(Text, { dimColor: true, children: turn.text });
326
+ if (turn.role === 'user')
327
+ return (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(Text, { color: "cyan", children: turn.text })] }));
328
+ return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: turn.text }) }));
132
329
  }
package/dist/ui/banner.js CHANGED
@@ -1,20 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text, useStdout } from 'ink';
2
+ import { Box, Text } from 'ink';
3
3
  import Gradient from 'ink-gradient';
4
- import BigText from 'ink-big-text';
5
4
  import { homedir } from 'node:os';
6
5
  import { readFileSync } from 'node:fs';
6
+ import { BRAND } from '../brand.js';
7
7
  // gradient ของ Sanook: เขียว → ส้ม → ฟ้า (สนุก = สดใส)
8
8
  const SANOOK_GRADIENT = ['#22C55E', '#F97316', '#38BDF8'];
9
9
  // version จาก package.json (single source of truth) — กัน default drift เหมือน bin.ts
10
10
  const VERSION = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
11
- /** welcome banner — big ASCII + gradient + info line (responsive ตามความกว้าง terminal) */
11
+ /** welcome banner — minimal: gradient wordmark + meta บรรทัดเดียว (terminal-first, ไม่รก) */
12
12
  export function Banner({ model, version = VERSION, account = 'BYOK', cwd }) {
13
- const { stdout } = useStdout();
14
- const columns = stdout?.columns ?? 80;
15
13
  const dir = (cwd ?? process.cwd()).replace(homedir(), '~');
16
- // จอกว้าง = "Sanook AI" ใหญ่, แคบ = "Sanook" / เล็กลง
17
- const bigText = columns >= 92 ? 'Sanook AI' : 'Sanook';
18
- const font = columns >= 48 ? 'block' : 'tiny';
19
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Gradient, { colors: SANOOK_GRADIENT, children: _jsx(BigText, { text: bigText, font: font }) }), _jsxs(Box, { marginTop: -1, marginLeft: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Sanook AI CLI" }), _jsxs(Text, { color: "gray", children: [" v", version, " \u00B7 terminal coding agent \u00B7 BYOK"] })] }), _jsxs(Text, { color: "gray", children: [_jsx(Text, { color: "green", children: "\u25CF" }), " ", model, ' ', "account: ", account, ' ', "cwd: ", dir] })] })] }));
14
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Gradient, { colors: SANOOK_GRADIENT, children: _jsx(Text, { bold: true, children: BRAND.cliName }) }), _jsxs(Text, { dimColor: true, children: [" v", version, " \u00B7 terminal coding agent \u00B7 ", account] })] }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "green", children: "\u25CF" }), " ", model, " \u00B7 ", dir] })] }));
20
15
  }
@@ -15,10 +15,10 @@ export function BrainWizard({ onComplete }) {
15
15
  return (_jsxs(Box, { flexDirection: "column", gap: 1, marginY: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "\uD83E\uDDE0 \u0E2A\u0E23\u0E49\u0E32\u0E07 Second Brain workspace" }), step === 'path' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "1. \u0E27\u0E32\u0E07\u0E42\u0E04\u0E23\u0E07\u0E2A\u0E23\u0E49\u0E32\u0E07\u0E44\u0E27\u0E49\u0E17\u0E35\u0E48\u0E44\u0E2B\u0E19? (Enter = default)" }), _jsxs(Text, { color: "gray", children: [" ", DEFAULT_PATH] }), _jsx(TextInput, { defaultValue: DEFAULT_PATH, placeholder: DEFAULT_PATH, onSubmit: (v) => {
16
16
  setPath(v.trim() || DEFAULT_PATH);
17
17
  setStep('owner');
18
- } })] })), step === 'owner' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "2. \u0E40\u0E23\u0E35\u0E22\u0E01\u0E04\u0E38\u0E13\u0E27\u0E48\u0E32\u0E2D\u0E30\u0E44\u0E23\u0E14\u0E35? (\u0E0A\u0E37\u0E48\u0E2D/\u0E0A\u0E37\u0E48\u0E2D\u0E40\u0E25\u0E48\u0E19 \u2014 Enter = \u0E02\u0E49\u0E32\u0E21)" }), _jsx(TextInput, { defaultValue: BRAIN_DEFAULTS.ownerName, onSubmit: (v) => {
18
+ } })] })), step === 'owner' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["2. \u0E40\u0E23\u0E35\u0E22\u0E01\u0E04\u0E38\u0E13\u0E27\u0E48\u0E32\u0E2D\u0E30\u0E44\u0E23\u0E14\u0E35? (\u0E0A\u0E37\u0E48\u0E2D/\u0E0A\u0E37\u0E48\u0E2D\u0E40\u0E25\u0E48\u0E19 \u2014 Enter \u0E40\u0E1B\u0E25\u0E48\u0E32 = \u0E43\u0E0A\u0E49 \"", BRAIN_DEFAULTS.ownerName, "\")"] }), _jsx(TextInput, { placeholder: BRAIN_DEFAULTS.ownerName, onSubmit: (v) => {
19
19
  setOwnerName(v.trim() || BRAIN_DEFAULTS.ownerName);
20
20
  setStep('ai');
21
- } })] })), step === 'ai' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "3. \u0E2D\u0E22\u0E32\u0E01\u0E43\u0E2B\u0E49 AI \u0E40\u0E23\u0E35\u0E22\u0E01\u0E15\u0E31\u0E27\u0E40\u0E2D\u0E07\u0E27\u0E48\u0E32\u0E2D\u0E30\u0E44\u0E23?" }), _jsx(TextInput, { defaultValue: BRAIN_DEFAULTS.aiName, onSubmit: (v) => {
21
+ } })] })), step === 'ai' && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["3. \u0E2D\u0E22\u0E32\u0E01\u0E43\u0E2B\u0E49 AI \u0E40\u0E23\u0E35\u0E22\u0E01\u0E15\u0E31\u0E27\u0E40\u0E2D\u0E07\u0E27\u0E48\u0E32\u0E2D\u0E30\u0E44\u0E23? ", _jsxs(Text, { color: "gray", children: ["(Enter \u0E40\u0E1B\u0E25\u0E48\u0E32 = \"", BRAIN_DEFAULTS.aiName, "\")"] })] }), _jsx(TextInput, { placeholder: BRAIN_DEFAULTS.aiName, onSubmit: (v) => {
22
22
  setAiName(v.trim() || BRAIN_DEFAULTS.aiName);
23
23
  setStep('autonomy');
24
24
  } })] })), step === 'autonomy' && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "4. \u0E43\u0E2B\u0E49 AI \u0E17\u0E33\u0E07\u0E32\u0E19\u0E41\u0E1A\u0E1A\u0E44\u0E2B\u0E19?" }), _jsx(Select, { options: [
@@ -0,0 +1,30 @@
1
+ import { readFileSync, appendFileSync, mkdirSync } from 'node:fs';
2
+ import { appHomePath, persistenceEnabled } from '../brand.js';
3
+ // prompt history แบบ persist ข้าม session (เลียน shell history) — เก็บที่ ~/.sanook/history
4
+ const HISTORY_PATH = appHomePath('history');
5
+ const MAX_ENTRIES = 500;
6
+ /** โหลด prompt เก่า (เก่า→ใหม่) สำหรับ Up/Down navigation ใน REPL */
7
+ export function loadHistory() {
8
+ try {
9
+ const lines = readFileSync(HISTORY_PATH, 'utf8').split('\n').filter(Boolean);
10
+ return lines.slice(-MAX_ENTRIES);
11
+ }
12
+ catch {
13
+ return [];
14
+ }
15
+ }
16
+ /** append 1 prompt (ข้ามถ้าซ้ำกับอันล่าสุด / เป็น slash command / ว่าง) */
17
+ export function appendHistory(prompt, last) {
18
+ if (!persistenceEnabled())
19
+ return;
20
+ const p = prompt.trim();
21
+ if (!p || p === last)
22
+ return;
23
+ try {
24
+ mkdirSync(appHomePath(), { recursive: true });
25
+ appendFileSync(HISTORY_PATH, `${p.replace(/\n/g, ' ')}\n`, { mode: 0o600 });
26
+ }
27
+ catch {
28
+ /* เขียนไม่ได้ = ไม่เป็นไร (history เป็น nice-to-have) */
29
+ }
30
+ }
@@ -0,0 +1,44 @@
1
+ import { readFile, realpath } from 'node:fs/promises';
2
+ import { resolve, extname } from 'node:path';
3
+ import { checkReadPath } from '../tools/permission.js';
4
+ // @-file mentions: "@path" ใน prompt → inline เนื้อหาไฟล์ (text) หรือแนบเป็น image (รูป)
5
+ // ลด tool round-trip (agent ไม่ต้อง read_file เอง) + เปิดทาง vision input
6
+ const IMAGE_EXT = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp']);
7
+ const MENTION_RE = /(?:^|\s)@([^\s]+)/g;
8
+ const MAX_INLINE = 60_000;
9
+ /** แตก @mention ใน prompt: text file → inline, image → คืน path ไปแนบ, ที่เหลือคงไว้ */
10
+ export async function expandMentions(input) {
11
+ const mentions = [...input.matchAll(MENTION_RE)].map((m) => m[1]);
12
+ if (!mentions.length)
13
+ return { text: input, images: [], errors: [] };
14
+ const images = [];
15
+ const errors = [];
16
+ const inlined = [];
17
+ for (const rel of [...new Set(mentions)]) {
18
+ const abs = resolve(rel);
19
+ // canonicalize ก่อนเช็ก extension → symlink ที่ชื่อไม่มีนามสกุลแต่ชี้ไปรูป ก็จับเป็น image ถูก
20
+ const real = await realpath(abs).catch(() => abs);
21
+ if (IMAGE_EXT.has(extname(real).toLowerCase())) {
22
+ const guard = await checkReadPath(real);
23
+ if (guard.ok)
24
+ images.push(real);
25
+ else
26
+ errors.push(`@${rel} (${guard.reason})`);
27
+ continue;
28
+ }
29
+ const guard = await checkReadPath(real);
30
+ if (!guard.ok) {
31
+ errors.push(`@${rel} (${guard.reason})`);
32
+ continue;
33
+ }
34
+ try {
35
+ const content = (await readFile(real, 'utf8')).slice(0, MAX_INLINE);
36
+ inlined.push(`<file path="${rel}">\n${content}\n</file>`);
37
+ }
38
+ catch (e) {
39
+ errors.push(`@${rel} (${e.message})`);
40
+ }
41
+ }
42
+ const text = inlined.length ? `${input}\n\n${inlined.join('\n\n')}` : input;
43
+ return { text, images, errors };
44
+ }