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.
- package/.env.example +19 -0
- package/CHANGELOG.md +173 -0
- package/README.md +153 -20
- package/README.th.md +136 -0
- package/dist/agentContext.js +4 -0
- package/dist/approval.js +6 -0
- package/dist/bin.js +405 -57
- package/dist/brain.js +92 -59
- package/dist/brand.js +47 -0
- package/dist/checkpoint.js +37 -0
- package/dist/commands.js +86 -6
- package/dist/compaction.js +76 -5
- package/dist/config.js +100 -12
- package/dist/cost.js +60 -3
- package/dist/doctor.js +92 -0
- package/dist/gateway/auth.js +2 -2
- package/dist/gateway/ledger.js +2 -2
- package/dist/gateway/scheduler.js +1 -0
- package/dist/gateway/serve.js +6 -4
- package/dist/gateway/server.js +10 -2
- package/dist/git.js +11 -2
- package/dist/hooks.js +43 -17
- package/dist/knowledge.js +48 -49
- package/dist/loop.js +182 -66
- package/dist/lsp/client.js +173 -0
- package/dist/lsp/framing.js +56 -0
- package/dist/lsp/index.js +138 -0
- package/dist/lsp/servers.js +82 -0
- package/dist/mcp-server.js +244 -0
- package/dist/mcp.js +184 -29
- package/dist/memory-store.js +559 -0
- package/dist/memory.js +143 -29
- package/dist/orchestrate.js +150 -0
- package/dist/providers/codex.js +21 -7
- package/dist/providers/keys.js +3 -2
- package/dist/providers/models.js +22 -6
- package/dist/providers/registry.js +155 -1
- package/dist/repomap.js +93 -0
- package/dist/search/chunk.js +158 -0
- package/dist/search/embed-store.js +187 -0
- package/dist/search/engine.js +203 -0
- package/dist/search/fuse.js +35 -0
- package/dist/search/index-core.js +187 -0
- package/dist/search/indexer.js +241 -0
- package/dist/search/store.js +77 -0
- package/dist/session.js +42 -8
- package/dist/skill-install.js +10 -10
- package/dist/skills.js +12 -9
- package/dist/summarize.js +31 -0
- package/dist/tools/bash.js +21 -2
- package/dist/tools/diagnostics.js +41 -0
- package/dist/tools/edit.js +29 -7
- package/dist/tools/index.js +8 -1
- package/dist/tools/list.js +7 -2
- package/dist/tools/permission.js +90 -9
- package/dist/tools/read.js +23 -4
- package/dist/tools/remember.js +1 -1
- package/dist/tools/sandbox.js +61 -0
- package/dist/tools/search.js +105 -4
- package/dist/tools/task.js +195 -29
- package/dist/tools/timeout.js +35 -0
- package/dist/tools/util.js +10 -0
- package/dist/tools/write.js +6 -4
- package/dist/trust.js +89 -0
- package/dist/ui/app.js +228 -31
- package/dist/ui/banner.js +4 -9
- package/dist/ui/brain-wizard.js +2 -2
- package/dist/ui/history.js +30 -0
- package/dist/ui/mentions.js +44 -0
- package/dist/ui/render.js +55 -15
- package/dist/ui/setup.js +97 -12
- package/dist/ui/useEditor.js +83 -0
- package/dist/update.js +114 -0
- package/dist/worktree.js +173 -0
- package/package.json +11 -5
- package/scripts/postinstall.mjs +33 -0
- package/second-brain/.agents/_Index.md +30 -0
- package/second-brain/.agents/skills/_Index.md +30 -0
- package/second-brain/.agents/workflows/_Index.md +30 -0
- package/second-brain/AGENTS.md +4 -4
- package/second-brain/Acceptance/_Index.md +30 -0
- package/second-brain/Acceptance/golden-case-template.md +39 -0
- package/second-brain/Areas/_Index.md +30 -0
- package/second-brain/Bugs/System-OS/_Index.md +30 -0
- package/second-brain/Bugs/_Index.md +30 -0
- package/second-brain/CLAUDE.md +4 -1
- package/second-brain/Checklists/_Index.md +30 -0
- package/second-brain/Checklists/preflight-postflight-template.md +29 -0
- package/second-brain/Distillations/_Index.md +30 -0
- package/second-brain/Entities/_Index.md +30 -0
- package/second-brain/Entities/entity-template.md +33 -0
- package/second-brain/Evals/_Index.md +30 -0
- package/second-brain/Evals/correction-pairs.md +24 -0
- package/second-brain/Evals/failure-taxonomy.md +24 -0
- package/second-brain/Evals/golden-set.md +25 -0
- package/second-brain/Evals/quality-ledger.md +23 -0
- package/second-brain/Evals/self-eval-rubric.md +23 -0
- package/second-brain/GEMINI.md +4 -4
- package/second-brain/Goals/_Index.md +30 -0
- package/second-brain/Handoffs/_Index.md +30 -0
- package/second-brain/Home.md +7 -0
- package/second-brain/Intake/Raw Sources/_Index.md +30 -0
- package/second-brain/Intake/_Index.md +30 -0
- package/second-brain/Intake/_Quarantine/_Index.md +30 -0
- package/second-brain/Learning/_Index.md +30 -0
- package/second-brain/Playbooks/_Index.md +30 -0
- package/second-brain/Playbooks/playbook-template.md +23 -0
- package/second-brain/Projects/_Index.md +30 -0
- package/second-brain/Prompts/_Index.md +30 -0
- package/second-brain/README.md +2 -1
- package/second-brain/Research/_Index.md +30 -0
- package/second-brain/Retrospectives/_Index.md +30 -0
- package/second-brain/Reviews/_Index.md +30 -0
- package/second-brain/Runbooks/_Index.md +30 -0
- package/second-brain/Runbooks/eval-loop.md +24 -0
- package/second-brain/Sessions/_Index.md +30 -0
- package/second-brain/Shared/AI-Context-Index.md +20 -0
- package/second-brain/Shared/AI-Threads/_Index.md +30 -0
- package/second-brain/Shared/Archive/_Index.md +30 -0
- package/second-brain/Shared/Assets/_Index.md +30 -0
- package/second-brain/Shared/Context-Packs/_Index.md +30 -0
- package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
- package/second-brain/Shared/Coordination/NOW.md +28 -0
- package/second-brain/Shared/Coordination/_Index.md +30 -0
- package/second-brain/Shared/Coordination/agent-registry.md +24 -0
- package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
- package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
- package/second-brain/Shared/Coordination/task-board.md +32 -0
- package/second-brain/Shared/Core-Facts/_Index.md +30 -0
- package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
- package/second-brain/Shared/Glossary/_Index.md +30 -0
- package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
- package/second-brain/Shared/Operating-State/_Index.md +30 -0
- package/second-brain/Shared/Prompting/_Index.md +30 -0
- package/second-brain/Shared/Provenance/_Index.md +30 -0
- package/second-brain/Shared/Rules/_Index.md +30 -0
- package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
- package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
- package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
- package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
- package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
- package/second-brain/Shared/Rules/rules-formatting.md +34 -0
- package/second-brain/Shared/Scripts/_Index.md +30 -0
- package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
- package/second-brain/Shared/User-Memory/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
- package/second-brain/Shared/Working-Memory/_Index.md +30 -0
- package/second-brain/Shared/_Index.md +30 -0
- package/second-brain/Shared/mcp-servers/_Index.md +30 -0
- package/second-brain/Skills/_Index.md +30 -0
- package/second-brain/Templates/_Index.md +30 -0
- package/second-brain/Templates/bug.md +2 -0
- package/second-brain/Templates/handoff.md +2 -0
- package/second-brain/Templates/session.md +2 -0
- package/second-brain/Tools/_Index.md +30 -0
- package/second-brain/Traces/_Index.md +30 -0
- package/second-brain/Vault Structure Map.md +33 -1
- package/second-brain/copilot/_Index.md +30 -0
- package/skills/audit-license-compliance/SKILL.md +117 -0
- package/skills/author-codemod/SKILL.md +110 -0
- package/skills/build-audit-logging/SKILL.md +112 -0
- package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
- package/skills/build-cli-tool/SKILL.md +108 -0
- package/skills/build-data-table/SKILL.md +141 -0
- package/skills/build-native-mobile-ui/SKILL.md +154 -0
- package/skills/build-offline-first-sync/SKILL.md +118 -0
- package/skills/build-realtime-channel/SKILL.md +122 -0
- package/skills/build-vector-search/SKILL.md +131 -0
- package/skills/compose-local-dev-stack/SKILL.md +149 -0
- package/skills/configure-bundler-build/SKILL.md +166 -0
- package/skills/configure-dns-tls/SKILL.md +142 -0
- package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
- package/skills/configure-security-headers-csp/SKILL.md +122 -0
- package/skills/contract-testing/SKILL.md +140 -0
- package/skills/datetime-timezone-correctness/SKILL.md +125 -0
- package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
- package/skills/debug-flaky-tests/SKILL.md +128 -0
- package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
- package/skills/deliver-webhooks/SKILL.md +116 -0
- package/skills/design-api-pagination/SKILL.md +144 -0
- package/skills/design-authorization-model/SKILL.md +119 -0
- package/skills/design-backup-dr-recovery/SKILL.md +113 -0
- package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
- package/skills/design-multi-tenancy/SKILL.md +100 -0
- package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
- package/skills/design-relational-schema/SKILL.md +129 -0
- package/skills/design-search-index-infra/SKILL.md +151 -0
- package/skills/design-state-machine/SKILL.md +108 -0
- package/skills/design-token-system/SKILL.md +109 -0
- package/skills/distributed-locks-leases/SKILL.md +120 -0
- package/skills/encrypt-sensitive-data/SKILL.md +148 -0
- package/skills/feature-flags-rollout/SKILL.md +130 -0
- package/skills/file-upload-object-storage/SKILL.md +107 -0
- package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
- package/skills/harden-llm-app-reliability/SKILL.md +126 -0
- package/skills/i18n-localization-setup/SKILL.md +113 -0
- package/skills/idempotency-keys/SKILL.md +107 -0
- package/skills/implement-push-notifications/SKILL.md +142 -0
- package/skills/ingest-webhook-secure/SKILL.md +120 -0
- package/skills/integrate-oauth-oidc/SKILL.md +126 -0
- package/skills/load-stress-test/SKILL.md +129 -0
- package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
- package/skills/model-nosql-data/SKILL.md +118 -0
- package/skills/money-decimal-arithmetic/SKILL.md +123 -0
- package/skills/monitor-ml-drift/SKILL.md +109 -0
- package/skills/numeric-precision-units/SKILL.md +144 -0
- package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
- package/skills/optimize-react-rerenders/SKILL.md +124 -0
- package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
- package/skills/payments-billing-integration/SKILL.md +114 -0
- package/skills/pin-toolchain-versions/SKILL.md +116 -0
- package/skills/plan-strangler-migration/SKILL.md +95 -0
- package/skills/property-based-testing/SKILL.md +108 -0
- package/skills/publish-package-registry/SKILL.md +130 -0
- package/skills/recover-git-state/SKILL.md +119 -0
- package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
- package/skills/resilience-timeouts-retries/SKILL.md +104 -0
- package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
- package/skills/rewrite-git-history/SKILL.md +109 -0
- package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
- package/skills/schema-evolution-compatibility/SKILL.md +121 -0
- package/skills/send-transactional-email/SKILL.md +126 -0
- package/skills/serve-deploy-ml-model/SKILL.md +107 -0
- package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
- package/skills/setup-devcontainer-env/SKILL.md +131 -0
- package/skills/setup-lint-format-precommit/SKILL.md +140 -0
- package/skills/setup-monorepo-tooling/SKILL.md +125 -0
- package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
- package/skills/structured-output-llm/SKILL.md +86 -0
- package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
- package/skills/test-data-factories/SKILL.md +158 -0
- package/skills/threat-model-stride/SKILL.md +123 -0
- package/skills/train-evaluate-ml-model/SKILL.md +109 -0
- package/skills/unicode-text-correctness/SKILL.md +109 -0
- 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
|
-
|
|
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(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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((
|
|
77
|
+
useInput((input, key) => {
|
|
31
78
|
// มี approval ค้าง → จับ y/n ก่อน (แม้ agent กำลังรัน/busy)
|
|
32
79
|
if (approvalReq) {
|
|
33
|
-
if (
|
|
80
|
+
if (input === 'y' || input === 'Y' || key.return) {
|
|
34
81
|
approvalResolve.current?.(true);
|
|
35
82
|
setApprovalReq(null);
|
|
36
83
|
}
|
|
37
|
-
else if (
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
|
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 —
|
|
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
|
-
|
|
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
|
}
|
package/dist/ui/brain-wizard.js
CHANGED
|
@@ -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: [
|
|
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: [
|
|
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
|
+
}
|