sanook-cli 0.5.1 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/.env.example +161 -3
  2. package/CHANGELOG.md +148 -10
  3. package/README.md +255 -26
  4. package/README.th.md +95 -7
  5. package/dist/approval.js +13 -0
  6. package/dist/bin.js +3552 -155
  7. package/dist/brain-consolidate.js +335 -0
  8. package/dist/brain-context.js +262 -0
  9. package/dist/brain-doctor.js +318 -0
  10. package/dist/brain-eval.js +186 -0
  11. package/dist/brain-final.js +377 -0
  12. package/dist/brain-metrics.js +277 -0
  13. package/dist/brain-new.js +402 -0
  14. package/dist/brain-pack.js +210 -0
  15. package/dist/brain-repair.js +280 -0
  16. package/dist/brain-review.js +382 -0
  17. package/dist/brain.js +15 -1
  18. package/dist/brand.js +1 -1
  19. package/dist/cli-args.js +190 -0
  20. package/dist/cli-option-values.js +16 -0
  21. package/dist/clipboard.js +65 -0
  22. package/dist/commands.js +266 -27
  23. package/dist/compaction.js +96 -11
  24. package/dist/config.js +149 -33
  25. package/dist/context-compression.js +191 -0
  26. package/dist/context-pack.js +145 -0
  27. package/dist/cost.js +49 -15
  28. package/dist/dashboard/api-helpers.js +87 -0
  29. package/dist/dashboard/server.js +179 -0
  30. package/dist/dashboard/static/app.js +277 -0
  31. package/dist/dashboard/static/index.html +39 -0
  32. package/dist/dashboard/static/styles.css +85 -0
  33. package/dist/diff.js +10 -2
  34. package/dist/first-run.js +21 -0
  35. package/dist/gateway/auth.js +49 -9
  36. package/dist/gateway/bluebubbles.js +205 -0
  37. package/dist/gateway/config.js +929 -0
  38. package/dist/gateway/deliver.js +399 -0
  39. package/dist/gateway/discord.js +124 -0
  40. package/dist/gateway/doctor.js +456 -0
  41. package/dist/gateway/email.js +501 -0
  42. package/dist/gateway/googlechat.js +207 -0
  43. package/dist/gateway/homeassistant.js +256 -0
  44. package/dist/gateway/ledger.js +38 -1
  45. package/dist/gateway/line.js +171 -0
  46. package/dist/gateway/lock.js +3 -1
  47. package/dist/gateway/matrix.js +366 -0
  48. package/dist/gateway/mattermost.js +322 -0
  49. package/dist/gateway/ntfy.js +218 -0
  50. package/dist/gateway/schedule.js +31 -4
  51. package/dist/gateway/serve.js +267 -7
  52. package/dist/gateway/server.js +253 -19
  53. package/dist/gateway/service.js +224 -0
  54. package/dist/gateway/session.js +362 -0
  55. package/dist/gateway/signal.js +351 -0
  56. package/dist/gateway/slack.js +124 -0
  57. package/dist/gateway/sms.js +169 -0
  58. package/dist/gateway/targets.js +576 -0
  59. package/dist/gateway/teams.js +106 -0
  60. package/dist/gateway/telegram.js +38 -15
  61. package/dist/gateway/webhooks.js +220 -0
  62. package/dist/gateway/whatsapp.js +230 -0
  63. package/dist/hooks.js +13 -2
  64. package/dist/hotkeys.js +21 -0
  65. package/dist/i18n/en.js +98 -0
  66. package/dist/i18n/index.js +19 -0
  67. package/dist/i18n/th.js +98 -0
  68. package/dist/i18n/types.js +1 -0
  69. package/dist/insights-args.js +55 -0
  70. package/dist/insights.js +86 -0
  71. package/dist/knowledge.js +55 -29
  72. package/dist/loop.js +157 -29
  73. package/dist/lsp/index.js +23 -5
  74. package/dist/mcp-hub.js +33 -0
  75. package/dist/mcp-registry.js +494 -0
  76. package/dist/mcp-risk.js +71 -0
  77. package/dist/mcp-server.js +1 -1
  78. package/dist/mcp.js +120 -10
  79. package/dist/memory-log.js +90 -0
  80. package/dist/memory-store.js +37 -1
  81. package/dist/memory.js +148 -37
  82. package/dist/model-picker.js +58 -0
  83. package/dist/orchestrate.js +51 -19
  84. package/dist/personality.js +58 -0
  85. package/dist/plan-handoff.js +17 -0
  86. package/dist/polyglot.js +162 -0
  87. package/dist/process-runner.js +96 -0
  88. package/dist/project-init.js +91 -0
  89. package/dist/project-registry.js +143 -0
  90. package/dist/project-scaffold.js +124 -0
  91. package/dist/prompt-size.js +155 -0
  92. package/dist/providers/codex-login.js +138 -0
  93. package/dist/providers/codex.js +89 -43
  94. package/dist/providers/keys.js +22 -1
  95. package/dist/providers/models.js +2 -2
  96. package/dist/providers/registry.js +14 -47
  97. package/dist/search/chunk.js +7 -8
  98. package/dist/search/cli.js +83 -0
  99. package/dist/search/embed-store.js +3 -0
  100. package/dist/search/embedding-config.js +22 -0
  101. package/dist/search/engine.js +2 -13
  102. package/dist/search/indexer.js +44 -1
  103. package/dist/search/store.js +23 -1
  104. package/dist/session-distill.js +84 -0
  105. package/dist/session.js +92 -16
  106. package/dist/skill-install.js +53 -13
  107. package/dist/skills.js +33 -0
  108. package/dist/slash-completion.js +155 -0
  109. package/dist/support-dump.js +206 -0
  110. package/dist/tool-catalog.js +59 -0
  111. package/dist/tools/edit.js +45 -15
  112. package/dist/tools/git.js +10 -5
  113. package/dist/tools/homeassistant.js +106 -0
  114. package/dist/tools/index.js +10 -0
  115. package/dist/tools/list.js +19 -6
  116. package/dist/tools/permission.js +992 -12
  117. package/dist/tools/polyglot.js +126 -0
  118. package/dist/tools/read.js +16 -4
  119. package/dist/tools/sandbox.js +38 -13
  120. package/dist/tools/schedule.js +19 -3
  121. package/dist/tools/search.js +226 -15
  122. package/dist/tools/task.js +40 -9
  123. package/dist/tools/timeout.js +23 -3
  124. package/dist/tools/web-fetch-tool.js +33 -0
  125. package/dist/trust.js +11 -1
  126. package/dist/turn-retrieval.js +83 -0
  127. package/dist/ui/app.js +878 -32
  128. package/dist/ui/banner.js +78 -4
  129. package/dist/ui/history.js +37 -5
  130. package/dist/ui/markdown.js +122 -0
  131. package/dist/ui/mentions.js +3 -2
  132. package/dist/ui/overlay.js +496 -0
  133. package/dist/ui/queue.js +23 -0
  134. package/dist/ui/render.js +20 -1
  135. package/dist/ui/session-panel.js +115 -0
  136. package/dist/ui/setup-providers.js +40 -0
  137. package/dist/ui/setup.js +172 -46
  138. package/dist/ui/status.js +142 -0
  139. package/dist/ui/thinking-panel.js +36 -0
  140. package/dist/ui/tool-trail.js +97 -0
  141. package/dist/ui/transcript.js +26 -0
  142. package/dist/ui/useBusyElapsed.js +19 -0
  143. package/dist/ui/useEditor.js +144 -5
  144. package/dist/ui/useGitBranch.js +57 -0
  145. package/dist/update.js +56 -17
  146. package/dist/web-fetch.js +637 -0
  147. package/dist/web-surface.js +190 -0
  148. package/dist/worktree.js +175 -4
  149. package/package.json +5 -5
  150. package/second-brain/AGENTS.md +6 -4
  151. package/second-brain/CLAUDE.md +7 -1
  152. package/second-brain/Evals/_Index.md +10 -2
  153. package/second-brain/Evals/quality-ledger.md +9 -1
  154. package/second-brain/Evals/second-brain-benchmarks.md +62 -0
  155. package/second-brain/GEMINI.md +5 -4
  156. package/second-brain/Home.md +1 -1
  157. package/second-brain/Projects/_Index.md +19 -4
  158. package/second-brain/Projects/sanook-cli/_Index.md +30 -0
  159. package/second-brain/Projects/sanook-cli/context.md +35 -0
  160. package/second-brain/Projects/sanook-cli/current-state.md +32 -0
  161. package/second-brain/Projects/sanook-cli/overview.md +41 -0
  162. package/second-brain/Projects/sanook-cli/repo.md +34 -0
  163. package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +197 -0
  164. package/second-brain/README.md +1 -1
  165. package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
  166. package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
  167. package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
  168. package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -0
  169. package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
  170. package/second-brain/Research/2026-06-19-hermes-python-architecture-for-sanook.md +49 -0
  171. package/second-brain/Research/2026-06-19-terminal-ui-brand-research.md +52 -0
  172. package/second-brain/Research/_Index.md +8 -1
  173. package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
  174. package/second-brain/Reviews/_Index.md +1 -1
  175. package/second-brain/Runbooks/_Index.md +6 -1
  176. package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
  177. package/second-brain/SANOOK.md +45 -0
  178. package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
  179. package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
  180. package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
  181. package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
  182. package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
  183. package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
  184. package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
  185. package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
  186. package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
  187. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
  188. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
  189. package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
  190. package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
  191. package/second-brain/Sessions/_Index.md +15 -1
  192. package/second-brain/Shared/AI-Context-Index.md +22 -0
  193. package/second-brain/Shared/Context-Packs/_Index.md +9 -1
  194. package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
  195. package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
  196. package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
  197. package/second-brain/Shared/Operating-State/current-state.md +14 -4
  198. package/second-brain/Shared/Scripts/_Index.md +3 -1
  199. package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
  200. package/second-brain/Shared/Tech-Standards/_Index.md +6 -1
  201. package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
  202. package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
  203. package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
  204. package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -0
  205. package/second-brain/Shared/User-Memory/_Index.md +4 -1
  206. package/second-brain/Shared/User-Memory/response-examples.md +98 -0
  207. package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
  208. package/second-brain/Templates/_Index.md +9 -0
  209. package/second-brain/Templates/final-lite.md +111 -0
  210. package/second-brain/Templates/final.md +231 -0
  211. package/second-brain/Templates/project-workspace/_Index.md +31 -0
  212. package/second-brain/Templates/project-workspace/context.md +28 -0
  213. package/second-brain/Templates/project-workspace/current-state.md +29 -0
  214. package/second-brain/Templates/project-workspace/overview.md +39 -0
  215. package/second-brain/Templates/project-workspace/repo.md +33 -0
  216. package/second-brain/Vault Structure Map.md +2 -1
  217. package/skills/structured-output-llm/SKILL.md +1 -1
package/dist/ui/banner.js CHANGED
@@ -1,15 +1,89 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Box, Text } from 'ink';
2
+ import { Box, Text, useStdout } from 'ink';
3
+ import BigText from 'ink-big-text';
3
4
  import Gradient from 'ink-gradient';
4
5
  import { homedir } from 'node:os';
5
6
  import { readFileSync } from 'node:fs';
6
7
  import { BRAND } from '../brand.js';
7
8
  // gradient ของ Sanook: เขียว → ส้ม → ฟ้า (สนุก = สดใส)
8
9
  const SANOOK_GRADIENT = ['#22C55E', '#F97316', '#38BDF8'];
10
+ const BANNER_TITLE = BRAND.bannerWide.toUpperCase();
11
+ const COMMAND_HINTS = ['/help', '/tools', '/mcp', '/status'];
12
+ const BRAND_LINE = 'งานหนักให้เบาลง · ไม่เบาความรับผิดชอบ · local-first memory';
13
+ const WORKFLOW = ['plan', 'patch', 'prove', 'remember'];
14
+ const PROMISE = ['readable', 'recoverable', 'remembered'];
15
+ const SERVICE_ROUTES = [
16
+ ['1', 'Code', '@file · /tools · /diff'],
17
+ ['2', 'Brain', 'brain context · /skills · /compress'],
18
+ ['3', 'Connect', '/mcp · serve · webhooks'],
19
+ ['4', 'Ship', '/cost · /copy · /undo'],
20
+ ];
21
+ const WIDE_WORDMARK_MIN_COLUMNS = 96;
22
+ const COMPACT_PANEL_COLUMNS = 76;
23
+ const TINY_PANEL_COLUMNS = 44;
24
+ const MAX_PANEL_COLUMNS = 100;
9
25
  // version จาก package.json (single source of truth) — กัน default drift เหมือน bin.ts
10
26
  const VERSION = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
11
- /** welcome banner — minimal: gradient wordmark + meta บรรทัดเดียว (terminal-first, ไม่รก) */
12
- export function Banner({ model, version = VERSION, account = 'BYOK', cwd }) {
27
+ const clip = (text, width) => {
28
+ if (width <= 0)
29
+ return '';
30
+ return text.length > width ? `${text.slice(0, Math.max(0, width - 1))}…` : text;
31
+ };
32
+ function signalText(signals) {
33
+ return signals
34
+ .filter((signal) => signal.label.trim() && signal.value.trim())
35
+ .map((signal) => {
36
+ const prefix = signal.tone === 'warn' ? '!' : signal.tone === 'muted' ? '-' : '+';
37
+ return `${prefix} ${signal.label} ${signal.value}`;
38
+ })
39
+ .join(' · ');
40
+ }
41
+ function bannerLines({ account, dir, model, mode, signals, version, }, columns) {
42
+ const title = `${BANNER_TITLE} v${version} · terminal AI agent · ${account}`;
43
+ const status = `● model ${model} · mode ${mode} · cwd ${dir}`;
44
+ const signalLine = signalText(signals);
45
+ const flow = `Flow ${WORKFLOW.join(' -> ')} · Promise ${PROMISE.join(' · ')}`;
46
+ const routeLine = `Routes ${SERVICE_ROUTES.map(([num, label]) => `${num} ${label}`).join(' | ')} · ${COMMAND_HINTS.join(' · ')}`;
47
+ if (columns < TINY_PANEL_COLUMNS) {
48
+ return [
49
+ title,
50
+ `● ${model} · ${mode}`,
51
+ ...(signalLine ? [`Signals ${signalLine}`] : []),
52
+ '› /help · /tools · /mcp',
53
+ ];
54
+ }
55
+ if (columns < COMPACT_PANEL_COLUMNS) {
56
+ return [
57
+ title,
58
+ status,
59
+ ...(signalLine ? [`Signals ${signalLine}`] : []),
60
+ `◆ ${BRAND_LINE}`,
61
+ `Flow ${WORKFLOW.join(' -> ')}`,
62
+ '› routes: Code · Brain · Connect · Ship',
63
+ '› code: @file · /tools · /diff',
64
+ '› brain: context · skills · compress',
65
+ '› connect: /mcp · serve',
66
+ '› ship: /copy · /cost · /undo',
67
+ ];
68
+ }
69
+ return [
70
+ title,
71
+ status,
72
+ ...(signalLine ? [`Signals ${signalLine}`] : []),
73
+ `◆ ${BRAND_LINE}`,
74
+ flow,
75
+ routeLine,
76
+ ...SERVICE_ROUTES.map(([num, label, hint]) => `› ${num} ${label.padEnd(7)} ${hint}`),
77
+ ];
78
+ }
79
+ /** welcome banner — Hermes-style responsive wordmark + compact Sanook launchpad. */
80
+ export function Banner({ model, version = VERSION, account = 'BYOK', cwd, mode = 'auto', columns, signals = [] }) {
81
+ const { stdout } = useStdout();
13
82
  const dir = (cwd ?? process.cwd()).replace(homedir(), '~');
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] })] }));
83
+ const terminalColumns = Math.max(1, Math.floor(columns ?? stdout?.columns ?? MAX_PANEL_COLUMNS));
84
+ const showWordmark = terminalColumns >= WIDE_WORDMARK_MIN_COLUMNS;
85
+ const panelWidth = Math.max(28, Math.min(terminalColumns, MAX_PANEL_COLUMNS));
86
+ const innerWidth = Math.max(1, panelWidth - 4);
87
+ const lines = bannerLines({ account, dir, model, mode, signals, version }, terminalColumns);
88
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [showWordmark ? (_jsx(Gradient, { colors: SANOOK_GRADIENT, children: _jsx(BigText, { text: BANNER_TITLE, font: "block", align: "left" }) })) : null, _jsx(Box, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, width: panelWidth, children: lines.map((line, index) => (_jsx(Text, { color: index === 0 ? 'cyan' : undefined, dimColor: index > 0, wrap: "truncate-end", children: clip(line, innerWidth) }, `${index}-${line}`))) })] }));
15
89
  }
@@ -1,13 +1,40 @@
1
- import { readFileSync, appendFileSync, mkdirSync } from 'node:fs';
1
+ import { readFileSync, appendFileSync, chmodSync, mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { appHomePath, persistenceEnabled } from '../brand.js';
3
3
  // prompt history แบบ persist ข้าม session (เลียน shell history) — เก็บที่ ~/.sanook/history
4
4
  const HISTORY_PATH = appHomePath('history');
5
5
  const MAX_ENTRIES = 500;
6
+ function historyLines() {
7
+ return readFileSync(HISTORY_PATH, 'utf8')
8
+ .split('\n')
9
+ .map((line) => line.trim())
10
+ .filter(Boolean);
11
+ }
12
+ function trimHistoryFile() {
13
+ const lines = historyLines();
14
+ if (lines.length <= MAX_ENTRIES)
15
+ return;
16
+ writeFileSync(HISTORY_PATH, `${lines.slice(-MAX_ENTRIES).join('\n')}\n`, { mode: 0o600 });
17
+ }
18
+ function lastPersistedPrompt() {
19
+ try {
20
+ return historyLines().at(-1);
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ function persistedPrompt(prompt) {
27
+ return prompt.replace(/[\r\n]+/g, ' ');
28
+ }
29
+ function samePersistedPrompt(persisted, last) {
30
+ return last !== undefined && persisted === persistedPrompt(last.trim());
31
+ }
6
32
  /** โหลด prompt เก่า (เก่า→ใหม่) สำหรับ Up/Down navigation ใน REPL */
7
33
  export function loadHistory() {
34
+ if (!persistenceEnabled())
35
+ return [];
8
36
  try {
9
- const lines = readFileSync(HISTORY_PATH, 'utf8').split('\n').filter(Boolean);
10
- return lines.slice(-MAX_ENTRIES);
37
+ return historyLines().slice(-MAX_ENTRIES);
11
38
  }
12
39
  catch {
13
40
  return [];
@@ -18,11 +45,16 @@ export function appendHistory(prompt, last) {
18
45
  if (!persistenceEnabled())
19
46
  return;
20
47
  const p = prompt.trim();
21
- if (!p || p === last)
48
+ if (!p || p.startsWith('/'))
49
+ return;
50
+ const persisted = persistedPrompt(p);
51
+ if (samePersistedPrompt(persisted, last) || persisted === lastPersistedPrompt())
22
52
  return;
23
53
  try {
24
54
  mkdirSync(appHomePath(), { recursive: true });
25
- appendFileSync(HISTORY_PATH, `${p.replace(/\n/g, ' ')}\n`, { mode: 0o600 });
55
+ appendFileSync(HISTORY_PATH, `${persisted}\n`, { mode: 0o600 });
56
+ trimHistoryFile();
57
+ chmodSync(HISTORY_PATH, 0o600);
26
58
  }
27
59
  catch {
28
60
  /* เขียนไม่ได้ = ไม่เป็นไร (history เป็น nice-to-have) */
@@ -0,0 +1,122 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { memo, useRef } from 'react';
4
+ const FENCE_RE = /^\s*(`{3,}|~{3,})(.*)$/;
5
+ function clip(text, width) {
6
+ if (width <= 0)
7
+ return '';
8
+ return text.length > width ? `${text.slice(0, Math.max(0, width - 3))}...` : text;
9
+ }
10
+ function bodyWidth(columns) {
11
+ return Math.max(24, Math.min(Math.max(30, columns - 4), 100));
12
+ }
13
+ function lineStartsFence(line) {
14
+ return FENCE_RE.test(line);
15
+ }
16
+ export function findStableMarkdownBoundary(text) {
17
+ let inFence = false;
18
+ let last = -1;
19
+ for (let i = 0; i < text.length;) {
20
+ const nl = text.indexOf('\n', i);
21
+ const end = nl === -1 ? text.length : nl;
22
+ const line = text.slice(i, end);
23
+ if (lineStartsFence(line))
24
+ inFence = !inFence;
25
+ if (!inFence && text.slice(end, end + 2) === '\n\n')
26
+ last = end + 2;
27
+ if (nl === -1)
28
+ break;
29
+ i = nl + 1;
30
+ }
31
+ return last;
32
+ }
33
+ function inlineSegments(text) {
34
+ const out = [];
35
+ const re = /(`[^`\n]+`|\*\*[^*\n]+\*\*)/g;
36
+ let last = 0;
37
+ for (const match of text.matchAll(re)) {
38
+ const index = match.index ?? 0;
39
+ if (index > last)
40
+ out.push({ kind: 'text', text: text.slice(last, index) });
41
+ const token = match[0];
42
+ out.push(token.startsWith('`')
43
+ ? { kind: 'code', text: token.slice(1, -1) }
44
+ : { kind: 'bold', text: token.slice(2, -2) });
45
+ last = index + token.length;
46
+ }
47
+ if (last < text.length)
48
+ out.push({ kind: 'text', text: text.slice(last) });
49
+ return out;
50
+ }
51
+ function InlineMarkdown({ text }) {
52
+ return (_jsx(Text, { children: inlineSegments(text).map((segment, index) => {
53
+ if (segment.kind === 'code') {
54
+ return (_jsx(Text, { color: "yellow", children: segment.text }, index));
55
+ }
56
+ if (segment.kind === 'bold') {
57
+ return (_jsx(Text, { bold: true, children: segment.text }, index));
58
+ }
59
+ return _jsx(Text, { children: segment.text }, index);
60
+ }) }));
61
+ }
62
+ export function MarkdownText({ columns, text }) {
63
+ const width = bodyWidth(columns);
64
+ const lines = text.replace(/\r\n/g, '\n').split('\n');
65
+ const nodes = [];
66
+ let inFence = false;
67
+ let fenceLang = '';
68
+ for (let index = 0; index < lines.length; index += 1) {
69
+ const raw = lines[index] ?? '';
70
+ const fence = FENCE_RE.exec(raw);
71
+ if (fence) {
72
+ inFence = !inFence;
73
+ fenceLang = inFence ? fence[2]?.trim() ?? '' : '';
74
+ nodes.push(_jsx(Text, { color: "cyan", dimColor: true, children: inFence ? `code${fenceLang ? ` ${clip(fenceLang, 24)}` : ''}` : 'end code' }, `f-${index}`));
75
+ continue;
76
+ }
77
+ if (inFence) {
78
+ nodes.push(_jsxs(Text, { color: "gray", wrap: "truncate-end", children: [' ', clip(raw || ' ', width - 2)] }, `c-${index}`));
79
+ continue;
80
+ }
81
+ const trimmed = raw.trim();
82
+ if (!trimmed) {
83
+ nodes.push(_jsx(Text, { children: " " }, `b-${index}`));
84
+ continue;
85
+ }
86
+ const heading = /^(#{1,6})\s+(.+)$/.exec(trimmed);
87
+ if (heading) {
88
+ nodes.push(_jsx(Text, { color: "cyan", bold: true, wrap: "truncate-end", children: clip(heading[2] ?? '', width) }, `h-${index}`));
89
+ continue;
90
+ }
91
+ const quote = /^>\s?(.*)$/.exec(trimmed);
92
+ if (quote) {
93
+ nodes.push(_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ['> ', clip(quote[1] ?? '', width - 2)] }, `q-${index}`));
94
+ continue;
95
+ }
96
+ const bullet = /^([-*])\s+(.+)$/.exec(trimmed);
97
+ if (bullet) {
98
+ nodes.push(_jsxs(Text, { wrap: "truncate-end", children: ['- ', _jsx(InlineMarkdown, { text: clip(bullet[2] ?? '', width - 2) })] }, `li-${index}`));
99
+ continue;
100
+ }
101
+ const ordered = /^(\d+[.)])\s+(.+)$/.exec(trimmed);
102
+ if (ordered) {
103
+ const marker = `${ordered[1]} `;
104
+ nodes.push(_jsxs(Text, { wrap: "truncate-end", children: [marker, _jsx(InlineMarkdown, { text: clip(ordered[2] ?? '', width - marker.length) })] }, `ol-${index}`));
105
+ continue;
106
+ }
107
+ nodes.push(_jsx(Text, { wrap: "truncate-end", children: _jsx(InlineMarkdown, { text: clip(trimmed, width) }) }, `p-${index}`));
108
+ }
109
+ return _jsx(Box, { flexDirection: "column", children: nodes });
110
+ }
111
+ const MemoMarkdownText = memo(MarkdownText);
112
+ export const StreamingMarkdownText = memo(function StreamingMarkdownText({ columns, text }) {
113
+ const stableRef = useRef('');
114
+ if (!text.startsWith(stableRef.current))
115
+ stableRef.current = '';
116
+ const boundary = findStableMarkdownBoundary(text);
117
+ if (boundary > stableRef.current.length)
118
+ stableRef.current = text.slice(0, boundary);
119
+ const stable = stableRef.current;
120
+ const tail = text.slice(stable.length);
121
+ return (_jsxs(Box, { flexDirection: "column", children: [stable ? _jsx(MemoMarkdownText, { columns: columns, text: stable }) : null, tail ? _jsx(MarkdownText, { columns: columns, text: tail }) : null] }));
122
+ });
@@ -1,6 +1,7 @@
1
1
  import { readFile, realpath } from 'node:fs/promises';
2
- import { resolve, extname } from 'node:path';
2
+ import { extname } from 'node:path';
3
3
  import { checkReadPath } from '../tools/permission.js';
4
+ import { resolveAgentPath } from '../tools/util.js';
4
5
  // @-file mentions: "@path" ใน prompt → inline เนื้อหาไฟล์ (text) หรือแนบเป็น image (รูป)
5
6
  // ลด tool round-trip (agent ไม่ต้อง read_file เอง) + เปิดทาง vision input
6
7
  const IMAGE_EXT = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp']);
@@ -15,7 +16,7 @@ export async function expandMentions(input) {
15
16
  const errors = [];
16
17
  const inlined = [];
17
18
  for (const rel of [...new Set(mentions)]) {
18
- const abs = resolve(rel);
19
+ const abs = resolveAgentPath(rel);
19
20
  // canonicalize ก่อนเช็ก extension → symlink ที่ชื่อไม่มีนามสกุลแต่ชี้ไปรูป ก็จับเป็น image ถูก
20
21
  const real = await realpath(abs).catch(() => abs);
21
22
  if (IMAGE_EXT.has(extname(real).toLowerCase())) {