sanook-cli 0.5.2 → 0.5.7
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/CHANGELOG.md +112 -2
- package/README.md +15 -3
- package/README.th.md +8 -1
- package/dist/approval.js +7 -0
- package/dist/bin.js +637 -56
- package/dist/brain-consolidate.js +335 -0
- package/dist/brain-context.js +42 -3
- package/dist/brain-final.js +15 -9
- package/dist/brain-link.js +73 -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.js +3 -0
- package/dist/brand.js +4 -0
- package/dist/cli-args.js +47 -9
- package/dist/cli-option-values.js +1 -1
- package/dist/clipboard.js +65 -0
- package/dist/commands.js +98 -15
- package/dist/config.js +66 -34
- package/dist/context-pack.js +145 -0
- package/dist/cost.js +20 -0
- 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/gateway/auth.js +14 -3
- package/dist/gateway/deliver.js +45 -3
- package/dist/gateway/doctor.js +456 -0
- package/dist/gateway/email.js +30 -1
- package/dist/gateway/ledger.js +20 -1
- package/dist/gateway/session.js +34 -11
- 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 +24 -4
- package/dist/knowledge.js +55 -29
- package/dist/loop.js +65 -9
- package/dist/mcp-hub.js +33 -0
- package/dist/mcp-registry.js +153 -9
- package/dist/mcp-risk.js +71 -0
- package/dist/mcp.js +77 -5
- package/dist/memory-log.js +90 -0
- package/dist/memory-store.js +37 -1
- package/dist/memory.js +51 -7
- package/dist/model-picker.js +58 -0
- package/dist/orchestrate.js +7 -5
- 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 +20 -8
- package/dist/providers/keys.js +21 -0
- package/dist/providers/models.js +1 -1
- package/dist/providers/registry.js +11 -1
- package/dist/search/cli.js +9 -1
- package/dist/search/embedding-config.js +22 -0
- package/dist/search/engine.js +2 -13
- package/dist/search/indexer.js +10 -10
- package/dist/session-brain.js +103 -0
- package/dist/session-distill.js +84 -0
- package/dist/session.js +1 -11
- package/dist/skill-install.js +24 -1
- package/dist/skills.js +33 -0
- package/dist/slash-completion.js +155 -0
- package/dist/support-dump.js +31 -0
- package/dist/tool-catalog.js +59 -0
- package/dist/tools/index.js +5 -0
- package/dist/tools/permission.js +82 -16
- package/dist/tools/polyglot.js +126 -0
- package/dist/tools/sandbox.js +38 -13
- package/dist/tools/search.js +9 -2
- package/dist/tools/task.js +22 -2
- package/dist/tools/timeout.js +7 -5
- package/dist/tools/web-fetch-tool.js +33 -0
- package/dist/turn-retrieval.js +83 -0
- package/dist/ui/app.js +874 -35
- package/dist/ui/banner.js +78 -4
- package/dist/ui/markdown.js +122 -0
- package/dist/ui/overlay.js +496 -0
- package/dist/ui/queue.js +23 -0
- package/dist/ui/render.js +30 -2
- package/dist/ui/session-panel.js +115 -0
- package/dist/ui/setup-providers.js +40 -0
- package/dist/ui/setup.js +163 -50
- 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 +32 -6
- package/dist/usage-cli.js +160 -0
- package/dist/usage-ledger.js +169 -0
- package/dist/web-fetch.js +637 -0
- package/dist/web-surface.js +190 -0
- package/package.json +4 -3
- package/scripts/postinstall.mjs +4 -4
- package/second-brain/Projects/_Index.md +17 -4
- package/second-brain/Projects/sanook-cli/_Index.md +7 -3
- 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 +52 -11
- package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -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 +2 -0
- package/second-brain/Shared/Operating-State/current-state.md +14 -23
- package/second-brain/Shared/Tech-Standards/_Index.md +2 -0
- package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
- package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -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/dist/gateway/ledger.js
CHANGED
|
@@ -9,6 +9,8 @@ import { appHomePath } from '../brand.js';
|
|
|
9
9
|
const GATEWAY_DIR = appHomePath('gateway');
|
|
10
10
|
const TASKS_FILE = join(GATEWAY_DIR, 'tasks.json');
|
|
11
11
|
const LOCK_FILE = join(GATEWAY_DIR, 'tasks.lock');
|
|
12
|
+
const TASK_KINDS = new Set(['cron', 'message', 'once']);
|
|
13
|
+
const TASK_STATUSES = new Set(['queued', 'running', 'done', 'failed']);
|
|
12
14
|
function normalizeOptionalModel(model) {
|
|
13
15
|
const trimmed = model?.trim();
|
|
14
16
|
return trimmed ? trimmed : undefined;
|
|
@@ -17,11 +19,28 @@ function normalizeOptionalText(value) {
|
|
|
17
19
|
const trimmed = value?.trim();
|
|
18
20
|
return trimmed ? trimmed : undefined;
|
|
19
21
|
}
|
|
22
|
+
function isTask(value) {
|
|
23
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
24
|
+
return false;
|
|
25
|
+
const task = value;
|
|
26
|
+
return (typeof task.id === 'string' &&
|
|
27
|
+
TASK_KINDS.has(task.kind) &&
|
|
28
|
+
TASK_STATUSES.has(task.status) &&
|
|
29
|
+
typeof task.spec === 'string' &&
|
|
30
|
+
Number.isFinite(task.runAt) &&
|
|
31
|
+
Number.isFinite(task.createdAt) &&
|
|
32
|
+
(task.schedule === undefined || typeof task.schedule === 'string') &&
|
|
33
|
+
(task.model === undefined || typeof task.model === 'string') &&
|
|
34
|
+
(task.deliver === undefined || typeof task.deliver === 'string') &&
|
|
35
|
+
(task.lastRun === undefined || Number.isFinite(task.lastRun)) &&
|
|
36
|
+
(task.lastResult === undefined || typeof task.lastResult === 'string') &&
|
|
37
|
+
(task.lastError === undefined || typeof task.lastError === 'string'));
|
|
38
|
+
}
|
|
20
39
|
// ── low-level: read ตรงจากไฟล์ทุกครั้ง (ไม่ cache snapshot → ไม่มี stale-overwrite) ──
|
|
21
40
|
async function readTasks() {
|
|
22
41
|
try {
|
|
23
42
|
const parsed = JSON.parse(await readFile(TASKS_FILE, 'utf8'));
|
|
24
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
43
|
+
return Array.isArray(parsed) ? parsed.filter(isTask) : [];
|
|
25
44
|
}
|
|
26
45
|
catch {
|
|
27
46
|
return []; // ไม่มีไฟล์/พัง → empty (write แบบ atomic จึงไม่ทำลายของเดิม)
|
package/dist/gateway/session.js
CHANGED
|
@@ -3,7 +3,7 @@ import { chmod, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { appHomePath, persistenceEnabled } from '../brand.js';
|
|
5
5
|
import { runAgent } from '../loop.js';
|
|
6
|
-
import { redactKey } from '../providers/keys.js';
|
|
6
|
+
import { redactKey, redactUnknown } from '../providers/keys.js';
|
|
7
7
|
import { canonicalSpec, parseSpec, PROVIDERS } from '../providers/registry.js';
|
|
8
8
|
import { autoCompact, estimateTokens } from '../compaction.js';
|
|
9
9
|
import { patchGlobalConfig } from '../config.js';
|
|
@@ -25,15 +25,32 @@ function sessionPath(id) {
|
|
|
25
25
|
}
|
|
26
26
|
return join(SESSION_DIR, `${id}.json`);
|
|
27
27
|
}
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
function isRecord(value) {
|
|
29
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
function isModelMessage(value) {
|
|
32
|
+
if (!isRecord(value))
|
|
33
|
+
return false;
|
|
34
|
+
if (value.role === 'system')
|
|
35
|
+
return typeof value.content === 'string';
|
|
36
|
+
if (value.role === 'tool')
|
|
37
|
+
return Array.isArray(value.content);
|
|
38
|
+
if (value.role === 'user' || value.role === 'assistant') {
|
|
39
|
+
return typeof value.content === 'string' || Array.isArray(value.content);
|
|
35
40
|
}
|
|
36
|
-
return
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function isGatewaySession(value) {
|
|
44
|
+
if (!isRecord(value))
|
|
45
|
+
return false;
|
|
46
|
+
return (typeof value.id === 'string' &&
|
|
47
|
+
typeof value.platform === 'string' &&
|
|
48
|
+
typeof value.target === 'string' &&
|
|
49
|
+
typeof value.created === 'string' &&
|
|
50
|
+
typeof value.updated === 'string' &&
|
|
51
|
+
typeof value.model === 'string' &&
|
|
52
|
+
Array.isArray(value.messages) &&
|
|
53
|
+
value.messages.every(isModelMessage));
|
|
37
54
|
}
|
|
38
55
|
export function shouldSuppressDelivery(text) {
|
|
39
56
|
const normalized = text.trim().toUpperCase().replace(/[\s_-]+/g, ' ');
|
|
@@ -42,7 +59,8 @@ export function shouldSuppressDelivery(text) {
|
|
|
42
59
|
export async function loadGatewaySession(platform, target) {
|
|
43
60
|
try {
|
|
44
61
|
const id = gatewaySessionId(platform, target);
|
|
45
|
-
|
|
62
|
+
const parsed = JSON.parse(await readFile(sessionPath(id), 'utf8'));
|
|
63
|
+
return isGatewaySession(parsed) && parsed.id === id ? parsed : null;
|
|
46
64
|
}
|
|
47
65
|
catch {
|
|
48
66
|
return null;
|
|
@@ -53,7 +71,8 @@ export async function listGatewaySessions() {
|
|
|
53
71
|
const files = (await readdir(SESSION_DIR)).filter((f) => f.endsWith('.json'));
|
|
54
72
|
const sessions = await Promise.all(files.map(async (file) => {
|
|
55
73
|
try {
|
|
56
|
-
|
|
74
|
+
const parsed = JSON.parse(await readFile(join(SESSION_DIR, file), 'utf8'));
|
|
75
|
+
return isGatewaySession(parsed) ? parsed : null;
|
|
57
76
|
}
|
|
58
77
|
catch {
|
|
59
78
|
return null;
|
|
@@ -153,6 +172,10 @@ async function runAndSaveGatewayTurn(opts, existing, prompt, history, model) {
|
|
|
153
172
|
maxSteps: opts.maxSteps ?? 20,
|
|
154
173
|
budgetUsd: opts.budgetUsd,
|
|
155
174
|
permissionMode: opts.permissionMode ?? 'ask',
|
|
175
|
+
usageMeta: {
|
|
176
|
+
sessionId: `${opts.platform}:${opts.target}`,
|
|
177
|
+
source: 'gateway',
|
|
178
|
+
},
|
|
156
179
|
});
|
|
157
180
|
await saveGatewayState(opts, existing, model, messages);
|
|
158
181
|
return { text, messages, suppressDelivery: shouldSuppressDelivery(text) };
|
package/dist/hotkeys.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const HOTKEYS = [
|
|
2
|
+
['Ctrl+C', 'clear draft / interrupt running turn / exit when input is empty'],
|
|
3
|
+
['Esc', 'stop running turn and clear queued prompts'],
|
|
4
|
+
['↑/↓', 'prompt history'],
|
|
5
|
+
['Ctrl+A/E', 'start / end of line'],
|
|
6
|
+
['Ctrl+U/K', 'delete to start / end'],
|
|
7
|
+
['Ctrl+W', 'delete previous word'],
|
|
8
|
+
['Alt+Enter', 'insert newline'],
|
|
9
|
+
['\\+Enter', 'multi-line continuation fallback'],
|
|
10
|
+
['paste 5+ lines', 'collapse into a readable token, expand before submit'],
|
|
11
|
+
['type while busy + Enter', 'queue the next prompt'],
|
|
12
|
+
['busy ↑/↓ + Ctrl+X', 'select and delete queued prompts'],
|
|
13
|
+
['Ctrl+T', 'toggle tool trail compact / expanded'],
|
|
14
|
+
['@file', 'inline a file or attach an image'],
|
|
15
|
+
['/model <spec>', 'switch model'],
|
|
16
|
+
['/diff /undo /rewind', 'inspect, stash, or rewind file changes'],
|
|
17
|
+
];
|
|
18
|
+
export function formatHotkeys() {
|
|
19
|
+
const width = HOTKEYS.reduce((max, [key]) => Math.max(max, key.length), 0);
|
|
20
|
+
return ['hotkeys:', ...HOTKEYS.map(([key, help]) => ` ${key.padEnd(width)} ${help}`)].join('\n');
|
|
21
|
+
}
|
package/dist/i18n/en.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export const en = {
|
|
2
|
+
setup: {
|
|
3
|
+
title: 'Set up Sanook AI CLI (first run)',
|
|
4
|
+
stepLanguage: '1. Choose language',
|
|
5
|
+
stepWelcome: '2. Welcome',
|
|
6
|
+
stepProvider: '3. Choose AI provider',
|
|
7
|
+
stepCodex: '4. Connect OpenAI Codex',
|
|
8
|
+
stepKey: '4. Paste API key',
|
|
9
|
+
stepModel: '5. Choose default model',
|
|
10
|
+
stepAgent: '6. Agent settings',
|
|
11
|
+
stepTools: '7. Tools & MCP',
|
|
12
|
+
stepGateway: '8. Messaging gateway',
|
|
13
|
+
stepBrain: '9. Second brain workspace',
|
|
14
|
+
stepComplete: '10. Ready',
|
|
15
|
+
languageHint: 'You can change this later with sanook config set locale en|th',
|
|
16
|
+
languageEn: 'English',
|
|
17
|
+
languageTh: 'Thai (ภาษาไทย)',
|
|
18
|
+
welcomeBody: 'Sanook is a terminal AI agent with MCP, gateway, and an optional Obsidian second brain.\nThis wizard walks you through provider, model, and vault setup step by step.',
|
|
19
|
+
welcomeContinue: 'Continue setup',
|
|
20
|
+
providerHint: '↑↓ select · Enter confirm · ★ Codex = ChatGPT plan (no API key)',
|
|
21
|
+
providerMenuHint: 'cloud = API key · ★ Codex = ChatGPT plan · local = free on device',
|
|
22
|
+
codexTitle: 'Connect OpenAI Codex (ChatGPT plan quota — no API key)',
|
|
23
|
+
codexChecking: 'Checking codex CLI + login state…',
|
|
24
|
+
codexNeedInstall: 'Codex CLI not installed yet',
|
|
25
|
+
codexNeedLogin: 'Codex CLI installed but ChatGPT not signed in',
|
|
26
|
+
codexLoggedInNeedCli: 'Signed in — install codex CLI to run the agent',
|
|
27
|
+
codexReady: 'ChatGPT signed in — continuing…',
|
|
28
|
+
codexDeviceTitle: 'Sign in with device code (Hermes-style)',
|
|
29
|
+
codexDeviceOpen: '1. Open in browser:',
|
|
30
|
+
codexDeviceEnter: '2. Enter this code:',
|
|
31
|
+
codexDeviceWaiting: '3. Waiting for sign-in…',
|
|
32
|
+
codexDeviceRetry: 'Try device code again',
|
|
33
|
+
codexDeviceBack: '← Back to other login options',
|
|
34
|
+
codexOptionDevice: 'Login with device code (recommended)',
|
|
35
|
+
codexOptionCliLogin: 'Use codex login in another terminal',
|
|
36
|
+
codexOptionRecheck: 'Re-check (after install/login)',
|
|
37
|
+
codexOptionBack: '← Choose another provider',
|
|
38
|
+
codexInstallCmd: 'npm i -g @openai/codex',
|
|
39
|
+
keyEscHint: '(Esc = back)',
|
|
40
|
+
keyOpenAiCodexHint: 'Have ChatGPT Plus/Pro? Press Esc and pick OpenAI Codex (ChatGPT plan) — no API key needed.',
|
|
41
|
+
keyFormatHint: 'Key format',
|
|
42
|
+
keyStorageHint: 'Direct console API key only — no OAuth tokens · stored at ~/.sanook/auth.json mode 0600',
|
|
43
|
+
keyEmptyError: 'Paste an API key first (Enter on empty is blocked) · Esc = back to providers',
|
|
44
|
+
modelLoading: 'Fetching models from',
|
|
45
|
+
modelPick: 'Pick a default model',
|
|
46
|
+
brainQuestion: 'Create a second-brain workspace (Obsidian) for durable AI memory?',
|
|
47
|
+
brainYes: 'Yes — a few questions (name + path)',
|
|
48
|
+
brainNo: 'Skip for now (run sanook brain init later)',
|
|
49
|
+
completeTitle: 'Setup complete',
|
|
50
|
+
completeBody: 'Your CLI is ready. Open the dashboard for config and sessions, or start chatting in the terminal.',
|
|
51
|
+
completeDashboard: 'Open Sanook Dashboard',
|
|
52
|
+
completeRepl: 'Start terminal REPL',
|
|
53
|
+
continueLabel: 'Continue',
|
|
54
|
+
backLabel: 'Back',
|
|
55
|
+
recheckLabel: 'Re-check',
|
|
56
|
+
agentTitle: 'How should Sanook handle write/bash tools?',
|
|
57
|
+
agentAsk: 'Ask before risky actions (recommended)',
|
|
58
|
+
agentAuto: 'Act first (auto mode — faster, less safe)',
|
|
59
|
+
agentHint: 'Change anytime: sanook config set permissionMode ask|auto',
|
|
60
|
+
toolsTitle: 'Built-in tools + MCP',
|
|
61
|
+
toolsBody: 'Sanook ships git, bash, MCP, web search hooks, and skills. MCP servers live in ~/.sanook/mcp.json.',
|
|
62
|
+
toolsMcpHint: 'Browse MCP: /mcp in REPL · sanook mcp search <query>',
|
|
63
|
+
toolsWebSkip: 'Continue (configure web search later)',
|
|
64
|
+
toolsWebLater: 'Note: run sanook web setup tavily for web search',
|
|
65
|
+
gatewayTitle: 'Connect messaging platforms (optional)',
|
|
66
|
+
gatewayBody: 'Run sanook serve for 24/7 gateway. Pick a platform to configure next, or skip.',
|
|
67
|
+
gatewaySkip: 'Skip gateway for now',
|
|
68
|
+
gatewayTelegram: 'Telegram — sanook gateway setup telegram',
|
|
69
|
+
gatewayDiscord: 'Discord — sanook gateway setup discord',
|
|
70
|
+
gatewaySlack: 'Slack — sanook gateway setup slack',
|
|
71
|
+
gatewayDashboard: 'Configure in Sanook Dashboard → Channels',
|
|
72
|
+
},
|
|
73
|
+
dashboard: {
|
|
74
|
+
productName: 'Sanook Dashboard',
|
|
75
|
+
tagline: 'Configure models, sessions, MCP, gateway, and your second brain',
|
|
76
|
+
nav: {
|
|
77
|
+
home: 'Home',
|
|
78
|
+
chat: 'Chat',
|
|
79
|
+
models: 'Models',
|
|
80
|
+
sessions: 'Sessions',
|
|
81
|
+
files: 'Files',
|
|
82
|
+
logs: 'Logs',
|
|
83
|
+
cron: 'Cron',
|
|
84
|
+
channels: 'Channels',
|
|
85
|
+
config: 'Config',
|
|
86
|
+
mcp: 'MCP',
|
|
87
|
+
brain: 'Brain',
|
|
88
|
+
},
|
|
89
|
+
home: {
|
|
90
|
+
title: 'System status',
|
|
91
|
+
cliVersion: 'CLI version',
|
|
92
|
+
model: 'Default model',
|
|
93
|
+
brainPath: 'Second brain',
|
|
94
|
+
gateway: 'Gateway',
|
|
95
|
+
openRepl: 'Run sanook in your terminal to chat',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { en } from './en.js';
|
|
2
|
+
import { th } from './th.js';
|
|
3
|
+
const CATALOGS = { en, th };
|
|
4
|
+
export const SUPPORTED_LOCALES = ['en', 'th'];
|
|
5
|
+
export function normalizeLocale(raw) {
|
|
6
|
+
const v = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
|
|
7
|
+
if (v === 'en' || v.startsWith('en-'))
|
|
8
|
+
return 'en';
|
|
9
|
+
if (v === 'th' || v.startsWith('th-'))
|
|
10
|
+
return 'th';
|
|
11
|
+
return 'th';
|
|
12
|
+
}
|
|
13
|
+
export function getLocaleCatalog(locale) {
|
|
14
|
+
return CATALOGS[locale] ?? CATALOGS.th;
|
|
15
|
+
}
|
|
16
|
+
export function detectDefaultLocale() {
|
|
17
|
+
const lang = process.env.LANG ?? process.env.LC_ALL ?? process.env.LC_MESSAGES ?? '';
|
|
18
|
+
return lang.toLowerCase().includes('th') ? 'th' : 'en';
|
|
19
|
+
}
|
package/dist/i18n/th.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export const th = {
|
|
2
|
+
setup: {
|
|
3
|
+
title: 'ตั้งค่า Sanook AI CLI (ครั้งแรก)',
|
|
4
|
+
stepLanguage: '1. เลือกภาษา',
|
|
5
|
+
stepWelcome: '2. ยินดีต้อนรับ',
|
|
6
|
+
stepProvider: '3. เลือก AI provider',
|
|
7
|
+
stepCodex: '4. เชื่อม OpenAI Codex',
|
|
8
|
+
stepKey: '4. วาง API key',
|
|
9
|
+
stepModel: '5. เลือก model เริ่มต้น',
|
|
10
|
+
stepAgent: '6. ตั้งค่า agent',
|
|
11
|
+
stepTools: '7. Tools & MCP',
|
|
12
|
+
stepGateway: '8. Messaging gateway',
|
|
13
|
+
stepBrain: '9. second brain workspace',
|
|
14
|
+
stepComplete: '10. พร้อมใช้งาน',
|
|
15
|
+
languageHint: 'เปลี่ยนภาษาทีหลังได้: sanook config set locale en|th',
|
|
16
|
+
languageEn: 'English (ภาษาอังกฤษ)',
|
|
17
|
+
languageTh: 'ภาษาไทย',
|
|
18
|
+
welcomeBody: 'Sanook คือ AI agent บน terminal พร้อม MCP, gateway และ second brain (Obsidian) แบบเลือกได้\nwizard นี้จะพาตั้งค่า provider → model → vault ทีละขั้น',
|
|
19
|
+
welcomeContinue: 'เริ่มตั้งค่า',
|
|
20
|
+
providerHint: '↑↓ เลือก · Enter ยืนยัน · ★ Codex = ChatGPT plan (ไม่ต้อง API key)',
|
|
21
|
+
providerMenuHint: 'cloud = ใส่ API key · ★ Codex = ChatGPT plan · local = ฟรีบนเครื่อง',
|
|
22
|
+
codexTitle: 'เชื่อม OpenAI Codex (ใช้โควต้า ChatGPT plan — ไม่ต้องมี API key)',
|
|
23
|
+
codexChecking: 'กำลังเช็ก codex CLI + สถานะ login…',
|
|
24
|
+
codexNeedInstall: 'ยังไม่ได้ติดตั้ง codex CLI',
|
|
25
|
+
codexNeedLogin: 'ติดตั้ง codex CLI แล้ว แต่ยังไม่ได้ login ChatGPT',
|
|
26
|
+
codexLoggedInNeedCli: 'login แล้ว — ติดตั้ง codex CLI ก่อนรัน agent',
|
|
27
|
+
codexReady: 'login ChatGPT แล้ว — กำลังไปต่อ…',
|
|
28
|
+
codexDeviceTitle: 'Login ด้วย device code (แบบ Hermes)',
|
|
29
|
+
codexDeviceOpen: '1. เปิดใน browser:',
|
|
30
|
+
codexDeviceEnter: '2. ใส่รหัสนี้:',
|
|
31
|
+
codexDeviceWaiting: '3. รอ sign-in…',
|
|
32
|
+
codexDeviceRetry: 'ลอง device code ใหม่',
|
|
33
|
+
codexDeviceBack: '← กลับไปเลือกวิธี login อื่น',
|
|
34
|
+
codexOptionDevice: 'Login ด้วย device code (แนะนำ)',
|
|
35
|
+
codexOptionCliLogin: 'ใช้ codex login ใน terminal อีกหน้าต่าง',
|
|
36
|
+
codexOptionRecheck: 'เช็กใหม่ (หลังติดตั้ง/login)',
|
|
37
|
+
codexOptionBack: '← กลับไปเลือก provider อื่น',
|
|
38
|
+
codexInstallCmd: 'npm i -g @openai/codex',
|
|
39
|
+
keyEscHint: '(Esc = กลับ)',
|
|
40
|
+
keyOpenAiCodexHint: 'มี ChatGPT Plus/Pro? กด Esc แล้วเลือก OpenAI Codex (ChatGPT plan) — ไม่ต้อง API key',
|
|
41
|
+
keyFormatHint: 'รูปแบบ key',
|
|
42
|
+
keyStorageHint: 'API key ตรงจาก console — ห้าม OAuth/subscription token · เก็บที่ ~/.sanook/auth.json สิทธิ์ 0600',
|
|
43
|
+
keyEmptyError: 'วาง API key ก่อนค่ะ · Esc = กลับไปเลือก provider',
|
|
44
|
+
modelLoading: 'กำลังดึงรายชื่อ model จาก',
|
|
45
|
+
modelPick: 'เลือก model เริ่มต้น',
|
|
46
|
+
brainQuestion: 'สร้าง second-brain workspace (Obsidian) สำหรับความจำ AI ข้าม session?',
|
|
47
|
+
brainYes: 'สร้างเลย — ตอบไม่กี่ข้อ (ชื่อ + ที่เก็บ)',
|
|
48
|
+
brainNo: 'ข้ามไปก่อน (สั่ง sanook brain init ทีหลังได้)',
|
|
49
|
+
completeTitle: 'ตั้งค่าเสร็จแล้ว',
|
|
50
|
+
completeBody: 'CLI พร้อมใช้แล้ว เปิด Dashboard จัดการ config/sessions หรือเริ่มแชทใน terminal',
|
|
51
|
+
completeDashboard: 'เปิด Sanook Dashboard',
|
|
52
|
+
completeRepl: 'เริ่ม REPL ใน terminal',
|
|
53
|
+
continueLabel: 'ต่อไป',
|
|
54
|
+
backLabel: 'กลับ',
|
|
55
|
+
recheckLabel: 'เช็กใหม่',
|
|
56
|
+
agentTitle: 'Sanook ควรจัดการ write/bash tools อย่างไร?',
|
|
57
|
+
agentAsk: 'ถามก่อนทำ action เสี่ยง (แนะนำ)',
|
|
58
|
+
agentAuto: 'ทำเลย (auto mode — เร็วกว่า แต่เสี่ยงกว่า)',
|
|
59
|
+
agentHint: 'เปลี่ยนทีหลังได้: sanook config set permissionMode ask|auto',
|
|
60
|
+
toolsTitle: 'Tools ในตัว + MCP',
|
|
61
|
+
toolsBody: 'Sanook มี git, bash, MCP, web search, skills · MCP อยู่ที่ ~/.sanook/mcp.json',
|
|
62
|
+
toolsMcpHint: 'ดู MCP: /mcp ใน REPL · sanook mcp search <query>',
|
|
63
|
+
toolsWebSkip: 'ต่อไป (ตั้ง web search ทีหลัง)',
|
|
64
|
+
toolsWebLater: 'หมายเหตุ: รัน sanook web setup tavily สำหรับ web search',
|
|
65
|
+
gatewayTitle: 'เชื่อม messaging platforms (ไม่บังคับ)',
|
|
66
|
+
gatewayBody: 'รัน sanook serve สำหรับ gateway 24/7 · เลือก platform หรือข้าม',
|
|
67
|
+
gatewaySkip: 'ข้าม gateway ไปก่อน',
|
|
68
|
+
gatewayTelegram: 'Telegram — sanook gateway setup telegram',
|
|
69
|
+
gatewayDiscord: 'Discord — sanook gateway setup discord',
|
|
70
|
+
gatewaySlack: 'Slack — sanook gateway setup slack',
|
|
71
|
+
gatewayDashboard: 'ตั้งใน Sanook Dashboard → Channels',
|
|
72
|
+
},
|
|
73
|
+
dashboard: {
|
|
74
|
+
productName: 'Sanook Dashboard',
|
|
75
|
+
tagline: 'จัดการ model, session, MCP, gateway และ second brain',
|
|
76
|
+
nav: {
|
|
77
|
+
home: 'หน้าแรก',
|
|
78
|
+
chat: 'Chat',
|
|
79
|
+
models: 'Models',
|
|
80
|
+
sessions: 'Sessions',
|
|
81
|
+
files: 'Files',
|
|
82
|
+
logs: 'Logs',
|
|
83
|
+
cron: 'Cron',
|
|
84
|
+
channels: 'Channels',
|
|
85
|
+
config: 'Config',
|
|
86
|
+
mcp: 'MCP',
|
|
87
|
+
brain: 'Brain',
|
|
88
|
+
},
|
|
89
|
+
home: {
|
|
90
|
+
title: 'สถานะระบบ',
|
|
91
|
+
cliVersion: 'เวอร์ชัน CLI',
|
|
92
|
+
model: 'Model หลัก',
|
|
93
|
+
brainPath: 'Second brain',
|
|
94
|
+
gateway: 'Gateway',
|
|
95
|
+
openRepl: 'รัน sanook ใน terminal เพื่อแชท',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/insights-args.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inlineValue, takeValue } from './cli-option-values.js';
|
|
1
2
|
function parsePositiveInteger(raw) {
|
|
2
3
|
if (!raw || !/^[1-9]\d*$/.test(raw))
|
|
3
4
|
return null;
|
|
@@ -28,8 +29,27 @@ export function parseInsightsDays(args) {
|
|
|
28
29
|
}
|
|
29
30
|
export function parseInsightsArgs(args) {
|
|
30
31
|
const parts = typeof args === 'string' ? args.trim().split(/\s+/).filter(Boolean) : [...args];
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
let days = 30;
|
|
33
|
+
let all = false;
|
|
34
|
+
let sawDays = false;
|
|
35
|
+
for (let i = 0; i < parts.length; i++) {
|
|
36
|
+
const arg = parts[i];
|
|
37
|
+
if (arg === '--all' || arg === '-a') {
|
|
38
|
+
all = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const inlineDays = inlineValue('--days', arg) ?? inlineValue('-d', arg);
|
|
42
|
+
const next = arg === '--days' || arg === '-d' ? takeValue(parts, i) : undefined;
|
|
43
|
+
const raw = next ? next.value : inlineDays ?? arg;
|
|
44
|
+
if (sawDays)
|
|
45
|
+
return null;
|
|
46
|
+
const parsed = parsePositiveInteger(raw);
|
|
47
|
+
if (parsed === null)
|
|
48
|
+
return null;
|
|
49
|
+
days = parsed;
|
|
50
|
+
sawDays = true;
|
|
51
|
+
if (next)
|
|
52
|
+
i = next.nextIndex;
|
|
53
|
+
}
|
|
54
|
+
return { days, all };
|
|
35
55
|
}
|
package/dist/knowledge.js
CHANGED
|
@@ -2,8 +2,8 @@ import { loadStore, activeFacts } from './memory-store.js';
|
|
|
2
2
|
import { loadSkills } from './skills.js';
|
|
3
3
|
import { loadIndex } from './search/store.js';
|
|
4
4
|
import { foldFacts, foldSessions, foldSkills, loadRecentSessions } from './search/indexer.js';
|
|
5
|
-
import { rankSearch } from './search/engine.js';
|
|
6
|
-
import { termList } from './search/index-core.js';
|
|
5
|
+
import { rankSearch, search } from './search/engine.js';
|
|
6
|
+
import { termList, SEARCH_SOURCES } from './search/index-core.js';
|
|
7
7
|
// recall = ค้น knowledge ที่สะสม (auto-memory + vault + skills + session เก่า) แบบ BM25
|
|
8
8
|
// เดิมเป็น substring term-count (ไม่มี ranking/IDF) → อัปเกรดเป็น real BM25 inverted index
|
|
9
9
|
// (src/search/) ที่ rank ข้าม corpus เดียวกัน + ตัด snippet ให้
|
|
@@ -20,7 +20,7 @@ export function scoreText(text, terms) {
|
|
|
20
20
|
return terms.reduce((s, t) => s + (l.includes(t) ? 1 : 0), 0);
|
|
21
21
|
}
|
|
22
22
|
/** label สั้นต่อ hit (memory ไม่มี title → ใช้ snippet; vault มี path ต่อท้าย) */
|
|
23
|
-
function formatHit(h) {
|
|
23
|
+
export function formatHit(h) {
|
|
24
24
|
const title = h.title.trim();
|
|
25
25
|
const snippet = h.snippet.trim();
|
|
26
26
|
const head = title ? [title, snippet].filter(Boolean).join(' — ') : snippet;
|
|
@@ -31,37 +31,63 @@ function formatHit(h) {
|
|
|
31
31
|
* ค้น knowledge ข้าม memory + vault + skills + sessions ด้วย BM25 (ranked + snippet).
|
|
32
32
|
* คืน plain-text สำหรับ agent อ่าน (สัญญาเดิม) — ใช้โดย recall tool.
|
|
33
33
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Ranked hits over memory + vault + skills + sessions (BM25, deterministic, no network).
|
|
36
|
+
* Loads the persisted index then folds LIVE corpora so a just-remembered fact is found
|
|
37
|
+
* immediately without a reindex. Shared by the recall tool and per-turn auto-retrieval.
|
|
38
|
+
*/
|
|
39
|
+
export async function recallHits(query, limit = 8, sources) {
|
|
38
40
|
const now = Date.now();
|
|
41
|
+
const want = sources ? new Set(sources) : undefined; // undefined = all sources
|
|
39
42
|
const { index } = await loadIndex(); // persisted (vault chunks); empty ok
|
|
40
|
-
// fold live corpora สด — memory/session/skill ล่าสุด (ไม่แตะไฟล์ persisted)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
// fold live corpora สด — memory/session/skill ล่าสุด (ไม่แตะไฟล์ persisted). Skip a corpus when
|
|
44
|
+
// it's filtered out (saves folding 100+ skills when callers only want project sources).
|
|
45
|
+
if (!want || want.has('memory')) {
|
|
46
|
+
try {
|
|
47
|
+
foldFacts(index, activeFacts(await loadStore(now)), now);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* ยังไม่มี memory */
|
|
51
|
+
}
|
|
49
52
|
}
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
if (!want || want.has('session')) {
|
|
54
|
+
try {
|
|
55
|
+
foldSessions(index, await loadRecentSessions());
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* ยังไม่มี session */
|
|
59
|
+
}
|
|
52
60
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
if (!want || want.has('skill')) {
|
|
62
|
+
try {
|
|
63
|
+
foldSkills(index, (await loadSkills()).map((s) => ({
|
|
64
|
+
id: `skill:${s.name}`,
|
|
65
|
+
name: s.name,
|
|
66
|
+
text: `${s.description} ${s.whenToUse ?? ''}`.trim(),
|
|
67
|
+
})));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* ยังไม่มี skill */
|
|
71
|
+
}
|
|
59
72
|
}
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
return rankSearch(index, query, { mode: 'fts', limit, sources }).hits;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Hybrid (semantic + BM25) recall over the persisted index + embeddings — the "lever" identified by
|
|
77
|
+
* experiment H5 for paraphrase/synonym queries. Degrades to BM25 automatically when no embedder /
|
|
78
|
+
* vectors are configured (never throws). Covers INDEXED content (run `sanook index`); just-remembered
|
|
79
|
+
* facts still surface via the default BM25 recallHits path. Opt-in per-turn (network/latency cost).
|
|
80
|
+
*/
|
|
81
|
+
export async function semanticRecallHits(query, limit = 8, sources) {
|
|
82
|
+
const res = await search(query, { mode: 'hybrid', limit, sources: sources ?? [...SEARCH_SOURCES] });
|
|
83
|
+
return res.hits;
|
|
84
|
+
}
|
|
85
|
+
export async function recall(query, limit = 8) {
|
|
86
|
+
if (termList(query).length === 0) {
|
|
87
|
+
return 'query สั้นเกินไป — ใส่คำค้นยาวขึ้น';
|
|
62
88
|
}
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
89
|
+
const hits = await recallHits(query, limit);
|
|
90
|
+
if (!hits.length)
|
|
65
91
|
return `ไม่เจอความรู้เกี่ยวกับ "${query}" ใน memory/vault/skills/sessions`;
|
|
66
|
-
return
|
|
92
|
+
return hits.map(formatHit).join('\n');
|
|
67
93
|
}
|