sanook-cli 0.5.1 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +161 -3
- package/CHANGELOG.md +148 -10
- package/README.md +255 -26
- package/README.th.md +95 -7
- package/dist/approval.js +13 -0
- package/dist/bin.js +3552 -155
- package/dist/brain-consolidate.js +335 -0
- package/dist/brain-context.js +262 -0
- package/dist/brain-doctor.js +318 -0
- package/dist/brain-eval.js +186 -0
- package/dist/brain-final.js +377 -0
- package/dist/brain-metrics.js +277 -0
- package/dist/brain-new.js +402 -0
- package/dist/brain-pack.js +210 -0
- package/dist/brain-repair.js +280 -0
- package/dist/brain-review.js +382 -0
- package/dist/brain.js +15 -1
- package/dist/brand.js +1 -1
- package/dist/cli-args.js +190 -0
- package/dist/cli-option-values.js +16 -0
- package/dist/clipboard.js +65 -0
- package/dist/commands.js +266 -27
- package/dist/compaction.js +96 -11
- package/dist/config.js +149 -33
- package/dist/context-compression.js +191 -0
- package/dist/context-pack.js +145 -0
- package/dist/cost.js +49 -15
- package/dist/dashboard/api-helpers.js +87 -0
- package/dist/dashboard/server.js +179 -0
- package/dist/dashboard/static/app.js +277 -0
- package/dist/dashboard/static/index.html +39 -0
- package/dist/dashboard/static/styles.css +85 -0
- package/dist/diff.js +10 -2
- package/dist/first-run.js +21 -0
- package/dist/gateway/auth.js +49 -9
- package/dist/gateway/bluebubbles.js +205 -0
- package/dist/gateway/config.js +929 -0
- package/dist/gateway/deliver.js +399 -0
- package/dist/gateway/discord.js +124 -0
- package/dist/gateway/doctor.js +456 -0
- package/dist/gateway/email.js +501 -0
- package/dist/gateway/googlechat.js +207 -0
- package/dist/gateway/homeassistant.js +256 -0
- package/dist/gateway/ledger.js +38 -1
- package/dist/gateway/line.js +171 -0
- package/dist/gateway/lock.js +3 -1
- package/dist/gateway/matrix.js +366 -0
- package/dist/gateway/mattermost.js +322 -0
- package/dist/gateway/ntfy.js +218 -0
- package/dist/gateway/schedule.js +31 -4
- package/dist/gateway/serve.js +267 -7
- package/dist/gateway/server.js +253 -19
- package/dist/gateway/service.js +224 -0
- package/dist/gateway/session.js +362 -0
- package/dist/gateway/signal.js +351 -0
- package/dist/gateway/slack.js +124 -0
- package/dist/gateway/sms.js +169 -0
- package/dist/gateway/targets.js +576 -0
- package/dist/gateway/teams.js +106 -0
- package/dist/gateway/telegram.js +38 -15
- package/dist/gateway/webhooks.js +220 -0
- package/dist/gateway/whatsapp.js +230 -0
- package/dist/hooks.js +13 -2
- package/dist/hotkeys.js +21 -0
- package/dist/i18n/en.js +98 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/th.js +98 -0
- package/dist/i18n/types.js +1 -0
- package/dist/insights-args.js +55 -0
- package/dist/insights.js +86 -0
- package/dist/knowledge.js +55 -29
- package/dist/loop.js +157 -29
- package/dist/lsp/index.js +23 -5
- package/dist/mcp-hub.js +33 -0
- package/dist/mcp-registry.js +494 -0
- package/dist/mcp-risk.js +71 -0
- package/dist/mcp-server.js +1 -1
- package/dist/mcp.js +120 -10
- package/dist/memory-log.js +90 -0
- package/dist/memory-store.js +37 -1
- package/dist/memory.js +148 -37
- package/dist/model-picker.js +58 -0
- package/dist/orchestrate.js +51 -19
- package/dist/personality.js +58 -0
- package/dist/plan-handoff.js +17 -0
- package/dist/polyglot.js +162 -0
- package/dist/process-runner.js +96 -0
- package/dist/project-init.js +91 -0
- package/dist/project-registry.js +143 -0
- package/dist/project-scaffold.js +124 -0
- package/dist/prompt-size.js +155 -0
- package/dist/providers/codex-login.js +138 -0
- package/dist/providers/codex.js +89 -43
- package/dist/providers/keys.js +22 -1
- package/dist/providers/models.js +2 -2
- package/dist/providers/registry.js +14 -47
- package/dist/search/chunk.js +7 -8
- package/dist/search/cli.js +83 -0
- package/dist/search/embed-store.js +3 -0
- package/dist/search/embedding-config.js +22 -0
- package/dist/search/engine.js +2 -13
- package/dist/search/indexer.js +44 -1
- package/dist/search/store.js +23 -1
- package/dist/session-distill.js +84 -0
- package/dist/session.js +92 -16
- package/dist/skill-install.js +53 -13
- package/dist/skills.js +33 -0
- package/dist/slash-completion.js +155 -0
- package/dist/support-dump.js +206 -0
- package/dist/tool-catalog.js +59 -0
- package/dist/tools/edit.js +45 -15
- package/dist/tools/git.js +10 -5
- package/dist/tools/homeassistant.js +106 -0
- package/dist/tools/index.js +10 -0
- package/dist/tools/list.js +19 -6
- package/dist/tools/permission.js +992 -12
- package/dist/tools/polyglot.js +126 -0
- package/dist/tools/read.js +16 -4
- package/dist/tools/sandbox.js +38 -13
- package/dist/tools/schedule.js +19 -3
- package/dist/tools/search.js +226 -15
- package/dist/tools/task.js +40 -9
- package/dist/tools/timeout.js +23 -3
- package/dist/tools/web-fetch-tool.js +33 -0
- package/dist/trust.js +11 -1
- package/dist/turn-retrieval.js +83 -0
- package/dist/ui/app.js +878 -32
- package/dist/ui/banner.js +78 -4
- package/dist/ui/history.js +37 -5
- package/dist/ui/markdown.js +122 -0
- package/dist/ui/mentions.js +3 -2
- package/dist/ui/overlay.js +496 -0
- package/dist/ui/queue.js +23 -0
- package/dist/ui/render.js +20 -1
- package/dist/ui/session-panel.js +115 -0
- package/dist/ui/setup-providers.js +40 -0
- package/dist/ui/setup.js +172 -46
- package/dist/ui/status.js +142 -0
- package/dist/ui/thinking-panel.js +36 -0
- package/dist/ui/tool-trail.js +97 -0
- package/dist/ui/transcript.js +26 -0
- package/dist/ui/useBusyElapsed.js +19 -0
- package/dist/ui/useEditor.js +144 -5
- package/dist/ui/useGitBranch.js +57 -0
- package/dist/update.js +56 -17
- package/dist/web-fetch.js +637 -0
- package/dist/web-surface.js +190 -0
- package/dist/worktree.js +175 -4
- package/package.json +5 -5
- package/second-brain/AGENTS.md +6 -4
- package/second-brain/CLAUDE.md +7 -1
- package/second-brain/Evals/_Index.md +10 -2
- package/second-brain/Evals/quality-ledger.md +9 -1
- package/second-brain/Evals/second-brain-benchmarks.md +62 -0
- package/second-brain/GEMINI.md +5 -4
- package/second-brain/Home.md +1 -1
- package/second-brain/Projects/_Index.md +19 -4
- package/second-brain/Projects/sanook-cli/_Index.md +30 -0
- package/second-brain/Projects/sanook-cli/context.md +35 -0
- package/second-brain/Projects/sanook-cli/current-state.md +32 -0
- package/second-brain/Projects/sanook-cli/overview.md +41 -0
- package/second-brain/Projects/sanook-cli/repo.md +34 -0
- package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +197 -0
- package/second-brain/README.md +1 -1
- package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
- package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
- package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
- package/second-brain/Research/2026-06-18-hermes-tui-parity-map.md +129 -0
- package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
- package/second-brain/Research/2026-06-19-hermes-python-architecture-for-sanook.md +49 -0
- package/second-brain/Research/2026-06-19-terminal-ui-brand-research.md +52 -0
- package/second-brain/Research/_Index.md +8 -1
- package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
- package/second-brain/Reviews/_Index.md +1 -1
- package/second-brain/Runbooks/_Index.md +6 -1
- package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
- package/second-brain/SANOOK.md +45 -0
- package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
- package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
- package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
- package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
- package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
- package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
- package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
- package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
- package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
- package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
- package/second-brain/Sessions/_Index.md +15 -1
- package/second-brain/Shared/AI-Context-Index.md +22 -0
- package/second-brain/Shared/Context-Packs/_Index.md +9 -1
- package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
- package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
- package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
- package/second-brain/Shared/Operating-State/current-state.md +14 -4
- package/second-brain/Shared/Scripts/_Index.md +3 -1
- package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +6 -1
- package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
- package/second-brain/Shared/Tech-Standards/polyglot-runtime-strategy.md +46 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
- package/second-brain/Shared/Tech-Standards/web-search-grounding-policy.md +70 -0
- package/second-brain/Shared/User-Memory/_Index.md +4 -1
- package/second-brain/Shared/User-Memory/response-examples.md +98 -0
- package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
- package/second-brain/Templates/_Index.md +9 -0
- package/second-brain/Templates/final-lite.md +111 -0
- package/second-brain/Templates/final.md +231 -0
- package/second-brain/Templates/project-workspace/_Index.md +31 -0
- package/second-brain/Templates/project-workspace/context.md +28 -0
- package/second-brain/Templates/project-workspace/current-state.md +29 -0
- package/second-brain/Templates/project-workspace/overview.md +39 -0
- package/second-brain/Templates/project-workspace/repo.md +33 -0
- package/second-brain/Vault Structure Map.md +2 -1
- package/skills/structured-output-llm/SKILL.md +1 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { findBinary } from '../lsp/servers.js';
|
|
7
|
+
import { runProcess, formatProcessResult } from '../process-runner.js';
|
|
8
|
+
import { agentCwd } from '../agentContext.js';
|
|
9
|
+
import { checkReadPath } from './permission.js';
|
|
10
|
+
import { maybeSandboxExec } from './sandbox.js';
|
|
11
|
+
import { resolveAgentPath } from './util.js';
|
|
12
|
+
const MAX_TIMEOUT_MS = 300_000;
|
|
13
|
+
// Run a runtime (python/rustc/compiled exe) under the same OS sandbox as run_bash so
|
|
14
|
+
// inline code cannot write outside the workspace (cwd + brain + tmp). No-shell: argv is
|
|
15
|
+
// wrapped directly, so there is no shell interpolation of the code/args.
|
|
16
|
+
async function runConfined(file, args, cwd, opts) {
|
|
17
|
+
const sb = await maybeSandboxExec(file, args, cwd);
|
|
18
|
+
return sb ? runProcess(sb.file, sb.args, { cwd, ...opts }) : runProcess(file, args, { cwd, ...opts });
|
|
19
|
+
}
|
|
20
|
+
const RuntimeScriptSchema = z
|
|
21
|
+
.object({
|
|
22
|
+
code: z.string().optional().describe('source code to run as a temporary script'),
|
|
23
|
+
path: z.string().optional().describe('existing script/source file to run instead of code'),
|
|
24
|
+
args: z.array(z.string()).optional().describe('argv passed to the script/program'),
|
|
25
|
+
stdin: z.string().optional().describe('stdin content for the process'),
|
|
26
|
+
timeoutMs: z.number().int().positive().max(MAX_TIMEOUT_MS).optional().describe('timeout in ms (default 120000, max 300000)'),
|
|
27
|
+
})
|
|
28
|
+
.refine((v) => Boolean(v.code) !== Boolean(v.path), 'provide exactly one of code or path');
|
|
29
|
+
async function existingSourcePath(path) {
|
|
30
|
+
const full = resolveAgentPath(path);
|
|
31
|
+
const guard = await checkReadPath(full);
|
|
32
|
+
if (!guard.ok)
|
|
33
|
+
return { ok: false, reason: guard.reason };
|
|
34
|
+
return { ok: true, path: full };
|
|
35
|
+
}
|
|
36
|
+
async function tempSource(suffix, content) {
|
|
37
|
+
const dir = await mkdtemp(join(tmpdir(), 'sanook-polyglot-'));
|
|
38
|
+
const path = join(dir, `main${suffix}`);
|
|
39
|
+
await writeFile(path, content, 'utf8');
|
|
40
|
+
return { dir, path };
|
|
41
|
+
}
|
|
42
|
+
async function findRuntime(command) {
|
|
43
|
+
const bin = await findBinary(command, agentCwd());
|
|
44
|
+
return bin ?? findBinary(command, process.cwd()) ?? null;
|
|
45
|
+
}
|
|
46
|
+
async function runPython(input) {
|
|
47
|
+
const cwd = agentCwd();
|
|
48
|
+
const python = (await findRuntime('python3')) ?? (await findRuntime('python'));
|
|
49
|
+
if (!python)
|
|
50
|
+
return 'PYTHON: ยังไม่พบ python3/python — ติดตั้ง Python 3.11+ แล้วลองใหม่';
|
|
51
|
+
let tempDir;
|
|
52
|
+
let scriptPath;
|
|
53
|
+
try {
|
|
54
|
+
if (input.path) {
|
|
55
|
+
const source = await existingSourcePath(input.path);
|
|
56
|
+
if (!source.ok)
|
|
57
|
+
return `BLOCKED: ${source.reason}`;
|
|
58
|
+
scriptPath = source.path;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const temp = await tempSource('.py', input.code ?? '');
|
|
62
|
+
tempDir = temp.dir;
|
|
63
|
+
scriptPath = temp.path;
|
|
64
|
+
}
|
|
65
|
+
const result = await runConfined(python, [scriptPath, ...(input.args ?? [])], cwd, {
|
|
66
|
+
input: input.stdin,
|
|
67
|
+
timeoutMs: input.timeoutMs,
|
|
68
|
+
});
|
|
69
|
+
return formatProcessResult(result);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
if (tempDir)
|
|
73
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function runRust(input) {
|
|
77
|
+
const cwd = agentCwd();
|
|
78
|
+
const rustc = await findRuntime('rustc');
|
|
79
|
+
if (!rustc)
|
|
80
|
+
return 'RUST: ยังไม่พบ rustc — ติดตั้ง Rust ผ่าน rustup แล้วลองใหม่';
|
|
81
|
+
let tempDir;
|
|
82
|
+
try {
|
|
83
|
+
const temp = await mkdtemp(join(tmpdir(), 'sanook-rust-'));
|
|
84
|
+
tempDir = temp;
|
|
85
|
+
const sourcePath = input.path
|
|
86
|
+
? await (async () => {
|
|
87
|
+
const source = await existingSourcePath(input.path);
|
|
88
|
+
if (!source.ok)
|
|
89
|
+
return source;
|
|
90
|
+
return { ok: true, path: source.path };
|
|
91
|
+
})()
|
|
92
|
+
: { ok: true, path: join(temp, 'main.rs') };
|
|
93
|
+
if (!sourcePath.ok)
|
|
94
|
+
return `BLOCKED: ${sourcePath.reason}`;
|
|
95
|
+
if (!input.path)
|
|
96
|
+
await writeFile(sourcePath.path, input.code ?? '', 'utf8');
|
|
97
|
+
const exe = join(temp, process.platform === 'win32' ? 'sanook-rust-helper.exe' : 'sanook-rust-helper');
|
|
98
|
+
const compile = await runConfined(rustc, ['--edition=2021', sourcePath.path, '-o', exe], cwd, {
|
|
99
|
+
timeoutMs: input.timeoutMs,
|
|
100
|
+
});
|
|
101
|
+
if (!compile.ok)
|
|
102
|
+
return `RUST COMPILE ${formatProcessResult(compile)}`;
|
|
103
|
+
const run = await runConfined(exe, input.args ?? [], cwd, {
|
|
104
|
+
input: input.stdin,
|
|
105
|
+
timeoutMs: input.timeoutMs,
|
|
106
|
+
});
|
|
107
|
+
return formatProcessResult(run);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
if (tempDir)
|
|
111
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export const pythonTool = tool({
|
|
115
|
+
description: 'รัน Python แบบ no-shell สำหรับงานที่ Python ถนัด: data/JSON/CSV transform, document/text parsing, ML/OCR helper, research script. ' +
|
|
116
|
+
'ใช้ code สำหรับ snippet สั้น หรือ path สำหรับไฟล์ .py ใน workspace. ต้องมี python3/python ใน PATH.',
|
|
117
|
+
inputSchema: RuntimeScriptSchema,
|
|
118
|
+
execute: runPython,
|
|
119
|
+
});
|
|
120
|
+
export const rustTool = tool({
|
|
121
|
+
description: 'compile+run Rust single-file/snippet แบบ no-shell สำหรับงานที่ Rust ถนัด: parser/checker ที่เร็ว, algorithm ที่ต้อง type-safe, native helper prototype. ' +
|
|
122
|
+
'ใช้ code สำหรับ main.rs ชั่วคราว หรือ path สำหรับไฟล์ .rs เดี่ยวใน workspace. ต้องมี rustc ใน PATH; งาน Cargo project ให้ใช้ run_bash ตามปกติ.',
|
|
123
|
+
inputSchema: RuntimeScriptSchema,
|
|
124
|
+
execute: runRust,
|
|
125
|
+
});
|
|
126
|
+
export const runtimeScriptSchema = RuntimeScriptSchema;
|
package/dist/tools/read.js
CHANGED
|
@@ -3,6 +3,17 @@ import { z } from 'zod';
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { clamp, resolveAgentPath } from './util.js';
|
|
5
5
|
import { checkReadPath } from './permission.js';
|
|
6
|
+
function splitReadableLines(content) {
|
|
7
|
+
if (!content)
|
|
8
|
+
return [];
|
|
9
|
+
const lines = content.split(/\r\n|\n|\r/);
|
|
10
|
+
if (/(\r\n|\n|\r)$/.test(content))
|
|
11
|
+
lines.pop();
|
|
12
|
+
return lines;
|
|
13
|
+
}
|
|
14
|
+
function normalizeReadableLineEndings(content) {
|
|
15
|
+
return content.replace(/\r\n|\r/g, '\n');
|
|
16
|
+
}
|
|
6
17
|
export const readFileTool = tool({
|
|
7
18
|
description: 'อ่านไฟล์ใน workspace (UTF-8). อ่านก่อนแก้ไฟล์เสมอ. ' +
|
|
8
19
|
'ไฟล์ใหญ่หรือต้องการแค่บางส่วน → ใส่ offset/limit อ่านเฉพาะช่วงบรรทัด (ประหยัด token มาก — คู่กับ grep ที่ให้เลขบรรทัด)',
|
|
@@ -20,12 +31,13 @@ export const readFileTool = tool({
|
|
|
20
31
|
const content = await readFile(full, 'utf8');
|
|
21
32
|
// ไม่ระบุช่วง → คืนทั้งไฟล์ (clamp) เหมือนเดิม
|
|
22
33
|
if (offset == null && limit == null)
|
|
23
|
-
return clamp(content);
|
|
34
|
+
return clamp(normalizeReadableLineEndings(content));
|
|
24
35
|
// ระบุช่วง → อ่านเฉพาะบรรทัด start..end (ส่งเฉพาะที่ต้องการเข้า context, ประหยัด token)
|
|
25
|
-
const lines = content
|
|
26
|
-
const
|
|
36
|
+
const lines = splitReadableLines(content);
|
|
37
|
+
const requestedOffset = offset ?? 1;
|
|
38
|
+
const start = Math.max(0, requestedOffset - 1);
|
|
27
39
|
if (start >= lines.length)
|
|
28
|
-
return `(ไฟล์มี ${lines.length} บรรทัด — offset ${
|
|
40
|
+
return `(ไฟล์มี ${lines.length} บรรทัด — offset ${requestedOffset} เกินช่วง)`;
|
|
29
41
|
const end = limit == null ? lines.length : Math.min(lines.length, start + limit);
|
|
30
42
|
const slice = lines.slice(start, end).join('\n');
|
|
31
43
|
return clamp(`[บรรทัด ${start + 1}-${end} จาก ${lines.length}]\n${slice}`);
|
package/dist/tools/sandbox.js
CHANGED
|
@@ -27,15 +27,12 @@ function seatbeltProfile(writable) {
|
|
|
27
27
|
' (literal "/dev/null") (literal "/dev/stdout") (literal "/dev/stderr"))',
|
|
28
28
|
].join('\n');
|
|
29
29
|
}
|
|
30
|
-
function bwrapArgs(writable,
|
|
30
|
+
function bwrapArgs(writable, tail) {
|
|
31
31
|
const binds = writable.flatMap((w) => ['--bind', w, w]);
|
|
32
|
-
return ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', ...binds,
|
|
32
|
+
return ['--ro-bind', '/', '/', '--dev', '/dev', '--proc', '/proc', ...binds, ...tail];
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* (caller รัน cmd ตรงๆ ตามเดิม). path ที่มี '"' → ข้าม sandbox (กัน profile พัง)
|
|
37
|
-
*/
|
|
38
|
-
export async function maybeSandbox(cmd, cwd = process.cwd()) {
|
|
34
|
+
// writable dirs (cwd + tmp + brain) — or null when sandbox is disabled / unusable
|
|
35
|
+
async function sandboxWritable(cwd) {
|
|
39
36
|
if (envFlag(BRAND_ENV.allowOutsideWorkspace) || envFlag('SANOOK_NO_SANDBOX'))
|
|
40
37
|
return null;
|
|
41
38
|
const writable = [canon(cwd), canon(tmpdir())];
|
|
@@ -44,18 +41,46 @@ export async function maybeSandbox(cmd, cwd = process.cwd()) {
|
|
|
44
41
|
writable.push(canon(brain));
|
|
45
42
|
if (writable.some((w) => w.includes('"')))
|
|
46
43
|
return null;
|
|
44
|
+
return writable;
|
|
45
|
+
}
|
|
46
|
+
function sandboxBin() {
|
|
47
47
|
const os = platform();
|
|
48
48
|
if (os === 'darwin') {
|
|
49
49
|
const bin = ['/usr/bin/sandbox-exec', '/usr/sbin/sandbox-exec'].find((p) => existsSync(p));
|
|
50
|
-
|
|
51
|
-
return null;
|
|
52
|
-
return { file: bin, args: ['-p', seatbeltProfile(writable), '/bin/sh', '-c', cmd] };
|
|
50
|
+
return bin ? { os, bin } : null;
|
|
53
51
|
}
|
|
54
52
|
if (os === 'linux') {
|
|
55
53
|
const bin = ['/usr/bin/bwrap', '/bin/bwrap'].find((p) => existsSync(p));
|
|
56
|
-
|
|
57
|
-
return null;
|
|
58
|
-
return { file: bin, args: bwrapArgs(writable, cmd) };
|
|
54
|
+
return bin ? { os, bin } : null;
|
|
59
55
|
}
|
|
60
56
|
return null;
|
|
61
57
|
}
|
|
58
|
+
// wrap an execFile-style argv (already split — no shell) under the sandbox tail
|
|
59
|
+
function wrap(writable, tail) {
|
|
60
|
+
const sb = sandboxBin();
|
|
61
|
+
if (!sb)
|
|
62
|
+
return null;
|
|
63
|
+
if (sb.os === 'darwin')
|
|
64
|
+
return { file: sb.bin, args: ['-p', seatbeltProfile(writable), ...tail] };
|
|
65
|
+
return { file: sb.bin, args: bwrapArgs(writable, tail) };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* คืน {file,args} สำหรับรัน cmd แบบ sandbox (ผ่าน execFile) — หรือ null ถ้าไม่มี sandbox/ปิดไว้
|
|
69
|
+
* (caller รัน cmd ตรงๆ ตามเดิม). path ที่มี '"' → ข้าม sandbox (กัน profile พัง)
|
|
70
|
+
*/
|
|
71
|
+
export async function maybeSandbox(cmd, cwd = process.cwd()) {
|
|
72
|
+
const writable = await sandboxWritable(cwd);
|
|
73
|
+
if (!writable)
|
|
74
|
+
return null;
|
|
75
|
+
return wrap(writable, ['/bin/sh', '-c', cmd]);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* เหมือน maybeSandbox แต่สำหรับ "no-shell" exec (run_python/run_rust): ห่อ argv โดยตรง
|
|
79
|
+
* ผ่าน execFile (ไม่มี shell interpolation) — confine write ให้อยู่ใน cwd+brain+tmp เท่ากับ run_bash.
|
|
80
|
+
*/
|
|
81
|
+
export async function maybeSandboxExec(file, argv, cwd = process.cwd()) {
|
|
82
|
+
const writable = await sandboxWritable(cwd);
|
|
83
|
+
if (!writable)
|
|
84
|
+
return null;
|
|
85
|
+
return wrap(writable, [file, ...argv]);
|
|
86
|
+
}
|
package/dist/tools/schedule.js
CHANGED
|
@@ -2,6 +2,7 @@ import { tool } from 'ai';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { parseSchedule } from '../gateway/schedule.js';
|
|
4
4
|
import { enqueueTask, listTasks, removeTask } from '../gateway/ledger.js';
|
|
5
|
+
import { formatTarget, parseSendTarget } from '../gateway/targets.js';
|
|
5
6
|
/** ตั้งงานตามเวลา — agent เรียกเองเมื่อ user พูดเรื่องเวลา/รอบ ("ทุกๆ X โมง/นาที") */
|
|
6
7
|
export const scheduleTaskTool = tool({
|
|
7
8
|
description: 'ตั้งงานให้ทำตามเวลา/เป็นรอบ — เรียกเมื่อ user ขอให้ทำอะไร "ทุกๆ X" หรือ "ตอน X โมง" หรือเวลาในอนาคต. ' +
|
|
@@ -13,21 +14,36 @@ export const scheduleTaskTool = tool({
|
|
|
13
14
|
when: z.string().describe('เวลา: every 30m / 09:00 / ISO / "ทุก 2 ชั่วโมง"'),
|
|
14
15
|
task: z.string().describe('สิ่งที่จะให้ทำตอนถึงเวลา — เขียนเป็น prompt เต็มในตัวเอง (รันเป็น fresh agent ไม่มี context นี้)'),
|
|
15
16
|
model: z.string().optional().describe('model spec (ไม่ใส่ = default ของ gateway)'),
|
|
17
|
+
deliver: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('ปลายทางส่งผลลัพธ์ เช่น telegram, telegram:123, discord:channel, slack:C01, mattermost:channel, homeassistant:notification_id, email:owner@example.com, line:U123, sms:+15551234567, ntfy:topic, signal:+15551234567, whatsapp:15551234567, matrix:!room:server, googlechat:spaces/AAA, bluebubbles:user@example.com, teams'),
|
|
16
21
|
}),
|
|
17
|
-
execute: async ({ when, task, model }) => {
|
|
22
|
+
execute: async ({ when, task, model, deliver }) => {
|
|
18
23
|
const sched = parseSchedule(when, Date.now());
|
|
19
24
|
if (!sched) {
|
|
20
25
|
return `ตั้งเวลาไม่ได้: "${when}" ไม่ใช่รูปแบบที่รองรับ — ลอง "every 30m", "09:00", ISO, หรือ "ทุก 2 ชั่วโมง"`;
|
|
21
26
|
}
|
|
27
|
+
let normalizedDeliver;
|
|
28
|
+
if (deliver?.trim()) {
|
|
29
|
+
try {
|
|
30
|
+
normalizedDeliver = formatTarget(parseSendTarget(deliver));
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
return `ตั้งปลายทางส่งผลลัพธ์ไม่ได้: ${e.message}`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
22
36
|
const t = await enqueueTask({
|
|
23
37
|
kind: sched.recurring ? 'cron' : 'once',
|
|
24
38
|
spec: task,
|
|
25
39
|
schedule: sched.recurring ? sched.normalized : undefined,
|
|
26
40
|
model,
|
|
41
|
+
deliver: normalizedDeliver,
|
|
27
42
|
runAt: sched.runAt,
|
|
28
43
|
});
|
|
29
44
|
const at = new Date(t.runAt).toLocaleString();
|
|
30
|
-
return (`ตั้งงาน ${t.id} แล้ว — รัน ${at}${sched.recurring ? ` แล้วทุก ${sched.normalized}` : ' (ครั้งเดียว)'}
|
|
45
|
+
return (`ตั้งงาน ${t.id} แล้ว — รัน ${at}${sched.recurring ? ` แล้วทุก ${sched.normalized}` : ' (ครั้งเดียว)'}` +
|
|
46
|
+
`${normalizedDeliver ? ` และส่งผลลัพธ์ไป ${normalizedDeliver}` : ''}. ` +
|
|
31
47
|
`งานจะทำงานเมื่อ gateway เปิดอยู่ (sanook serve)`);
|
|
32
48
|
},
|
|
33
49
|
});
|
|
@@ -44,7 +60,7 @@ export const listScheduledTool = tool({
|
|
|
44
60
|
if (!tasks.length)
|
|
45
61
|
return filter ? `ไม่มีงานสถานะ ${filter}` : 'ยังไม่มีงานที่ตั้งเวลาไว้';
|
|
46
62
|
return tasks
|
|
47
|
-
.map((t) => `${t.id} [${t.status}] ${t.schedule ?? 'once'} → ${t.spec.slice(0, 60)} (next ${new Date(t.runAt).toLocaleString()})`)
|
|
63
|
+
.map((t) => `${t.id} [${t.status}] ${t.schedule ?? 'once'}${t.deliver ? ` to:${t.deliver}` : ''} → ${t.spec.slice(0, 60)} (next ${new Date(t.runAt).toLocaleString()})`)
|
|
48
64
|
.join('\n');
|
|
49
65
|
},
|
|
50
66
|
});
|
package/dist/tools/search.js
CHANGED
|
@@ -9,19 +9,203 @@ import { checkReadPath } from './permission.js';
|
|
|
9
9
|
import { agentCwd } from '../agentContext.js';
|
|
10
10
|
// pure-JS grep fallback — ใช้เมื่อ ripgrep (rg) ไม่ได้ติดตั้ง (เช่น Windows สะอาด) → grep ใช้ได้ทุกแพลตฟอร์ม
|
|
11
11
|
const FALLBACK_IGNORE = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.cache', '.turbo', '.vercel', 'vendor']);
|
|
12
|
+
const FALLBACK_IGNORE_FILES = new Set(['.ds_store', '.localized', 'desktop.ini', 'thumbs.db']);
|
|
12
13
|
const FALLBACK_MAX_FILE = 2 * 1024 * 1024; // ข้ามไฟล์ใหญ่ (กันช้า/binary)
|
|
13
14
|
const PER_FILE_CAP = 50; // เหมือน rg --max-count 50
|
|
15
|
+
function isFallbackIgnoredFile(name) {
|
|
16
|
+
return FALLBACK_IGNORE_FILES.has(name.toLowerCase()) || name.startsWith('._');
|
|
17
|
+
}
|
|
18
|
+
function isFallbackIgnoredDir(name) {
|
|
19
|
+
return FALLBACK_IGNORE.has(name.toLowerCase()) || name.startsWith('.');
|
|
20
|
+
}
|
|
21
|
+
function otherAsciiCase(ch) {
|
|
22
|
+
const code = ch.charCodeAt(0);
|
|
23
|
+
if (code >= 65 && code <= 90)
|
|
24
|
+
return ch.toLowerCase();
|
|
25
|
+
if (code >= 97 && code <= 122)
|
|
26
|
+
return ch.toUpperCase();
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
function isAsciiLower(ch) {
|
|
30
|
+
const code = ch.charCodeAt(0);
|
|
31
|
+
return code >= 97 && code <= 122;
|
|
32
|
+
}
|
|
33
|
+
function isAsciiUpper(ch) {
|
|
34
|
+
const code = ch.charCodeAt(0);
|
|
35
|
+
return code >= 65 && code <= 90;
|
|
36
|
+
}
|
|
37
|
+
function findCharClassEnd(source, start) {
|
|
38
|
+
let escaping = false;
|
|
39
|
+
const literalRightBracket = source[start] === '^' ? start + 1 : start;
|
|
40
|
+
for (let i = start; i < source.length; i += 1) {
|
|
41
|
+
const ch = source[i];
|
|
42
|
+
if (escaping) {
|
|
43
|
+
escaping = false;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ch === '\\') {
|
|
47
|
+
escaping = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (ch === ']' && i !== literalRightBracket)
|
|
51
|
+
return i;
|
|
52
|
+
}
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
function findScopedGroupEnd(source, start) {
|
|
56
|
+
let depth = 1;
|
|
57
|
+
let escaping = false;
|
|
58
|
+
for (let i = start; i < source.length; i += 1) {
|
|
59
|
+
const ch = source[i];
|
|
60
|
+
if (escaping) {
|
|
61
|
+
escaping = false;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (ch === '\\') {
|
|
65
|
+
escaping = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (ch === '[') {
|
|
69
|
+
const end = findCharClassEnd(source, i + 1);
|
|
70
|
+
if (end < 0)
|
|
71
|
+
return -1;
|
|
72
|
+
i = end;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (ch === '(')
|
|
76
|
+
depth += 1;
|
|
77
|
+
if (ch === ')') {
|
|
78
|
+
depth -= 1;
|
|
79
|
+
if (depth === 0)
|
|
80
|
+
return i;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return -1;
|
|
84
|
+
}
|
|
85
|
+
function foldAsciiRegexCharClass(source) {
|
|
86
|
+
let out = '';
|
|
87
|
+
const literalRightBracket = source[0] === '^' ? 1 : 0;
|
|
88
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
89
|
+
const ch = source[i];
|
|
90
|
+
if (ch === ']' && i === literalRightBracket) {
|
|
91
|
+
out += '\\]';
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (ch === '\\' && i + 1 < source.length) {
|
|
95
|
+
out += `${ch}${source[i + 1]}`;
|
|
96
|
+
i += 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (i + 2 < source.length &&
|
|
100
|
+
source[i + 1] === '-' &&
|
|
101
|
+
source[i + 2] !== ']' &&
|
|
102
|
+
((isAsciiLower(ch) && isAsciiLower(source[i + 2])) || (isAsciiUpper(ch) && isAsciiUpper(source[i + 2]))) &&
|
|
103
|
+
ch.charCodeAt(0) <= source[i + 2].charCodeAt(0)) {
|
|
104
|
+
out += `${ch}-${source[i + 2]}${otherAsciiCase(ch)}-${otherAsciiCase(source[i + 2])}`;
|
|
105
|
+
i += 2;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const other = i === 0 && ch === '^' ? undefined : otherAsciiCase(ch);
|
|
109
|
+
out += other ? `${ch}${other}` : ch;
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
function foldAsciiRegexLetters(source) {
|
|
114
|
+
let out = '';
|
|
115
|
+
let escaping = false;
|
|
116
|
+
for (let i = 0; i < source.length; i += 1) {
|
|
117
|
+
const ch = source[i];
|
|
118
|
+
if (escaping) {
|
|
119
|
+
out += ch;
|
|
120
|
+
escaping = false;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (ch === '\\') {
|
|
124
|
+
out += ch;
|
|
125
|
+
escaping = true;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ch === '[') {
|
|
129
|
+
const end = findCharClassEnd(source, i + 1);
|
|
130
|
+
if (end < 0) {
|
|
131
|
+
out += ch;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
out += `[${foldAsciiRegexCharClass(source.slice(i + 1, end))}]`;
|
|
135
|
+
i = end;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const other = otherAsciiCase(ch);
|
|
139
|
+
out += other ? `[${ch}${other}]` : ch;
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
function expandScopedCaseInsensitiveGroups(pattern) {
|
|
144
|
+
let out = '';
|
|
145
|
+
let changed = false;
|
|
146
|
+
let escaping = false;
|
|
147
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
148
|
+
const ch = pattern[i];
|
|
149
|
+
if (escaping) {
|
|
150
|
+
out += ch;
|
|
151
|
+
escaping = false;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (ch === '\\') {
|
|
155
|
+
out += ch;
|
|
156
|
+
escaping = true;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (ch === '[') {
|
|
160
|
+
const end = findCharClassEnd(pattern, i + 1);
|
|
161
|
+
if (end < 0)
|
|
162
|
+
return undefined;
|
|
163
|
+
out += pattern.slice(i, end + 1);
|
|
164
|
+
i = end;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (!pattern.startsWith('(?i:', i)) {
|
|
168
|
+
out += ch;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const end = findScopedGroupEnd(pattern, i + 4);
|
|
172
|
+
if (end < 0)
|
|
173
|
+
return undefined;
|
|
174
|
+
out += `(?:${foldAsciiRegexLetters(pattern.slice(i + 4, end))})`;
|
|
175
|
+
i = end;
|
|
176
|
+
changed = true;
|
|
177
|
+
}
|
|
178
|
+
return changed ? out : undefined;
|
|
179
|
+
}
|
|
180
|
+
function compileFallbackRegex(pattern) {
|
|
181
|
+
const caseInsensitive = pattern.match(/^\(\?i\)([\s\S]*)$/);
|
|
182
|
+
if (caseInsensitive) {
|
|
183
|
+
const source = expandScopedCaseInsensitiveGroups(caseInsensitive[1]) ?? caseInsensitive[1];
|
|
184
|
+
return new RegExp(source, 'i');
|
|
185
|
+
}
|
|
186
|
+
const scopedCaseInsensitive = expandScopedCaseInsensitiveGroups(pattern);
|
|
187
|
+
if (scopedCaseInsensitive)
|
|
188
|
+
return new RegExp(scopedCaseInsensitive);
|
|
189
|
+
return new RegExp(pattern); // rg ใช้ Rust regex; JS regex ใกล้เคียงพอสำหรับ pattern ทั่วไป
|
|
190
|
+
}
|
|
14
191
|
export async function jsGrep(pattern, base, target) {
|
|
15
192
|
let re;
|
|
16
193
|
try {
|
|
17
|
-
re =
|
|
194
|
+
re = compileFallbackRegex(pattern);
|
|
18
195
|
}
|
|
19
196
|
catch {
|
|
20
197
|
return `ERROR: grep regex ไม่ถูกต้อง: "${pattern}"`;
|
|
21
198
|
}
|
|
22
199
|
const root = isAbsolute(target) ? target : join(base, target);
|
|
200
|
+
const rootGuard = await checkReadPath(root);
|
|
201
|
+
if (!rootGuard.ok)
|
|
202
|
+
return `BLOCKED: ${rootGuard.reason}`;
|
|
23
203
|
const out = [];
|
|
204
|
+
let truncated = false;
|
|
24
205
|
const scanFile = async (full) => {
|
|
206
|
+
const guard = await checkReadPath(full);
|
|
207
|
+
if (!guard.ok)
|
|
208
|
+
return;
|
|
25
209
|
let s;
|
|
26
210
|
try {
|
|
27
211
|
s = await stat(full);
|
|
@@ -41,10 +225,14 @@ export async function jsGrep(pattern, base, target) {
|
|
|
41
225
|
if (content.includes('\u0000'))
|
|
42
226
|
return; // binary
|
|
43
227
|
const rel = relative(base, full) || full;
|
|
44
|
-
const lines = content.split(/\r
|
|
228
|
+
const lines = content.split(/\r\n|\n|\r/);
|
|
45
229
|
let perFile = 0;
|
|
46
|
-
for (let i = 0; i < lines.length &&
|
|
230
|
+
for (let i = 0; i < lines.length && !truncated; i++) {
|
|
47
231
|
if (re.test(lines[i])) {
|
|
232
|
+
if (out.length >= MAX_RESULTS) {
|
|
233
|
+
truncated = true;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
48
236
|
out.push(`${rel}:${i + 1}:${lines[i].slice(0, 300)}`);
|
|
49
237
|
if (++perFile >= PER_FILE_CAP)
|
|
50
238
|
break;
|
|
@@ -52,7 +240,7 @@ export async function jsGrep(pattern, base, target) {
|
|
|
52
240
|
}
|
|
53
241
|
};
|
|
54
242
|
const walk = async (dir) => {
|
|
55
|
-
if (
|
|
243
|
+
if (truncated)
|
|
56
244
|
return;
|
|
57
245
|
let entries;
|
|
58
246
|
try {
|
|
@@ -61,15 +249,19 @@ export async function jsGrep(pattern, base, target) {
|
|
|
61
249
|
catch {
|
|
62
250
|
return;
|
|
63
251
|
}
|
|
64
|
-
for (const e of entries) {
|
|
65
|
-
if (
|
|
252
|
+
for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
|
|
253
|
+
if (truncated)
|
|
66
254
|
return;
|
|
255
|
+
const full = join(dir, e.name);
|
|
256
|
+
const guard = await checkReadPath(full);
|
|
257
|
+
if (!guard.ok)
|
|
258
|
+
continue;
|
|
67
259
|
if (e.isDirectory()) {
|
|
68
|
-
if (!
|
|
69
|
-
await walk(
|
|
260
|
+
if (!isFallbackIgnoredDir(e.name))
|
|
261
|
+
await walk(full);
|
|
70
262
|
}
|
|
71
|
-
else if (e.isFile()) {
|
|
72
|
-
await scanFile(
|
|
263
|
+
else if (e.isFile() && !isFallbackIgnoredFile(e.name)) {
|
|
264
|
+
await scanFile(full);
|
|
73
265
|
}
|
|
74
266
|
}
|
|
75
267
|
};
|
|
@@ -86,10 +278,22 @@ export async function jsGrep(pattern, base, target) {
|
|
|
86
278
|
await walk(root);
|
|
87
279
|
if (!out.length)
|
|
88
280
|
return '(no matches)';
|
|
281
|
+
if (truncated)
|
|
282
|
+
out.push(`... [>${MAX_RESULTS} matches, truncated]`);
|
|
89
283
|
return `${clamp(out.join('\n'))}\n[JS fallback — ติดตั้ง ripgrep (rg) เพื่อความเร็ว + เคารพ .gitignore: brew/apt/choco/scoop install ripgrep]`;
|
|
90
284
|
}
|
|
91
285
|
const execFileAsync = promisify(execFile);
|
|
92
286
|
const MAX_RESULTS = 200;
|
|
287
|
+
export function formatRipgrepOutput(stdout) {
|
|
288
|
+
const text = stdout.replace(/(?:\r\n|\n|\r)$/, '');
|
|
289
|
+
if (!text)
|
|
290
|
+
return '(no matches)';
|
|
291
|
+
const allLines = text.split(/\r\n|\n|\r/);
|
|
292
|
+
const lines = allLines.slice(0, MAX_RESULTS);
|
|
293
|
+
if (allLines.length > MAX_RESULTS)
|
|
294
|
+
lines.push(`... [>${MAX_RESULTS} matches, truncated]`);
|
|
295
|
+
return clamp(lines.join('\n')) || '(no matches)';
|
|
296
|
+
}
|
|
93
297
|
function unsafeGlobPattern(pattern) {
|
|
94
298
|
return isAbsolute(pattern) || pattern.split(/[\\/]+/).includes('..');
|
|
95
299
|
}
|
|
@@ -109,14 +313,22 @@ export const globTool = tool({
|
|
|
109
313
|
return `BLOCKED: ${guard.reason}`;
|
|
110
314
|
try {
|
|
111
315
|
const out = [];
|
|
316
|
+
let truncated = false;
|
|
112
317
|
for await (const f of glob(pattern, { cwd: base })) {
|
|
113
|
-
|
|
318
|
+
const match = String(f);
|
|
319
|
+
const itemGuard = await checkReadPath(join(base, match));
|
|
320
|
+
if (!itemGuard.ok)
|
|
321
|
+
continue;
|
|
114
322
|
if (out.length >= MAX_RESULTS) {
|
|
115
|
-
|
|
323
|
+
truncated = true;
|
|
116
324
|
break;
|
|
117
325
|
}
|
|
326
|
+
out.push(match);
|
|
118
327
|
}
|
|
119
|
-
|
|
328
|
+
out.sort();
|
|
329
|
+
if (truncated)
|
|
330
|
+
out.push(`... [>${MAX_RESULTS} matches, truncated]`);
|
|
331
|
+
return out.length ? out.join('\n') : '(no matches)';
|
|
120
332
|
}
|
|
121
333
|
catch (err) {
|
|
122
334
|
return `ERROR: glob "${pattern}" ล้มเหลว — ${err.message}`;
|
|
@@ -138,8 +350,7 @@ export const grepTool = tool({
|
|
|
138
350
|
// execFile (args array, ไม่ผ่าน shell) → $(...)/backtick/$VAR ใน pattern/path เป็น inert
|
|
139
351
|
// กัน command injection (JSON.stringify ไม่ใช่ shell quoting — เคยรั่ว); -e กัน pattern ขึ้นต้นด้วย -
|
|
140
352
|
const { stdout } = await execFileAsync('rg', ['--line-number', '--no-heading', '--max-count', '50', '-e', pattern, '--', path], { cwd: base, maxBuffer: 10 * 1024 * 1024 });
|
|
141
|
-
|
|
142
|
-
return clamp(lines.join('\n')) || '(no matches)';
|
|
353
|
+
return formatRipgrepOutput(stdout);
|
|
143
354
|
}
|
|
144
355
|
catch (err) {
|
|
145
356
|
// ripgrep exit code 1 = ไม่เจอ match (ไม่ใช่ error จริง)
|