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.
- package/.env.example +161 -3
- package/CHANGELOG.md +148 -10
- package/README.md +255 -26
- package/README.th.md +95 -7
- package/dist/approval.js +13 -0
- package/dist/bin.js +3552 -155
- package/dist/brain-consolidate.js +335 -0
- package/dist/brain-context.js +262 -0
- package/dist/brain-doctor.js +318 -0
- package/dist/brain-eval.js +186 -0
- package/dist/brain-final.js +377 -0
- package/dist/brain-metrics.js +277 -0
- package/dist/brain-new.js +402 -0
- package/dist/brain-pack.js +210 -0
- package/dist/brain-repair.js +280 -0
- package/dist/brain-review.js +382 -0
- package/dist/brain.js +15 -1
- package/dist/brand.js +1 -1
- package/dist/cli-args.js +190 -0
- package/dist/cli-option-values.js +16 -0
- package/dist/clipboard.js +65 -0
- package/dist/commands.js +266 -27
- package/dist/compaction.js +96 -11
- package/dist/config.js +149 -33
- package/dist/context-compression.js +191 -0
- package/dist/context-pack.js +145 -0
- package/dist/cost.js +49 -15
- package/dist/dashboard/api-helpers.js +87 -0
- package/dist/dashboard/server.js +179 -0
- package/dist/dashboard/static/app.js +277 -0
- package/dist/dashboard/static/index.html +39 -0
- package/dist/dashboard/static/styles.css +85 -0
- package/dist/diff.js +10 -2
- package/dist/first-run.js +21 -0
- package/dist/gateway/auth.js +49 -9
- package/dist/gateway/bluebubbles.js +205 -0
- package/dist/gateway/config.js +929 -0
- package/dist/gateway/deliver.js +399 -0
- package/dist/gateway/discord.js +124 -0
- package/dist/gateway/doctor.js +456 -0
- package/dist/gateway/email.js +501 -0
- package/dist/gateway/googlechat.js +207 -0
- package/dist/gateway/homeassistant.js +256 -0
- package/dist/gateway/ledger.js +38 -1
- package/dist/gateway/line.js +171 -0
- package/dist/gateway/lock.js +3 -1
- package/dist/gateway/matrix.js +366 -0
- package/dist/gateway/mattermost.js +322 -0
- package/dist/gateway/ntfy.js +218 -0
- package/dist/gateway/schedule.js +31 -4
- package/dist/gateway/serve.js +267 -7
- package/dist/gateway/server.js +253 -19
- package/dist/gateway/service.js +224 -0
- package/dist/gateway/session.js +362 -0
- package/dist/gateway/signal.js +351 -0
- package/dist/gateway/slack.js +124 -0
- package/dist/gateway/sms.js +169 -0
- package/dist/gateway/targets.js +576 -0
- package/dist/gateway/teams.js +106 -0
- package/dist/gateway/telegram.js +38 -15
- package/dist/gateway/webhooks.js +220 -0
- package/dist/gateway/whatsapp.js +230 -0
- package/dist/hooks.js +13 -2
- package/dist/hotkeys.js +21 -0
- package/dist/i18n/en.js +98 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/th.js +98 -0
- package/dist/i18n/types.js +1 -0
- package/dist/insights-args.js +55 -0
- package/dist/insights.js +86 -0
- package/dist/knowledge.js +55 -29
- package/dist/loop.js +157 -29
- package/dist/lsp/index.js +23 -5
- package/dist/mcp-hub.js +33 -0
- package/dist/mcp-registry.js +494 -0
- package/dist/mcp-risk.js +71 -0
- package/dist/mcp-server.js +1 -1
- package/dist/mcp.js +120 -10
- package/dist/memory-log.js +90 -0
- package/dist/memory-store.js +37 -1
- package/dist/memory.js +148 -37
- package/dist/model-picker.js +58 -0
- package/dist/orchestrate.js +51 -19
- package/dist/personality.js +58 -0
- package/dist/plan-handoff.js +17 -0
- package/dist/polyglot.js +162 -0
- package/dist/process-runner.js +96 -0
- package/dist/project-init.js +91 -0
- package/dist/project-registry.js +143 -0
- package/dist/project-scaffold.js +124 -0
- package/dist/prompt-size.js +155 -0
- package/dist/providers/codex-login.js +138 -0
- package/dist/providers/codex.js +89 -43
- package/dist/providers/keys.js +22 -1
- package/dist/providers/models.js +2 -2
- package/dist/providers/registry.js +14 -47
- package/dist/search/chunk.js +7 -8
- package/dist/search/cli.js +83 -0
- package/dist/search/embed-store.js +3 -0
- package/dist/search/embedding-config.js +22 -0
- package/dist/search/engine.js +2 -13
- package/dist/search/indexer.js +44 -1
- package/dist/search/store.js +23 -1
- package/dist/session-distill.js +84 -0
- package/dist/session.js +92 -16
- package/dist/skill-install.js +53 -13
- package/dist/skills.js +33 -0
- package/dist/slash-completion.js +155 -0
- package/dist/support-dump.js +206 -0
- package/dist/tool-catalog.js +59 -0
- package/dist/tools/edit.js +45 -15
- package/dist/tools/git.js +10 -5
- package/dist/tools/homeassistant.js +106 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/list.js +19 -6
- package/dist/tools/permission.js +992 -12
- package/dist/tools/polyglot.js +126 -0
- package/dist/tools/read.js +16 -4
- package/dist/tools/sandbox.js +38 -13
- package/dist/tools/schedule.js +19 -3
- package/dist/tools/search.js +226 -15
- package/dist/tools/task.js +40 -9
- package/dist/tools/timeout.js +23 -3
- package/dist/tools/web-fetch-tool.js +33 -0
- package/dist/trust.js +11 -1
- package/dist/turn-retrieval.js +83 -0
- package/dist/ui/app.js +878 -32
- package/dist/ui/banner.js +78 -4
- package/dist/ui/history.js +37 -5
- package/dist/ui/markdown.js +122 -0
- package/dist/ui/mentions.js +3 -2
- package/dist/ui/overlay.js +496 -0
- package/dist/ui/queue.js +23 -0
- package/dist/ui/render.js +20 -1
- package/dist/ui/session-panel.js +115 -0
- package/dist/ui/setup-providers.js +40 -0
- package/dist/ui/setup.js +172 -46
- package/dist/ui/status.js +142 -0
- package/dist/ui/thinking-panel.js +36 -0
- package/dist/ui/tool-trail.js +97 -0
- package/dist/ui/transcript.js +26 -0
- package/dist/ui/useBusyElapsed.js +19 -0
- package/dist/ui/useEditor.js +144 -5
- package/dist/ui/useGitBranch.js +57 -0
- package/dist/update.js +56 -17
- package/dist/web-fetch.js +637 -0
- package/dist/web-surface.js +190 -0
- package/dist/worktree.js +175 -4
- package/package.json +5 -5
- package/second-brain/AGENTS.md +6 -4
- package/second-brain/CLAUDE.md +7 -1
- package/second-brain/Evals/_Index.md +10 -2
- package/second-brain/Evals/quality-ledger.md +9 -1
- package/second-brain/Evals/second-brain-benchmarks.md +62 -0
- package/second-brain/GEMINI.md +5 -4
- package/second-brain/Home.md +1 -1
- package/second-brain/Projects/_Index.md +19 -4
- package/second-brain/Projects/sanook-cli/_Index.md +30 -0
- package/second-brain/Projects/sanook-cli/context.md +35 -0
- package/second-brain/Projects/sanook-cli/current-state.md +32 -0
- package/second-brain/Projects/sanook-cli/overview.md +41 -0
- package/second-brain/Projects/sanook-cli/repo.md +34 -0
- package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +197 -0
- package/second-brain/README.md +1 -1
- package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
- package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
- package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
- package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -0
- package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
- package/second-brain/Research/2026-06-19-hermes-python-architecture-for-sanook.md +49 -0
- package/second-brain/Research/2026-06-19-terminal-ui-brand-research.md +52 -0
- package/second-brain/Research/_Index.md +8 -1
- package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
- package/second-brain/Reviews/_Index.md +1 -1
- package/second-brain/Runbooks/_Index.md +6 -1
- package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
- package/second-brain/SANOOK.md +45 -0
- package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
- package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
- package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
- package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
- package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
- package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
- package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
- package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
- package/second-brain/Sessions/_Index.md +15 -1
- package/second-brain/Shared/AI-Context-Index.md +22 -0
- package/second-brain/Shared/Context-Packs/_Index.md +9 -1
- package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
- package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
- package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
- package/second-brain/Shared/Operating-State/current-state.md +14 -4
- package/second-brain/Shared/Scripts/_Index.md +3 -1
- package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +6 -1
- package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
- package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
- package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -0
- package/second-brain/Shared/User-Memory/_Index.md +4 -1
- package/second-brain/Shared/User-Memory/response-examples.md +98 -0
- package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
- package/second-brain/Templates/_Index.md +9 -0
- package/second-brain/Templates/final-lite.md +111 -0
- package/second-brain/Templates/final.md +231 -0
- package/second-brain/Templates/project-workspace/_Index.md +31 -0
- package/second-brain/Templates/project-workspace/context.md +28 -0
- package/second-brain/Templates/project-workspace/current-state.md +29 -0
- package/second-brain/Templates/project-workspace/overview.md +39 -0
- package/second-brain/Templates/project-workspace/repo.md +33 -0
- package/second-brain/Vault Structure Map.md +2 -1
- package/skills/structured-output-llm/SKILL.md +1 -1
package/dist/commands.js
CHANGED
|
@@ -1,37 +1,71 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { PROVIDERS, parseSpec } from './providers/registry.js';
|
|
3
|
+
import { canonicalSpec, consoleUrl, hasUsableEnvKey, PROVIDERS, parseSpec } from './providers/registry.js';
|
|
4
4
|
import { appHomePath, BRAND } from './brand.js';
|
|
5
5
|
import { parseFrontmatter } from './skills.js';
|
|
6
6
|
import { projectConfigPathIfTrusted } from './trust.js';
|
|
7
|
-
|
|
7
|
+
import { normalizePersonalityName, personalityListText } from './personality.js';
|
|
8
|
+
import { parseInsightsArgs } from './insights-args.js';
|
|
9
|
+
import { formatHotkeys } from './hotkeys.js';
|
|
10
|
+
import { formatToolCatalog } from './tool-catalog.js';
|
|
11
|
+
export const HELP_TEXT = `คำสั่ง:
|
|
8
12
|
/help แสดงคำสั่งทั้งหมด
|
|
9
|
-
/
|
|
13
|
+
/new, /reset เริ่มบทสนทนาใหม่
|
|
14
|
+
/status ดูสถานะ session ปัจจุบัน
|
|
15
|
+
/model [spec] ดู/เปลี่ยน model — /model เปิด picker 2 ขั้น (provider → model)
|
|
16
|
+
/setup ดูขั้นตอน setup wizard (model · agent · tools · gateway · brain)
|
|
17
|
+
/dashboard เปิด Sanook Dashboard (local web UI)
|
|
18
|
+
/personality [name]
|
|
19
|
+
ดู/ตั้ง personality overlay
|
|
20
|
+
/details [thinking|tools] [hidden|collapsed|expanded]
|
|
21
|
+
คุมแผง thinking/tool trail แบบ Hermes-style
|
|
22
|
+
/platforms ดู providers + messaging platforms ที่รองรับ
|
|
10
23
|
/tools ดู tools ที่ agent ใช้ได้
|
|
11
|
-
/
|
|
24
|
+
/mcp เปิด MCP Hub overlay
|
|
25
|
+
/skills เปิด Skills Hub overlay (จัดการ: ${BRAND.cliName} skill list)
|
|
26
|
+
/sessions เปิด Session Switcher overlay · /trail พับ/ขยาย tool trail
|
|
27
|
+
/tasks ดู background sub-agents (task_spawn)
|
|
12
28
|
/diff ดู git diff (สิ่งที่ agent แก้ในรอบนี้)
|
|
29
|
+
/retry รัน prompt ล่าสุดอีกครั้ง
|
|
30
|
+
/stop หยุด turn ที่กำลังรัน
|
|
13
31
|
/undo stash การแก้ไฟล์ล่าสุด (กู้คืนด้วย git stash pop)
|
|
14
32
|
/rewind ย้อนกลับ 1 turn (คืนไฟล์ git + ตัดบทสนทนา, recoverable)
|
|
15
|
-
/cost
|
|
33
|
+
/cost, /usage ดู token + cost รอบล่าสุด
|
|
34
|
+
/insights [--days N] [--all]
|
|
35
|
+
ดู usage/session insights ในเครื่อง
|
|
36
|
+
/hotkeys เปิด overlay คีย์ลัดใน REPL
|
|
37
|
+
/copy [last] copy คำตอบ assistant ล่าสุดไป clipboard/OSC52
|
|
16
38
|
↑/↓ ประวัติ · @ไฟล์ แนบ context/รูป · \\ ลงท้าย = บรรทัดใหม่
|
|
17
39
|
/clear ล้าง conversation (เริ่มใหม่)
|
|
18
|
-
/compact
|
|
40
|
+
/compact, /compress
|
|
41
|
+
บีบ context (truncate · หรือ summarize ถ้าตั้ง compaction)
|
|
19
42
|
/quit ออก
|
|
20
43
|
|
|
21
44
|
นอก REPL (พิมพ์ใน shell):
|
|
22
|
-
${BRAND.cliName} search "<q>" · index · brain init · serve · mcp serve · config set <k> <v>
|
|
45
|
+
${BRAND.cliName} search "<q>" · index · brain init · brain context · brain eval · brain review · brain final · serve · mcp serve · config set <k> <v>
|
|
23
46
|
ดูทั้งหมด: ${BRAND.cliName} --help
|
|
24
47
|
|
|
25
48
|
custom commands:
|
|
26
|
-
~/.sanook/commands/<name>.md และ .sanook/commands/<name>.md (project ต้อง trust ก่อน)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
|
|
49
|
+
~/.sanook/commands/<name>.md และ .sanook/commands/<name>.md (project ต้อง trust ก่อน)
|
|
50
|
+
args: ใช้ $ARGUMENTS หรือ {{ args }}; ถ้าไม่มี placeholder จะ append args ต่อท้าย`;
|
|
51
|
+
const MESSAGING_PLATFORMS = [
|
|
52
|
+
'telegram',
|
|
53
|
+
'discord',
|
|
54
|
+
'slack',
|
|
55
|
+
'mattermost',
|
|
56
|
+
'homeassistant',
|
|
57
|
+
'email',
|
|
58
|
+
'line',
|
|
59
|
+
'sms',
|
|
60
|
+
'ntfy',
|
|
61
|
+
'signal',
|
|
62
|
+
'whatsapp',
|
|
63
|
+
'matrix',
|
|
64
|
+
'googlechat',
|
|
65
|
+
'bluebubbles',
|
|
66
|
+
'teams',
|
|
67
|
+
'webhooks',
|
|
68
|
+
];
|
|
35
69
|
export function parseSlashInvocation(input) {
|
|
36
70
|
const trimmed = input.trim();
|
|
37
71
|
if (!trimmed.startsWith('/'))
|
|
@@ -39,7 +73,10 @@ export function parseSlashInvocation(input) {
|
|
|
39
73
|
const match = /^\/(\S+)(?:\s+([\s\S]*))?$/.exec(trimmed);
|
|
40
74
|
if (!match)
|
|
41
75
|
return null;
|
|
42
|
-
|
|
76
|
+
const name = match[1].toLowerCase();
|
|
77
|
+
if (name !== '?' && !isValidCommandName(name))
|
|
78
|
+
return null;
|
|
79
|
+
return { name, args: match[2] ?? '' };
|
|
43
80
|
}
|
|
44
81
|
/** /model (ไม่มี arg) — โชว์ model ปัจจุบัน + ตัวเลือกของ provider นั้น (alias จาก registry) */
|
|
45
82
|
function modelMenu(current) {
|
|
@@ -60,37 +97,198 @@ function modelMenu(current) {
|
|
|
60
97
|
.filter(Boolean)
|
|
61
98
|
.join('\n');
|
|
62
99
|
}
|
|
100
|
+
function missingKeyHint(provider) {
|
|
101
|
+
const cfg = PROVIDERS[provider];
|
|
102
|
+
if (!cfg?.requiresKey || hasUsableEnvKey(provider))
|
|
103
|
+
return undefined;
|
|
104
|
+
const url = consoleUrl(provider);
|
|
105
|
+
const lines = [
|
|
106
|
+
`⚠ ยังไม่มี API key ของ ${cfg.label} (${cfg.envVar}) — model นี้จะยังรันไม่ได้จนกว่าจะตั้ง key`,
|
|
107
|
+
url ? ` • เอา key ที่: ${url}` : undefined,
|
|
108
|
+
` • ตั้ง: export ${cfg.envVar}="..." หรือรัน ${BRAND.cliName} เพื่อเข้า setup wizard`,
|
|
109
|
+
].filter(Boolean);
|
|
110
|
+
if (provider === 'openai') {
|
|
111
|
+
lines.push(' • ถ้าต้องการใช้ ChatGPT plan ไม่ใช้ API key: /model codex แล้วรัน codex login');
|
|
112
|
+
}
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
function platformMenu() {
|
|
116
|
+
return [
|
|
117
|
+
`providers: ${Object.keys(PROVIDERS).join(' · ')}`,
|
|
118
|
+
`messaging: ${MESSAGING_PLATFORMS.join(' · ')}`,
|
|
119
|
+
`setup: ${BRAND.cliName} setup หรือ ${BRAND.cliName} gateway setup <platform>`,
|
|
120
|
+
].join('\n');
|
|
121
|
+
}
|
|
122
|
+
function statusMenu(ctx) {
|
|
123
|
+
const { provider } = parseSpec(ctx.model);
|
|
124
|
+
const cfg = PROVIDERS[provider];
|
|
125
|
+
return [
|
|
126
|
+
`session: REPL`,
|
|
127
|
+
`model: ${ctx.model}`,
|
|
128
|
+
`provider: ${cfg?.label ?? provider}`,
|
|
129
|
+
`usage: ${ctx.costSummary ?? '(ยังไม่มี usage รอบนี้)'}`,
|
|
130
|
+
`platforms: พิมพ์ /platforms`,
|
|
131
|
+
`system status: ${BRAND.cliName} status`,
|
|
132
|
+
].join('\n');
|
|
133
|
+
}
|
|
134
|
+
function modelChange(spec) {
|
|
135
|
+
const canonical = canonicalSpec(spec);
|
|
136
|
+
const { provider, model } = parseSpec(canonical);
|
|
137
|
+
if (!PROVIDERS[provider]) {
|
|
138
|
+
return {
|
|
139
|
+
handled: true,
|
|
140
|
+
message: `provider ไม่รองรับ: "${provider}" — รองรับ: ${Object.keys(PROVIDERS).join(' · ')}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (!model) {
|
|
144
|
+
return {
|
|
145
|
+
handled: true,
|
|
146
|
+
message: `model spec ไม่ครบ: "${spec}" — ใช้ /model <alias> หรือ /model <provider:model>`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const hint = missingKeyHint(provider);
|
|
150
|
+
return {
|
|
151
|
+
handled: true,
|
|
152
|
+
modelChange: canonical,
|
|
153
|
+
message: [`เปลี่ยน model → ${canonical}`, hint].filter(Boolean).join('\n'),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
63
156
|
/** parse input — ถ้าขึ้นต้น / = slash command, ไม่งั้น handled=false (ส่งเข้า agent) */
|
|
64
157
|
export function parseCommand(input, ctx) {
|
|
65
158
|
const trimmed = input.trim();
|
|
66
159
|
if (!trimmed.startsWith('/'))
|
|
67
160
|
return { handled: false };
|
|
68
|
-
const [
|
|
161
|
+
const [rawCmd, ...args] = trimmed.slice(1).split(/\s+/);
|
|
162
|
+
const cmd = rawCmd.toLowerCase();
|
|
69
163
|
switch (cmd) {
|
|
70
164
|
case 'help':
|
|
71
165
|
case '?':
|
|
72
166
|
return { handled: true, action: 'help', message: HELP_TEXT };
|
|
73
167
|
case 'clear':
|
|
168
|
+
case 'new':
|
|
169
|
+
case 'reset':
|
|
74
170
|
return { handled: true, action: 'clear', message: 'ล้าง conversation แล้ว' };
|
|
171
|
+
case 'status':
|
|
172
|
+
return { handled: true, message: statusMenu(ctx) };
|
|
173
|
+
case 'hotkeys':
|
|
174
|
+
return { handled: true, action: 'hotkeys', message: formatHotkeys() };
|
|
75
175
|
case 'compact':
|
|
176
|
+
case 'compress':
|
|
76
177
|
return { handled: true, action: 'compact', message: 'บีบ context แล้ว' };
|
|
178
|
+
case 'copy': {
|
|
179
|
+
const target = args[0]?.toLowerCase();
|
|
180
|
+
if (!target || target === 'last' || target === 'assistant')
|
|
181
|
+
return { handled: true, action: 'copyLast' };
|
|
182
|
+
return { handled: true, message: 'ใช้ /copy หรือ /copy last' };
|
|
183
|
+
}
|
|
77
184
|
case 'quit':
|
|
78
185
|
case 'exit':
|
|
79
186
|
return { handled: true, action: 'quit' };
|
|
80
187
|
case 'model':
|
|
81
188
|
if (!args[0])
|
|
82
|
-
return { handled: true, message: modelMenu(ctx.model) };
|
|
83
|
-
return
|
|
189
|
+
return { handled: true, action: 'modelPicker', message: modelMenu(ctx.model) };
|
|
190
|
+
return modelChange(args[0]);
|
|
191
|
+
case 'setup':
|
|
192
|
+
return {
|
|
193
|
+
handled: true,
|
|
194
|
+
message: [
|
|
195
|
+
`${BRAND.productName} setup (Hermes-style sections):`,
|
|
196
|
+
` 1. ${BRAND.cliName} setup model — provider + model wizard`,
|
|
197
|
+
` 2. ${BRAND.cliName} setup agent — permissionMode, budget, personality`,
|
|
198
|
+
` 3. ${BRAND.cliName} setup tools — built-in tools + MCP`,
|
|
199
|
+
` 4. ${BRAND.cliName} setup gateway — Telegram/Discord/Slack/…`,
|
|
200
|
+
` 5. ${BRAND.cliName} setup brain — second-brain vault`,
|
|
201
|
+
` หรือรัน ${BRAND.cliName} ครั้งแรก → wizard 10 ขั้น (ภาษา → … → gateway → brain)`,
|
|
202
|
+
].join('\n'),
|
|
203
|
+
};
|
|
204
|
+
case 'dashboard':
|
|
205
|
+
return {
|
|
206
|
+
handled: true,
|
|
207
|
+
message: `Sanook Dashboard — รัน: ${BRAND.cliName} dashboard\n แล้วเปิด http://127.0.0.1:9119 (Chat · Files · Logs · Cron · Channels)`,
|
|
208
|
+
};
|
|
209
|
+
case 'personality': {
|
|
210
|
+
const raw = args.join(' ').trim();
|
|
211
|
+
if (!raw)
|
|
212
|
+
return { handled: true, message: personalityListText() };
|
|
213
|
+
const name = normalizePersonalityName(raw);
|
|
214
|
+
if (!name)
|
|
215
|
+
return { handled: true, message: `ไม่รู้จัก personality: ${raw}\n\n${personalityListText()}` };
|
|
216
|
+
return {
|
|
217
|
+
handled: true,
|
|
218
|
+
action: 'personality',
|
|
219
|
+
personalityChange: name === 'none' ? '' : name,
|
|
220
|
+
message: name === 'none' ? 'ปิด personality overlay แล้ว' : `ตั้ง personality → ${name}`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
84
223
|
case 'tools':
|
|
85
|
-
return { handled: true, message: `tools ที่ agent ใช้ได้ (+ MCP ที่ตั้งไว้):\n ${
|
|
224
|
+
return { handled: true, action: 'toolsHub', message: `tools ที่ agent ใช้ได้ (+ MCP ที่ตั้งไว้):\n ${formatToolCatalog()}` };
|
|
225
|
+
case 'trail': {
|
|
226
|
+
const rawMode = args[0]?.toLowerCase();
|
|
227
|
+
if (!rawMode)
|
|
228
|
+
return { handled: true, action: 'toolTrail', message: 'toggle tool trail view' };
|
|
229
|
+
if (['compact', 'collapse', 'collapsed', 'hide', 'summary'].includes(rawMode)) {
|
|
230
|
+
return { handled: true, action: 'toolTrail', message: 'tool trail → compact', toolTrailMode: 'compact' };
|
|
231
|
+
}
|
|
232
|
+
if (['expanded', 'expand', 'full', 'show'].includes(rawMode)) {
|
|
233
|
+
return { handled: true, action: 'toolTrail', message: 'tool trail → expanded', toolTrailMode: 'expanded' };
|
|
234
|
+
}
|
|
235
|
+
return { handled: true, message: 'ใช้ /trail, /trail compact, หรือ /trail expanded' };
|
|
236
|
+
}
|
|
237
|
+
case 'details': {
|
|
238
|
+
const section = args[0]?.toLowerCase();
|
|
239
|
+
const mode = args[1]?.toLowerCase();
|
|
240
|
+
const usage = 'ใช้ /details thinking|tools hidden|collapsed|expanded';
|
|
241
|
+
if (!section && !mode)
|
|
242
|
+
return { handled: true, message: usage };
|
|
243
|
+
if (section !== 'thinking' && section !== 'tools')
|
|
244
|
+
return { handled: true, message: usage };
|
|
245
|
+
if (mode !== 'hidden' && mode !== 'collapsed' && mode !== 'expanded')
|
|
246
|
+
return { handled: true, message: usage };
|
|
247
|
+
return {
|
|
248
|
+
handled: true,
|
|
249
|
+
action: 'details',
|
|
250
|
+
detailMode: mode,
|
|
251
|
+
detailSection: section,
|
|
252
|
+
message: `details ${section} → ${mode}`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
case 'platforms':
|
|
256
|
+
return { handled: true, message: platformMenu() };
|
|
257
|
+
case 'mcp':
|
|
258
|
+
return {
|
|
259
|
+
handled: true,
|
|
260
|
+
action: 'mcpHub',
|
|
261
|
+
message: `MCP servers — จัดการด้วย "${BRAND.cliName} mcp list/search/install/doctor"`,
|
|
262
|
+
};
|
|
86
263
|
case 'skills':
|
|
87
|
-
return {
|
|
264
|
+
return {
|
|
265
|
+
handled: true,
|
|
266
|
+
action: 'skillsHub',
|
|
267
|
+
message: `skills โหลดจาก built-in + ${appHomePath('skills')} — จัดการด้วย "${BRAND.cliName} skill list/add/remove"`,
|
|
268
|
+
};
|
|
269
|
+
case 'sessions':
|
|
270
|
+
return { handled: true, action: 'sessionsHub', message: `saved sessions — จัดการด้วย "${BRAND.cliName} sessions"` };
|
|
271
|
+
case 'tasks':
|
|
272
|
+
return { handled: true, action: 'tasksHub', message: 'background tasks — จาก task_spawn (Enter ดูรายละเอียด)' };
|
|
88
273
|
case 'diff':
|
|
89
274
|
return { handled: true, action: 'diff' };
|
|
275
|
+
case 'retry':
|
|
276
|
+
return { handled: true, action: 'retry' };
|
|
277
|
+
case 'stop':
|
|
278
|
+
return { handled: true, action: 'stop', message: 'ไม่มี turn ที่กำลังทำงาน' };
|
|
90
279
|
case 'undo':
|
|
91
280
|
return { handled: true, action: 'undo' };
|
|
281
|
+
case 'rewind':
|
|
282
|
+
return { handled: true, action: 'rewind' };
|
|
92
283
|
case 'cost':
|
|
284
|
+
case 'usage':
|
|
93
285
|
return { handled: true, message: ctx.costSummary ?? '(ยังไม่มี usage รอบนี้)' };
|
|
286
|
+
case 'insights': {
|
|
287
|
+
const parsed = parseInsightsArgs(args);
|
|
288
|
+
if (parsed === null)
|
|
289
|
+
return { handled: true, message: 'ใช้: /insights [--days N] [--all] (N ต้องเป็นจำนวนวันบวก)' };
|
|
290
|
+
return { handled: true, action: 'insights', insightsDays: parsed.days, insightsAll: parsed.all };
|
|
291
|
+
}
|
|
94
292
|
default:
|
|
95
293
|
return { handled: true, message: `ไม่รู้จักคำสั่ง /${cmd} — พิมพ์ /help` };
|
|
96
294
|
}
|
|
@@ -99,11 +297,51 @@ export function parseCommand(input, ctx) {
|
|
|
99
297
|
// ไฟล์ markdown (frontmatter optional) = prompt template ที่ส่งเข้า agent. $ARGUMENTS = ส่วนหลังชื่อคำสั่ง
|
|
100
298
|
// (เลียน Claude Code .claude/commands) — global ~/.sanook/commands + project .sanook/commands (project ทับ)
|
|
101
299
|
export const BUILTIN_COMMANDS = new Set([
|
|
102
|
-
'help',
|
|
300
|
+
'help',
|
|
301
|
+
'?',
|
|
302
|
+
'clear',
|
|
303
|
+
'new',
|
|
304
|
+
'reset',
|
|
305
|
+
'status',
|
|
306
|
+
'hotkeys',
|
|
307
|
+
'compact',
|
|
308
|
+
'compress',
|
|
309
|
+
'copy',
|
|
310
|
+
'quit',
|
|
311
|
+
'exit',
|
|
312
|
+
'model',
|
|
313
|
+
'personality',
|
|
314
|
+
'details',
|
|
315
|
+
'platforms',
|
|
316
|
+
'trail',
|
|
317
|
+
'tools',
|
|
318
|
+
'mcp',
|
|
319
|
+
'skills',
|
|
320
|
+
'sessions',
|
|
321
|
+
'tasks',
|
|
322
|
+
'diff',
|
|
323
|
+
'retry',
|
|
324
|
+
'stop',
|
|
325
|
+
'undo',
|
|
326
|
+
'rewind',
|
|
327
|
+
'cost',
|
|
328
|
+
'usage',
|
|
329
|
+
'insights',
|
|
103
330
|
]);
|
|
104
331
|
function isValidCommandName(name) {
|
|
105
332
|
return /^[a-z0-9][a-z0-9-]{0,40}$/.test(name);
|
|
106
333
|
}
|
|
334
|
+
function compareCommandFiles(a, b) {
|
|
335
|
+
const an = a.toLowerCase();
|
|
336
|
+
const bn = b.toLowerCase();
|
|
337
|
+
if (an !== bn)
|
|
338
|
+
return an.localeCompare(bn);
|
|
339
|
+
if (a === an && b !== bn)
|
|
340
|
+
return 1;
|
|
341
|
+
if (a !== an && b === bn)
|
|
342
|
+
return -1;
|
|
343
|
+
return a.localeCompare(b);
|
|
344
|
+
}
|
|
107
345
|
/** scan custom commands จาก global + project (project override). ข้าม built-in ชื่อซ้ำ */
|
|
108
346
|
export async function loadCustomCommands(cwd = process.cwd()) {
|
|
109
347
|
const out = new Map();
|
|
@@ -119,10 +357,11 @@ export async function loadCustomCommands(cwd = process.cwd()) {
|
|
|
119
357
|
catch {
|
|
120
358
|
continue; // ไม่มีโฟลเดอร์ = ข้าม
|
|
121
359
|
}
|
|
122
|
-
for (const f of files) {
|
|
123
|
-
|
|
360
|
+
for (const f of files.sort(compareCommandFiles)) {
|
|
361
|
+
const normalizedFile = f.toLowerCase();
|
|
362
|
+
if (!normalizedFile.endsWith('.md'))
|
|
124
363
|
continue;
|
|
125
|
-
const name =
|
|
364
|
+
const name = normalizedFile.slice(0, -3);
|
|
126
365
|
if (!isValidCommandName(name) || BUILTIN_COMMANDS.has(name))
|
|
127
366
|
continue;
|
|
128
367
|
try {
|
|
@@ -140,7 +379,7 @@ export async function loadCustomCommands(cwd = process.cwd()) {
|
|
|
140
379
|
export function expandCustomCommand(cmd, args) {
|
|
141
380
|
const a = args.trim();
|
|
142
381
|
if (/\$ARGUMENTS|\{\{\s*args\s*\}\}/.test(cmd.body)) {
|
|
143
|
-
return cmd.body.replace(/\$ARGUMENTS|\{\{\s*args\s*\}\}/g, a);
|
|
382
|
+
return cmd.body.replace(/\$ARGUMENTS|\{\{\s*args\s*\}\}/g, () => a);
|
|
144
383
|
}
|
|
145
384
|
return a ? `${cmd.body}\n\n${a}` : cmd.body;
|
|
146
385
|
}
|
package/dist/compaction.js
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
|
+
import { selectiveCompressText } from './context-compression.js';
|
|
1
2
|
const TRUNC_HEAD = 400;
|
|
2
3
|
const TRUNC_TAIL = 600;
|
|
3
4
|
const CHARS_PER_TOKEN = 4; // ประมาณคร่าวๆ (จริง ~3.5-4 ต่อ token)
|
|
5
|
+
const SELECTIVE_TOOL_TARGET_CHARS = 6_000;
|
|
6
|
+
const SELECTIVE_TOOL_MIN_CHARS = 8_000;
|
|
7
|
+
function textFromMessageContent(content) {
|
|
8
|
+
if (typeof content === 'string')
|
|
9
|
+
return content;
|
|
10
|
+
if (!Array.isArray(content))
|
|
11
|
+
return '';
|
|
12
|
+
return content
|
|
13
|
+
.map((part) => {
|
|
14
|
+
if (typeof part === 'object' && part && 'type' in part && part.type === 'text' && 'text' in part && typeof part.text === 'string')
|
|
15
|
+
return part.text;
|
|
16
|
+
return '';
|
|
17
|
+
})
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.join('\n');
|
|
20
|
+
}
|
|
21
|
+
function latestUserText(messages) {
|
|
22
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
23
|
+
if (messages[i].role !== 'user')
|
|
24
|
+
continue;
|
|
25
|
+
const text = textFromMessageContent(messages[i].content).trim();
|
|
26
|
+
if (text)
|
|
27
|
+
return text;
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
function adaptiveStaleTarget(baseTarget, rank, count) {
|
|
32
|
+
if (count <= 1)
|
|
33
|
+
return baseTarget;
|
|
34
|
+
const recency = rank / Math.max(1, count - 1); // 0 = oldest, 1 = newest stale
|
|
35
|
+
return Math.max(1_500, Math.floor(baseTarget * (0.35 + 0.65 * recency)));
|
|
36
|
+
}
|
|
37
|
+
function adaptiveMinChars(targetChars, baseMinChars) {
|
|
38
|
+
return Math.min(baseMinChars, Math.max(targetChars + 1_000, Math.floor(targetChars * 1.45)));
|
|
39
|
+
}
|
|
4
40
|
/** ตัดข้อความยาว เก็บหัว (intent) + ท้าย (error/result) */
|
|
5
41
|
export function truncateText(s) {
|
|
6
42
|
if (s.length <= TRUNC_HEAD + TRUNC_TAIL + 40)
|
|
@@ -16,23 +52,72 @@ export function truncateText(s) {
|
|
|
16
52
|
*/
|
|
17
53
|
export function pruneToolResults(messages, keepTail = 4) {
|
|
18
54
|
const cut = Math.max(0, messages.length - keepTail);
|
|
19
|
-
|
|
55
|
+
let changed = false;
|
|
56
|
+
const out = messages.map((m, i) => {
|
|
20
57
|
if (i >= cut)
|
|
21
58
|
return m;
|
|
22
59
|
if (m.role !== 'tool' || !Array.isArray(m.content))
|
|
23
60
|
return m;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
61
|
+
const content = m.content.map((part) => {
|
|
62
|
+
if (part.type === 'tool-result' &&
|
|
63
|
+
part.output?.type === 'text' &&
|
|
64
|
+
typeof part.output.value === 'string') {
|
|
65
|
+
const compressed = selectiveCompressText(part.output.value, {
|
|
66
|
+
targetChars: SELECTIVE_TOOL_TARGET_CHARS,
|
|
67
|
+
minChars: SELECTIVE_TOOL_MIN_CHARS,
|
|
68
|
+
});
|
|
69
|
+
if (compressed.changed) {
|
|
70
|
+
changed = true;
|
|
71
|
+
return { ...part, output: { ...part.output, value: compressed.text } };
|
|
72
|
+
}
|
|
73
|
+
const truncated = truncateText(part.output.value);
|
|
74
|
+
if (truncated !== part.output.value) {
|
|
75
|
+
changed = true;
|
|
76
|
+
return { ...part, output: { ...part.output, value: truncated } };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return part;
|
|
80
|
+
});
|
|
81
|
+
return content === m.content ? m : { ...m, content };
|
|
82
|
+
});
|
|
83
|
+
return changed ? out : messages;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Per-step token optimizer (zero LLM cost).
|
|
87
|
+
* Compresses stale, very large tool results before each model request while keeping the latest tail full.
|
|
88
|
+
*/
|
|
89
|
+
export function selectivelyCompressStaleToolResults(messages, keepTail = 6, targetChars = SELECTIVE_TOOL_TARGET_CHARS, minChars = SELECTIVE_TOOL_MIN_CHARS, query = latestUserText(messages)) {
|
|
90
|
+
const cut = Math.max(0, messages.length - keepTail);
|
|
91
|
+
const staleToolIndexes = messages
|
|
92
|
+
.map((m, i) => ({ m, i }))
|
|
93
|
+
.filter(({ m, i }) => i < cut && m.role === 'tool' && Array.isArray(m.content))
|
|
94
|
+
.map(({ i }) => i);
|
|
95
|
+
const rankByIndex = new Map(staleToolIndexes.map((index, rank) => [index, rank]));
|
|
96
|
+
let changed = false;
|
|
97
|
+
const out = messages.map((m, i) => {
|
|
98
|
+
if (i >= cut)
|
|
99
|
+
return m;
|
|
100
|
+
if (m.role !== 'tool' || !Array.isArray(m.content))
|
|
101
|
+
return m;
|
|
102
|
+
let messageChanged = false;
|
|
103
|
+
const adaptiveTarget = adaptiveStaleTarget(targetChars, rankByIndex.get(i) ?? 0, staleToolIndexes.length);
|
|
104
|
+
const adaptiveMin = adaptiveMinChars(adaptiveTarget, minChars);
|
|
105
|
+
const content = m.content.map((part) => {
|
|
106
|
+
if (part.type === 'tool-result' &&
|
|
107
|
+
part.output?.type === 'text' &&
|
|
108
|
+
typeof part.output.value === 'string') {
|
|
109
|
+
const compressed = selectiveCompressText(part.output.value, { targetChars: adaptiveTarget, minChars: adaptiveMin, query });
|
|
110
|
+
if (compressed.changed) {
|
|
111
|
+
changed = true;
|
|
112
|
+
messageChanged = true;
|
|
113
|
+
return { ...part, output: { ...part.output, value: compressed.text } };
|
|
31
114
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
115
|
+
}
|
|
116
|
+
return part;
|
|
117
|
+
});
|
|
118
|
+
return messageChanged ? { ...m, content } : m;
|
|
35
119
|
});
|
|
120
|
+
return changed ? out : messages;
|
|
36
121
|
}
|
|
37
122
|
/** ประมาณ token ของ conversation (chars/4) — ไม่เป๊ะแต่พอใช้ตัดสิน compact */
|
|
38
123
|
export function estimateTokens(messages) {
|