sanook-cli 0.4.0
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 +23 -0
- package/CHANGELOG.md +38 -0
- package/LICENSE +201 -0
- package/README.md +239 -0
- package/dist/agentContext.js +2 -0
- package/dist/approval.js +78 -0
- package/dist/bin.js +461 -0
- package/dist/brain.js +186 -0
- package/dist/commands.js +66 -0
- package/dist/compaction.js +85 -0
- package/dist/config.js +101 -0
- package/dist/cost.js +59 -0
- package/dist/diff.js +36 -0
- package/dist/gateway/auth.js +32 -0
- package/dist/gateway/ledger.js +94 -0
- package/dist/gateway/lock.js +114 -0
- package/dist/gateway/schedule.js +74 -0
- package/dist/gateway/scheduler.js +87 -0
- package/dist/gateway/serve.js +57 -0
- package/dist/gateway/server.js +94 -0
- package/dist/gateway/telegram.js +115 -0
- package/dist/git.js +55 -0
- package/dist/hooks.js +104 -0
- package/dist/knowledge.js +68 -0
- package/dist/loop.js +169 -0
- package/dist/mcp.js +191 -0
- package/dist/memory.js +108 -0
- package/dist/providers/codex.js +86 -0
- package/dist/providers/keys.js +37 -0
- package/dist/providers/models.js +55 -0
- package/dist/providers/registry.js +241 -0
- package/dist/session.js +36 -0
- package/dist/skill-install.js +190 -0
- package/dist/skills.js +111 -0
- package/dist/tools/bash.js +26 -0
- package/dist/tools/edit.js +107 -0
- package/dist/tools/git.js +68 -0
- package/dist/tools/index.js +36 -0
- package/dist/tools/list.js +24 -0
- package/dist/tools/permission.js +30 -0
- package/dist/tools/read.js +18 -0
- package/dist/tools/recall.js +12 -0
- package/dist/tools/remember.js +14 -0
- package/dist/tools/schedule.js +61 -0
- package/dist/tools/search.js +54 -0
- package/dist/tools/skill.js +65 -0
- package/dist/tools/task.js +46 -0
- package/dist/tools/util.js +5 -0
- package/dist/tools/write.js +27 -0
- package/dist/ui/app.js +132 -0
- package/dist/ui/banner.js +20 -0
- package/dist/ui/brain-wizard.js +29 -0
- package/dist/ui/render.js +57 -0
- package/dist/ui/setup.js +46 -0
- package/package.json +77 -0
- package/second-brain/AGENTS.md +18 -0
- package/second-brain/CLAUDE.md +96 -0
- package/second-brain/Evals/retrieval-eval.md +30 -0
- package/second-brain/GEMINI.md +15 -0
- package/second-brain/Home.md +33 -0
- package/second-brain/README.md +29 -0
- package/second-brain/Runbooks/ingest-quarantine.md +27 -0
- package/second-brain/Runbooks/sleep-time-consolidation.md +26 -0
- package/second-brain/Shared/AI-Context-Index.md +52 -0
- package/second-brain/Shared/Core-Facts/protected-facts.md +21 -0
- package/second-brain/Shared/Decision-Memory/decision-log.md +24 -0
- package/second-brain/Shared/Memory-Inbox/memory-inbox.md +23 -0
- package/second-brain/Shared/Operating-State/current-state.md +30 -0
- package/second-brain/Shared/Provenance/ingest-log.md +27 -0
- package/second-brain/Shared/Rules/context-assembly-policy.md +28 -0
- package/second-brain/Shared/Rules/frontmatter-standard.md +33 -0
- package/second-brain/Shared/Rules/skills-admission.md +30 -0
- package/second-brain/Shared/User-Memory/user-preferences.md +25 -0
- package/second-brain/Templates/bug.md +22 -0
- package/second-brain/Templates/handoff.md +21 -0
- package/second-brain/Templates/project.md +24 -0
- package/second-brain/Templates/session.md +26 -0
- package/second-brain/USER.md +36 -0
- package/second-brain/Vault Structure Map.md +106 -0
- package/skills/agent-tool-mcp-builder/SKILL.md +88 -0
- package/skills/api-design-review/SKILL.md +70 -0
- package/skills/async-concurrency-correctness/SKILL.md +93 -0
- package/skills/audit-accessibility-wcag/SKILL.md +59 -0
- package/skills/audit-technical-seo/SKILL.md +62 -0
- package/skills/auth-jwt-session/SKILL.md +88 -0
- package/skills/brainstorm-design/SKILL.md +73 -0
- package/skills/build-etl-pipeline/SKILL.md +58 -0
- package/skills/build-form-validation/SKILL.md +103 -0
- package/skills/build-office-docs/SKILL.md +80 -0
- package/skills/build-react-component/SKILL.md +116 -0
- package/skills/build-spreadsheet/SKILL.md +106 -0
- package/skills/caching-strategy/SKILL.md +75 -0
- package/skills/cicd-pipeline-author/SKILL.md +65 -0
- package/skills/cloud-cost-optimize/SKILL.md +91 -0
- package/skills/code-comments/SKILL.md +52 -0
- package/skills/code-review/SKILL.md +61 -0
- package/skills/db-migration-safety/SKILL.md +67 -0
- package/skills/debug-frontend-browser/SKILL.md +58 -0
- package/skills/debug-root-cause/SKILL.md +54 -0
- package/skills/dependency-upgrade/SKILL.md +56 -0
- package/skills/deploy-release/SKILL.md +64 -0
- package/skills/diff-table-parity/SKILL.md +58 -0
- package/skills/dockerfile-optimize/SKILL.md +82 -0
- package/skills/error-message/SKILL.md +58 -0
- package/skills/estimate-work/SKILL.md +54 -0
- package/skills/explore-codebase/SKILL.md +73 -0
- package/skills/git-commit-pr/SKILL.md +65 -0
- package/skills/gitops-deploy-workflow/SKILL.md +97 -0
- package/skills/implement-from-design/SKILL.md +69 -0
- package/skills/incident-response-sre/SKILL.md +78 -0
- package/skills/k8s-debug-workload/SKILL.md +135 -0
- package/skills/k8s-manifest-review/SKILL.md +86 -0
- package/skills/llm-eval-harness/SKILL.md +63 -0
- package/skills/manage-client-server-state/SKILL.md +94 -0
- package/skills/mermaid-diagram/SKILL.md +61 -0
- package/skills/message-queue-jobs/SKILL.md +139 -0
- package/skills/naming-helper/SKILL.md +57 -0
- package/skills/observability-instrument/SKILL.md +113 -0
- package/skills/optimize-core-web-vitals/SKILL.md +75 -0
- package/skills/optimize-sql-query/SKILL.md +67 -0
- package/skills/performance-profiling/SKILL.md +65 -0
- package/skills/process-pdf/SKILL.md +107 -0
- package/skills/profile-dataset/SKILL.md +97 -0
- package/skills/prompt-engineering/SKILL.md +70 -0
- package/skills/rag-pipeline/SKILL.md +53 -0
- package/skills/rate-limiting/SKILL.md +96 -0
- package/skills/refactor-cleanup/SKILL.md +54 -0
- package/skills/regex-build/SKILL.md +72 -0
- package/skills/release-notes/SKILL.md +79 -0
- package/skills/rest-graphql-contract/SKILL.md +71 -0
- package/skills/scrape-structured-web-data/SKILL.md +61 -0
- package/skills/secrets-management/SKILL.md +96 -0
- package/skills/security-review/SKILL.md +62 -0
- package/skills/shell-script-robust/SKILL.md +71 -0
- package/skills/style-responsive-tailwind/SKILL.md +70 -0
- package/skills/terraform-plan-review/SKILL.md +95 -0
- package/skills/type-safety-strict/SKILL.md +82 -0
- package/skills/validate-data-quality/SKILL.md +62 -0
- package/skills/wrangle-tabular-data/SKILL.md +75 -0
- package/skills/write-adr/SKILL.md +75 -0
- package/skills/write-analytical-sql/SKILL.md +71 -0
- package/skills/write-data-viz/SKILL.md +58 -0
- package/skills/write-docs/SKILL.md +54 -0
- package/skills/write-plan/SKILL.md +59 -0
- package/skills/write-playwright-e2e/SKILL.md +86 -0
- package/skills/write-prd/SKILL.md +65 -0
- package/skills/write-rfc/SKILL.md +75 -0
- package/skills/write-tests/SKILL.md +50 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { checkWritePath } from './permission.js';
|
|
5
|
+
import { renderEditDiff } from '../diff.js';
|
|
6
|
+
/** tier 1: exact substring match + นับจำนวนครั้ง */
|
|
7
|
+
export function exactMatch(content, needle) {
|
|
8
|
+
if (needle.length === 0)
|
|
9
|
+
return null; // กัน infinite loop จาก empty needle
|
|
10
|
+
const first = content.indexOf(needle);
|
|
11
|
+
if (first === -1)
|
|
12
|
+
return null;
|
|
13
|
+
let count = 0;
|
|
14
|
+
let i = 0;
|
|
15
|
+
// i += 1 เพื่อ count overlapping occurrences ถูก (เช่น 'aaa'/'aa' = 2, '\n\n\n'/'\n\n' = 2)
|
|
16
|
+
while ((i = content.indexOf(needle, i)) !== -1) {
|
|
17
|
+
count++;
|
|
18
|
+
i += 1;
|
|
19
|
+
}
|
|
20
|
+
return { start: first, end: first + needle.length, count };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* tier 2: whitespace-flexible — เทียบทีละบรรทัดแบบ trim (indentation/trailing space ต่างได้)
|
|
24
|
+
* คืน offset ของบล็อกที่ match ในไฟล์จริง (รวม indentation เดิม)
|
|
25
|
+
*/
|
|
26
|
+
export function whitespaceFlexMatch(content, needle) {
|
|
27
|
+
const needleLines = needle.split('\n').map((l) => l.trim());
|
|
28
|
+
const contentLines = content.split('\n');
|
|
29
|
+
// offset อักขระของจุดเริ่มแต่ละบรรทัด
|
|
30
|
+
const offsets = [];
|
|
31
|
+
let acc = 0;
|
|
32
|
+
for (const l of contentLines) {
|
|
33
|
+
offsets.push(acc);
|
|
34
|
+
acc += l.length + 1; // +1 = '\n'
|
|
35
|
+
}
|
|
36
|
+
const matchStarts = [];
|
|
37
|
+
for (let i = 0; i + needleLines.length <= contentLines.length; i++) {
|
|
38
|
+
let ok = true;
|
|
39
|
+
for (let j = 0; j < needleLines.length; j++) {
|
|
40
|
+
if (contentLines[i + j].trim() !== needleLines[j]) {
|
|
41
|
+
ok = false;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (ok)
|
|
46
|
+
matchStarts.push(i);
|
|
47
|
+
}
|
|
48
|
+
if (matchStarts.length === 0)
|
|
49
|
+
return null;
|
|
50
|
+
const i = matchStarts[0];
|
|
51
|
+
const lastLineIdx = i + needleLines.length - 1;
|
|
52
|
+
const start = offsets[i];
|
|
53
|
+
const end = offsets[lastLineIdx] + contentLines[lastLineIdx].length; // ไม่รวม '\n' ท้าย
|
|
54
|
+
return { start, end, count: matchStarts.length };
|
|
55
|
+
}
|
|
56
|
+
/** หา match แบบ multi-tier: exact ก่อน แล้วค่อย whitespace-flexible */
|
|
57
|
+
export function findMatch(content, needle) {
|
|
58
|
+
return exactMatch(content, needle) ?? whitespaceFlexMatch(content, needle);
|
|
59
|
+
}
|
|
60
|
+
export const editFileTool = tool({
|
|
61
|
+
description: 'แก้ไฟล์โดยแทนที่ old_string ด้วย new_string. old_string ต้องมีอยู่จริงและ unique ในไฟล์ (ใส่ context รอบๆ ให้พอระบุตำแหน่งเดียว). อ่านไฟล์ด้วย read_file ก่อนเสมอ',
|
|
62
|
+
inputSchema: z.object({
|
|
63
|
+
path: z.string().describe('path ของไฟล์ที่จะแก้'),
|
|
64
|
+
old_string: z.string().describe('ข้อความเดิมที่จะถูกแทนที่ (ต้องตรงและ unique)'),
|
|
65
|
+
new_string: z.string().describe('ข้อความใหม่'),
|
|
66
|
+
}),
|
|
67
|
+
execute: async ({ path, old_string, new_string }) => {
|
|
68
|
+
const guard = checkWritePath(path);
|
|
69
|
+
if (!guard.ok)
|
|
70
|
+
return `BLOCKED: ${guard.reason}`;
|
|
71
|
+
if (old_string === '')
|
|
72
|
+
return `ERROR: old_string ต้องไม่ว่าง`;
|
|
73
|
+
if (old_string === new_string) {
|
|
74
|
+
return `ERROR: old_string กับ new_string เหมือนกัน — ไม่มีอะไรเปลี่ยน`;
|
|
75
|
+
}
|
|
76
|
+
let raw;
|
|
77
|
+
try {
|
|
78
|
+
raw = await readFile(path, 'utf8');
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
return `ERROR: อ่านไฟล์ "${path}" ไม่ได้ — ${err.message}`;
|
|
82
|
+
}
|
|
83
|
+
// normalize CRLF→LF เพื่อให้ match/offset consistent แล้ว restore EOL เดิมตอนเขียน
|
|
84
|
+
// (กัน flex match กิน \r แล้วทำ line ending พังบนไฟล์ Windows)
|
|
85
|
+
const usesCRLF = raw.includes('\r\n');
|
|
86
|
+
const content = usesCRLF ? raw.replace(/\r\n/g, '\n') : raw;
|
|
87
|
+
const oldNorm = old_string.replace(/\r\n/g, '\n');
|
|
88
|
+
const newNorm = new_string.replace(/\r\n/g, '\n');
|
|
89
|
+
const m = findMatch(content, oldNorm);
|
|
90
|
+
if (!m) {
|
|
91
|
+
return `ERROR: ไม่พบ old_string ในไฟล์ "${path}" — อ่านไฟล์ใหม่ด้วย read_file แล้วคัดข้อความที่ตรงเป๊ะมาใช้`;
|
|
92
|
+
}
|
|
93
|
+
if (m.count > 1) {
|
|
94
|
+
return `ERROR: old_string พบ ${m.count} ที่ในไฟล์ "${path}" (ต้อง unique) — ใส่ context รอบๆ ให้มากขึ้นเพื่อระบุตำแหน่งเดียว`;
|
|
95
|
+
}
|
|
96
|
+
let updated = content.slice(0, m.start) + newNorm + content.slice(m.end);
|
|
97
|
+
if (usesCRLF)
|
|
98
|
+
updated = updated.replace(/\n/g, '\r\n');
|
|
99
|
+
try {
|
|
100
|
+
await writeFile(path, updated, 'utf8');
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return `ERROR: เขียนไฟล์ "${path}" ไม่ได้ — ${err.message}`;
|
|
104
|
+
}
|
|
105
|
+
return `OK: แก้ "${path}" (1 ที่)\n${renderEditDiff(oldNorm, newNorm)}`;
|
|
106
|
+
},
|
|
107
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { runGit } from '../git.js';
|
|
4
|
+
const gitErr = (e) => `git error: ${e.message}`;
|
|
5
|
+
export const gitStatusTool = tool({
|
|
6
|
+
description: 'ดู git status — ไฟล์ที่เปลี่ยน/staged/untracked + branch',
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
path: z.string().optional().describe('จำกัดเฉพาะ path (ไม่ใส่ = ทั้ง repo)'),
|
|
9
|
+
}),
|
|
10
|
+
execute: async ({ path }) => {
|
|
11
|
+
try {
|
|
12
|
+
const args = ['status', '--short', '--branch', ...(path ? ['--', path] : [])];
|
|
13
|
+
return (await runGit(args)).trim() || '(clean)';
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
return gitErr(e);
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
export const gitDiffTool = tool({
|
|
21
|
+
description: 'ดู git diff — เนื้อหาที่เปลี่ยน (staged=true ดูที่ stage แล้ว)',
|
|
22
|
+
inputSchema: z.object({
|
|
23
|
+
staged: z.boolean().optional().describe('true = diff ของที่ staged แล้ว'),
|
|
24
|
+
path: z.string().optional().describe('จำกัดเฉพาะไฟล์/โฟลเดอร์'),
|
|
25
|
+
}),
|
|
26
|
+
execute: async ({ staged, path }) => {
|
|
27
|
+
try {
|
|
28
|
+
const args = ['diff', ...(staged ? ['--staged'] : []), ...(path ? ['--', path] : [])];
|
|
29
|
+
const out = await runGit(args);
|
|
30
|
+
return out.length > 20000 ? `${out.slice(0, 20000)}\n... [diff ยาว, ตัด]` : out || '(no changes)';
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
return gitErr(e);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
export const gitLogTool = tool({
|
|
38
|
+
description: 'ดู git log — commit ล่าสุด (oneline)',
|
|
39
|
+
inputSchema: z.object({
|
|
40
|
+
count: z.number().optional().describe('จำนวน commit (default 10, max 50)'),
|
|
41
|
+
}),
|
|
42
|
+
execute: async ({ count = 10 }) => {
|
|
43
|
+
try {
|
|
44
|
+
return (await runGit(['log', '--oneline', '-n', String(Math.min(Math.max(count, 1), 50))])) || '(no commits)';
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return gitErr(e);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
export const gitCommitTool = tool({
|
|
52
|
+
description: 'git commit — commit ที่ staged ไว้ (addAll=true เพื่อ git add -A ก่อน). ' +
|
|
53
|
+
'ใช้เมื่อ user สั่งให้ commit เท่านั้น. ไม่ push (push ต้องให้ user ทำเอง)',
|
|
54
|
+
inputSchema: z.object({
|
|
55
|
+
message: z.string().describe('commit message'),
|
|
56
|
+
addAll: z.boolean().optional().describe('true = git add -A ก่อน commit'),
|
|
57
|
+
}),
|
|
58
|
+
execute: async ({ message, addAll }) => {
|
|
59
|
+
try {
|
|
60
|
+
if (addAll)
|
|
61
|
+
await runGit(['add', '-A']);
|
|
62
|
+
return (await runGit(['commit', '-m', message])).trim();
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
return gitErr(e);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileTool } from './read.js';
|
|
2
|
+
import { writeFileTool } from './write.js';
|
|
3
|
+
import { editFileTool } from './edit.js';
|
|
4
|
+
import { listDirTool } from './list.js';
|
|
5
|
+
import { globTool, grepTool } from './search.js';
|
|
6
|
+
import { bashTool } from './bash.js';
|
|
7
|
+
import { rememberTool } from './remember.js';
|
|
8
|
+
import { skillTool, createSkillTool, findSkillsTool } from './skill.js';
|
|
9
|
+
import { recallTool } from './recall.js';
|
|
10
|
+
import { scheduleTaskTool, listScheduledTool, cancelScheduledTool } from './schedule.js';
|
|
11
|
+
import { taskTool } from './task.js';
|
|
12
|
+
import { gitStatusTool, gitDiffTool, gitLogTool, gitCommitTool } from './git.js';
|
|
13
|
+
/** tool registry ที่ส่งให้ agent loop */
|
|
14
|
+
export const tools = {
|
|
15
|
+
read_file: readFileTool,
|
|
16
|
+
write_file: writeFileTool,
|
|
17
|
+
edit_file: editFileTool,
|
|
18
|
+
list_dir: listDirTool,
|
|
19
|
+
glob: globTool,
|
|
20
|
+
grep: grepTool,
|
|
21
|
+
run_bash: bashTool,
|
|
22
|
+
remember: rememberTool,
|
|
23
|
+
recall: recallTool,
|
|
24
|
+
skill: skillTool,
|
|
25
|
+
find_skills: findSkillsTool,
|
|
26
|
+
create_skill: createSkillTool,
|
|
27
|
+
schedule_task: scheduleTaskTool,
|
|
28
|
+
list_scheduled: listScheduledTool,
|
|
29
|
+
cancel_scheduled: cancelScheduledTool,
|
|
30
|
+
task: taskTool,
|
|
31
|
+
git_status: gitStatusTool,
|
|
32
|
+
git_diff: gitDiffTool,
|
|
33
|
+
git_log: gitLogTool,
|
|
34
|
+
git_commit: gitCommitTool,
|
|
35
|
+
};
|
|
36
|
+
export { readFileTool, writeFileTool, editFileTool, listDirTool, globTool, grepTool, bashTool };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { readdir } from 'node:fs/promises';
|
|
4
|
+
import { clamp } from './util.js';
|
|
5
|
+
export const listDirTool = tool({
|
|
6
|
+
description: 'list ไฟล์และโฟลเดอร์ใน directory (โฟลเดอร์ลงท้ายด้วย /)',
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
path: z.string().default('.').describe('directory ที่จะ list (default: current dir)'),
|
|
9
|
+
}),
|
|
10
|
+
execute: async ({ path }) => {
|
|
11
|
+
try {
|
|
12
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
13
|
+
const out = entries
|
|
14
|
+
.filter((e) => !e.name.startsWith('.') || e.name === '.env.example' || e.name === '.gitignore')
|
|
15
|
+
.map((e) => (e.isDirectory() ? `${e.name}/` : e.name))
|
|
16
|
+
.sort()
|
|
17
|
+
.join('\n');
|
|
18
|
+
return clamp(out) || '(empty)';
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
return `ERROR: list "${path}" ไม่ได้ — ${err.message}`;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { resolve, join, sep } from 'node:path';
|
|
3
|
+
// Permission gate (M1): ก่อนมี interactive ask (M4) — hard-deny อันตราย, allow ที่เหลือ
|
|
4
|
+
// คำสั่ง shell ที่ทำลายล้าง irreversible
|
|
5
|
+
const DESTRUCTIVE_CMD = /(\brm\s+-rf\b|\bgit\s+reset\s+--hard\b|\bgit\s+push\b.*--force|\bmkfs\b|\bdd\s+if=|:\(\)\s*\{|\bchmod\s+-R\s+777\b|>\s*\/dev\/sd|\bsudo\b|\bcrontab\b)/i;
|
|
6
|
+
const HOME = homedir();
|
|
7
|
+
// ไฟล์ที่ห้ามเขียน (persistence backdoor): shell rc, git/npm config, ~/.sanook (token/mcp/hooks)
|
|
8
|
+
const PROTECTED_EXACT = new Set(['.gitconfig', '.zshrc', '.bashrc', '.bash_profile', '.profile', '.zprofile', '.npmrc'].map((f) => join(HOME, f)));
|
|
9
|
+
// โฟลเดอร์ที่ห้ามเขียนเข้าไป (credentials + sanook internal)
|
|
10
|
+
const PROTECTED_DIRS = ['.ssh', '.aws', '.gnupg', '.sanook'].map((d) => join(HOME, d));
|
|
11
|
+
// segment ที่ห้ามไม่ว่าอยู่ที่ไหน (.git internals / .env / deps / credentials dir)
|
|
12
|
+
const PROTECTED_SEGMENT = /(^|\/)(\.git|node_modules|\.ssh|\.aws|\.gnupg)(\/|$)|(^|\/)\.env($|\.)/i;
|
|
13
|
+
export function checkBash(cmd) {
|
|
14
|
+
if (DESTRUCTIVE_CMD.test(cmd)) {
|
|
15
|
+
return { ok: false, reason: `คำสั่งทำลายล้าง/irreversible ถูกปฏิเสธ: "${cmd}"` };
|
|
16
|
+
}
|
|
17
|
+
return { ok: true };
|
|
18
|
+
}
|
|
19
|
+
/** กันเขียนทับ secrets/shell-rc/.sanook — resolve เป็น absolute ก่อน (กัน ../ และ symlink-ish bypass) */
|
|
20
|
+
export function checkWritePath(path) {
|
|
21
|
+
const abs = resolve(path);
|
|
22
|
+
const inProtectedDir = PROTECTED_DIRS.some((d) => abs === d || abs.startsWith(d + sep));
|
|
23
|
+
if (PROTECTED_EXACT.has(abs) || inProtectedDir || PROTECTED_SEGMENT.test(abs)) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
reason: `path ที่ป้องกันถูกปฏิเสธ: "${path}" (secrets / shell-rc / .sanook / .git / .env / node_modules)`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return { ok: true };
|
|
30
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { clamp } from './util.js';
|
|
5
|
+
export const readFileTool = tool({
|
|
6
|
+
description: 'อ่านไฟล์ใน workspace แล้วคืนเนื้อหา (UTF-8). อ่านก่อนแก้ไฟล์เสมอ',
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
path: z.string().describe('relative หรือ absolute path ของไฟล์ที่จะอ่าน'),
|
|
9
|
+
}),
|
|
10
|
+
execute: async ({ path }) => {
|
|
11
|
+
try {
|
|
12
|
+
return clamp(await readFile(path, 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
return `ERROR: อ่านไฟล์ "${path}" ไม่ได้ — ${err.message}`;
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { recall } from '../knowledge.js';
|
|
4
|
+
/** ค้น knowledge ที่สะสม (memory + skills + session เก่า) — reuse ไม่เริ่มจากศูนย์ */
|
|
5
|
+
export const recallTool = tool({
|
|
6
|
+
description: 'ค้นความรู้ที่สะสมไว้ (สิ่งที่จำไว้, skills, งานที่เคยทำใน session เก่า) — เรียกตอนเริ่ม task ' +
|
|
7
|
+
'เพื่อ reuse ของเดิม/ไม่ลืมว่าเคยทำอะไรไปแล้ว ก่อนลงมือทำใหม่',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
query: z.string().describe('คำค้น — หัวข้อ/เทคโนโลยี/ชื่องาน'),
|
|
10
|
+
}),
|
|
11
|
+
execute: async ({ query }) => recall(query),
|
|
12
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { appendMemory } from '../memory.js';
|
|
4
|
+
export const rememberTool = tool({
|
|
5
|
+
description: 'จำข้อเท็จจริง/preference/decision สำคัญข้าม session — ใช้เมื่อเจอสิ่งที่ควรจำไว้ใช้ครั้งหน้า ' +
|
|
6
|
+
'(เช่น user ชอบ/ไม่ชอบอะไร, decision สำคัญ, convention ของ project). บันทึกลง ~/.sanook/memory',
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
fact: z.string().describe('สิ่งที่ต้องจำ — 1 ประโยคกระชับ atomic'),
|
|
9
|
+
}),
|
|
10
|
+
execute: async ({ fact }) => {
|
|
11
|
+
await appendMemory(fact);
|
|
12
|
+
return `OK: จำแล้ว — "${fact}"`;
|
|
13
|
+
},
|
|
14
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { parseSchedule } from '../gateway/schedule.js';
|
|
4
|
+
import { enqueueTask, listTasks, removeTask } from '../gateway/ledger.js';
|
|
5
|
+
/** ตั้งงานตามเวลา — agent เรียกเองเมื่อ user พูดเรื่องเวลา/รอบ ("ทุกๆ X โมง/นาที") */
|
|
6
|
+
export const scheduleTaskTool = tool({
|
|
7
|
+
description: 'ตั้งงานให้ทำตามเวลา/เป็นรอบ — เรียกเมื่อ user ขอให้ทำอะไร "ทุกๆ X" หรือ "ตอน X โมง" หรือเวลาในอนาคต. ' +
|
|
8
|
+
'งานรันโดย gateway (ต้องเปิด `sanook serve` ไว้). ' +
|
|
9
|
+
'when ใส่รูปแบบ canonical: "every 30m"/"every 2h"/"every 1d" (รอบ) · "09:00" (ทุกวันเวลานี้) · ' +
|
|
10
|
+
'ISO เช่น "2026-12-25T09:00" (ครั้งเดียว). ภาษาไทยก็ได้ ("ทุก 30 นาที", "ทุกวัน 9:00") — ' +
|
|
11
|
+
'แปลงคำพูด user เป็นรูปแบบนี้ก่อนส่ง',
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
when: z.string().describe('เวลา: every 30m / 09:00 / ISO / "ทุก 2 ชั่วโมง"'),
|
|
14
|
+
task: z.string().describe('สิ่งที่จะให้ทำตอนถึงเวลา — เขียนเป็น prompt เต็มในตัวเอง (รันเป็น fresh agent ไม่มี context นี้)'),
|
|
15
|
+
model: z.string().optional().describe('model spec (ไม่ใส่ = default ของ gateway)'),
|
|
16
|
+
}),
|
|
17
|
+
execute: async ({ when, task, model }) => {
|
|
18
|
+
const sched = parseSchedule(when, Date.now());
|
|
19
|
+
if (!sched) {
|
|
20
|
+
return `ตั้งเวลาไม่ได้: "${when}" ไม่ใช่รูปแบบที่รองรับ — ลอง "every 30m", "09:00", ISO, หรือ "ทุก 2 ชั่วโมง"`;
|
|
21
|
+
}
|
|
22
|
+
const t = await enqueueTask({
|
|
23
|
+
kind: sched.recurring ? 'cron' : 'once',
|
|
24
|
+
spec: task,
|
|
25
|
+
schedule: sched.recurring ? sched.normalized : undefined,
|
|
26
|
+
model,
|
|
27
|
+
runAt: sched.runAt,
|
|
28
|
+
});
|
|
29
|
+
const at = new Date(t.runAt).toLocaleString();
|
|
30
|
+
return (`ตั้งงาน ${t.id} แล้ว — รัน ${at}${sched.recurring ? ` แล้วทุก ${sched.normalized}` : ' (ครั้งเดียว)'}. ` +
|
|
31
|
+
`งานจะทำงานเมื่อ gateway เปิดอยู่ (sanook serve)`);
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
/** ดูงานที่ตั้งเวลาไว้ */
|
|
35
|
+
export const listScheduledTool = tool({
|
|
36
|
+
description: 'ดูงานที่ตั้งเวลาไว้ทั้งหมด (cron / one-shot) พร้อมสถานะและเวลารันถัดไป',
|
|
37
|
+
inputSchema: z.object({
|
|
38
|
+
filter: z.string().optional().describe('กรองตาม status เช่น queued/done/failed (ไม่ใส่ = ทั้งหมด)'),
|
|
39
|
+
}),
|
|
40
|
+
execute: async ({ filter }) => {
|
|
41
|
+
let tasks = await listTasks();
|
|
42
|
+
if (filter)
|
|
43
|
+
tasks = tasks.filter((t) => t.status === filter);
|
|
44
|
+
if (!tasks.length)
|
|
45
|
+
return filter ? `ไม่มีงานสถานะ ${filter}` : 'ยังไม่มีงานที่ตั้งเวลาไว้';
|
|
46
|
+
return tasks
|
|
47
|
+
.map((t) => `${t.id} [${t.status}] ${t.schedule ?? 'once'} → ${t.spec.slice(0, 60)} (next ${new Date(t.runAt).toLocaleString()})`)
|
|
48
|
+
.join('\n');
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
/** ยกเลิกงานที่ตั้งเวลาไว้ */
|
|
52
|
+
export const cancelScheduledTool = tool({
|
|
53
|
+
description: 'ยกเลิกงานที่ตั้งเวลาไว้ ด้วย task id (ดู id จาก list_scheduled)',
|
|
54
|
+
inputSchema: z.object({
|
|
55
|
+
id: z.string().describe('task id'),
|
|
56
|
+
}),
|
|
57
|
+
execute: async ({ id }) => {
|
|
58
|
+
const ok = await removeTask(id);
|
|
59
|
+
return ok ? `ยกเลิกงาน ${id} แล้ว` : `ไม่เจองาน ${id}`;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { glob } from 'node:fs/promises';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { clamp } from './util.js';
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const MAX_RESULTS = 200;
|
|
9
|
+
export const globTool = tool({
|
|
10
|
+
description: 'หาไฟล์ด้วย glob pattern (เช่น "src/**/*.ts", "**/*.json")',
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
pattern: z.string().describe('glob pattern'),
|
|
13
|
+
cwd: z.string().default('.').describe('directory ที่จะค้นจาก'),
|
|
14
|
+
}),
|
|
15
|
+
execute: async ({ pattern, cwd }) => {
|
|
16
|
+
try {
|
|
17
|
+
const out = [];
|
|
18
|
+
for await (const f of glob(pattern, { cwd })) {
|
|
19
|
+
out.push(f);
|
|
20
|
+
if (out.length >= MAX_RESULTS) {
|
|
21
|
+
out.push(`... [>${MAX_RESULTS} matches, truncated]`);
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return out.length ? out.sort().join('\n') : '(no matches)';
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
return `ERROR: glob "${pattern}" ล้มเหลว — ${err.message}`;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
export const grepTool = tool({
|
|
33
|
+
description: 'ค้นข้อความใน codebase ด้วย ripgrep (regex) — คืน file:line:text, เคารพ .gitignore',
|
|
34
|
+
inputSchema: z.object({
|
|
35
|
+
pattern: z.string().describe('regex ที่จะค้น'),
|
|
36
|
+
path: z.string().default('.').describe('directory หรือไฟล์ที่จะค้น'),
|
|
37
|
+
}),
|
|
38
|
+
execute: async ({ pattern, path }) => {
|
|
39
|
+
try {
|
|
40
|
+
// execFile (args array, ไม่ผ่าน shell) → $(...)/backtick/$VAR ใน pattern/path เป็น inert
|
|
41
|
+
// กัน command injection (JSON.stringify ไม่ใช่ shell quoting — เคยรั่ว); -e กัน pattern ขึ้นต้นด้วย -
|
|
42
|
+
const { stdout } = await execFileAsync('rg', ['--line-number', '--no-heading', '--max-count', '50', '-e', pattern, '--', path], { maxBuffer: 10 * 1024 * 1024 });
|
|
43
|
+
const lines = stdout.trim().split('\n').slice(0, MAX_RESULTS);
|
|
44
|
+
return clamp(lines.join('\n')) || '(no matches)';
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
// ripgrep exit code 1 = ไม่เจอ match (ไม่ใช่ error จริง)
|
|
48
|
+
const e = err;
|
|
49
|
+
if (e.code === 1)
|
|
50
|
+
return '(no matches)';
|
|
51
|
+
return `ERROR: grep "${pattern}" ล้มเหลว — ${err.message}`;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getSkillBody, saveSkill, loadSkills } from '../skills.js';
|
|
4
|
+
/** ค้น skill ที่เกี่ยวกับงาน — "skill ช่วยไปหา skill" (discovery) */
|
|
5
|
+
export const findSkillsTool = tool({
|
|
6
|
+
description: 'ค้น skill ที่เกี่ยวกับงานนี้ — คืน skill ที่ match (จาก built-in + ที่ติดตั้งไว้). ' +
|
|
7
|
+
'ใช้ตอนเริ่มงานเพื่อดูว่ามี skill ไหนช่วยได้ ก่อนลงมือเอง แล้วโหลดด้วย skill tool',
|
|
8
|
+
inputSchema: z.object({
|
|
9
|
+
query: z.string().describe('ประเภทงาน เช่น "review code", "debug", "deploy", "เขียน test"'),
|
|
10
|
+
}),
|
|
11
|
+
execute: async ({ query }) => {
|
|
12
|
+
const terms = query
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.split(/\s+/)
|
|
15
|
+
.filter((t) => t.length > 1);
|
|
16
|
+
const skills = await loadSkills();
|
|
17
|
+
const scored = skills
|
|
18
|
+
.map((s) => {
|
|
19
|
+
const hay = `${s.name} ${s.description} ${s.whenToUse ?? ''}`.toLowerCase();
|
|
20
|
+
return { s, score: terms.reduce((n, t) => n + (hay.includes(t) ? 1 : 0), 0) };
|
|
21
|
+
})
|
|
22
|
+
.filter((x) => x.score > 0)
|
|
23
|
+
.sort((a, b) => b.score - a.score)
|
|
24
|
+
.slice(0, 8);
|
|
25
|
+
if (!scored.length) {
|
|
26
|
+
return `ไม่เจอ skill ตรงกับ "${query}" — มี ${skills.length} skills (ดู <available_skills>)`;
|
|
27
|
+
}
|
|
28
|
+
return `${scored
|
|
29
|
+
.map(({ s }) => `- ${s.name}: ${s.description}${s.whenToUse ? ` (ใช้เมื่อ: ${s.whenToUse})` : ''}`)
|
|
30
|
+
.join('\n')}\n\nโหลดเต็มด้วย skill tool`;
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
/** โหลด skill เต็มเมื่อ task ตรงกับ available_skills */
|
|
34
|
+
export const skillTool = tool({
|
|
35
|
+
description: 'โหลดเนื้อหา skill เต็ม (วิธีทำงานเฉพาะทาง/runbook) เมื่อ task ตรงกับ skill ใน <available_skills>. ' +
|
|
36
|
+
'อ่านก่อนลงมือทำงานประเภทนั้น เพื่อทำตามขั้นตอนที่ผ่านการพิสูจน์แล้ว',
|
|
37
|
+
inputSchema: z.object({
|
|
38
|
+
name: z.string().describe('ชื่อ skill จาก <available_skills>'),
|
|
39
|
+
}),
|
|
40
|
+
execute: async ({ name }) => {
|
|
41
|
+
const body = await getSkillBody(name);
|
|
42
|
+
return body ?? `ไม่เจอ skill "${name}" — ดูชื่อที่มีใน <available_skills>`;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
/** self-improvement: agent เขียน skill ใหม่เมื่อเจอ procedure ที่น่าจะทำซ้ำอีก */
|
|
46
|
+
export const createSkillTool = tool({
|
|
47
|
+
description: 'สร้าง/อัปเดต skill ใหม่ — ใช้เมื่อเพิ่งทำงานที่ (1) มีหลายขั้นตอน (2) สำเร็จแล้ว (3) น่าจะเจออีก. ' +
|
|
48
|
+
'บันทึกขั้นตอนเป็น runbook เพื่อครั้งหน้าทำได้เร็ว/ไม่พลาดซ้ำ (เก็บใน ~/.sanook/skills/). ' +
|
|
49
|
+
'body ควรมี: When to Use, Steps, Common Errors/Gotchas',
|
|
50
|
+
inputSchema: z.object({
|
|
51
|
+
name: z.string().describe('slug a-z0-9- เช่น "deploy-vercel", "fix-eslint-flat-config"'),
|
|
52
|
+
description: z.string().describe('1 บรรทัด: skill นี้ทำอะไร'),
|
|
53
|
+
when_to_use: z.string().optional().describe('สถานการณ์ที่ควรหยิบ skill นี้มาใช้'),
|
|
54
|
+
body: z.string().describe('เนื้อหา markdown: When to Use, Steps, Common Errors'),
|
|
55
|
+
}),
|
|
56
|
+
execute: async ({ name, description, when_to_use, body }) => {
|
|
57
|
+
try {
|
|
58
|
+
const path = await saveSkill(name, description, body, when_to_use);
|
|
59
|
+
return `OK: บันทึก skill "${name}" แล้ว (${path}) — ครั้งหน้าจะเห็นใน available_skills`;
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
return `สร้าง skill ไม่ได้: ${err.message}`;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { agentContext } from '../agentContext.js';
|
|
4
|
+
import { approvalContext } from '../approval.js';
|
|
5
|
+
// task = มอบงานย่อยให้ sub-agent ทำใน context แยก (เลียน Claude Code Task tool)
|
|
6
|
+
// depth/model/budget thread ผ่าน AsyncLocalStorage (parallel-safe, ไม่ใช่ process.env)
|
|
7
|
+
const MAX_DEPTH = 2;
|
|
8
|
+
// read-only = อ่าน/ค้นเท่านั้น — ตัด run_bash ออก (shell = เลี่ยง read-only contract ได้)
|
|
9
|
+
const READ_TOOLS = ['read_file', 'list_dir', 'glob', 'grep', 'git_status', 'git_diff', 'git_log', 'skill', 'find_skills'];
|
|
10
|
+
// sub-agent ห้ามมี: task (recursion), scheduling (side-effect ที่ควรเป็น main agent)
|
|
11
|
+
const SUBAGENT_EXCLUDE = ['task', 'schedule_task', 'list_scheduled', 'cancel_scheduled'];
|
|
12
|
+
export const taskTool = tool({
|
|
13
|
+
description: 'มอบงานย่อยให้ sub-agent ทำใน context แยก — ใช้ตอนต้องสำรวจหลายไฟล์/ค้นหาเยอะแล้วอยากได้แค่บทสรุป ' +
|
|
14
|
+
'(กัน context หลักบวม). sub-agent เริ่มสะอาด ไม่เห็น conversation นี้ → เขียน prompt ให้ครบในตัว. ' +
|
|
15
|
+
'default read-only (อ่าน/ค้น); readonly=false ให้แก้ไฟล์/รัน bash ได้ด้วย',
|
|
16
|
+
inputSchema: z.object({
|
|
17
|
+
description: z.string().describe('สรุปงาน 3-5 คำ'),
|
|
18
|
+
prompt: z.string().describe('คำสั่งเต็ม self-contained ให้ sub-agent (มันไม่เห็น context นี้)'),
|
|
19
|
+
readonly: z.boolean().optional().describe('true (default) = อ่าน/ค้นเท่านั้น; false = แก้ไฟล์/bash ได้'),
|
|
20
|
+
}),
|
|
21
|
+
execute: async ({ prompt, readonly = true }) => {
|
|
22
|
+
const ctx = agentContext.getStore();
|
|
23
|
+
const depth = ctx?.depth ?? 0;
|
|
24
|
+
if (depth >= MAX_DEPTH) {
|
|
25
|
+
return 'ถึงขีดจำกัดความลึก sub-agent แล้ว (กัน spawn ไม่จบ) — ทำงานนี้เองแทน';
|
|
26
|
+
}
|
|
27
|
+
const { runAgent } = await import('../loop.js');
|
|
28
|
+
const { tools } = await import('./index.js');
|
|
29
|
+
const entries = Object.entries(tools);
|
|
30
|
+
const picked = readonly
|
|
31
|
+
? entries.filter(([k]) => READ_TOOLS.includes(k))
|
|
32
|
+
: entries.filter(([k]) => !SUBAGENT_EXCLUDE.includes(k));
|
|
33
|
+
const appr = approvalContext.getStore();
|
|
34
|
+
const { text } = await runAgent({
|
|
35
|
+
model: ctx?.model ?? 'sonnet', // inherit จาก main
|
|
36
|
+
budgetUsd: ctx?.budgetUsd, // cap เดียวกับ main (กัน sub-agent วิ่ง uncapped)
|
|
37
|
+
subagentDepth: depth + 1, // thread depth ผ่าน param — ไม่ mutate global
|
|
38
|
+
permissionMode: appr?.mode ?? 'auto', // inherit ask-mode (กัน sub-agent เลี่ยง approval)
|
|
39
|
+
approve: appr?.approve,
|
|
40
|
+
prompt,
|
|
41
|
+
maxSteps: 15,
|
|
42
|
+
tools: Object.fromEntries(picked),
|
|
43
|
+
});
|
|
44
|
+
return text || '(sub-agent ไม่มีผลลัพธ์)';
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { writeFile, mkdir, readFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname } from 'node:path';
|
|
5
|
+
import { checkWritePath } from './permission.js';
|
|
6
|
+
import { summarizeWrite } from '../diff.js';
|
|
7
|
+
export const writeFileTool = tool({
|
|
8
|
+
description: 'เขียนไฟล์ใหม่ (overwrite ถ้ามีอยู่แล้ว) — สร้าง directory ให้อัตโนมัติ. ใช้สร้างไฟล์ใหม่ทั้งไฟล์ (แก้บางส่วนใช้ edit_file)',
|
|
9
|
+
inputSchema: z.object({
|
|
10
|
+
path: z.string().describe('path ของไฟล์ที่จะเขียน'),
|
|
11
|
+
content: z.string().describe('เนื้อหาทั้งหมดของไฟล์'),
|
|
12
|
+
}),
|
|
13
|
+
execute: async ({ path, content }) => {
|
|
14
|
+
const guard = checkWritePath(path);
|
|
15
|
+
if (!guard.ok)
|
|
16
|
+
return `BLOCKED: ${guard.reason}`;
|
|
17
|
+
const previous = await readFile(path, 'utf8').catch(() => undefined); // มีอยู่เดิมไหม (โชว์ before→after)
|
|
18
|
+
try {
|
|
19
|
+
await mkdir(dirname(path), { recursive: true });
|
|
20
|
+
await writeFile(path, content, 'utf8');
|
|
21
|
+
return `OK: "${path}" — ${summarizeWrite(content, previous)}`;
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
return `ERROR: เขียนไฟล์ "${path}" ไม่ได้ — ${err.message}`;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
});
|